From a8c95411ab0ff3895869b2339ce85f573e259d35 Mon Sep 17 00:00:00 2001 From: DotNet Bot Date: Tue, 23 May 2023 09:38:10 +1000 Subject: [PATCH] Initial commit --- .config/dotnet-tools.json | 30 + .devcontainer/devcontainer.json | 47 + .devcontainer/scripts/onCreateCommand.sh | 12 + .devcontainer/scripts/postCreateCommand.sh | 6 + .editorconfig | 22 + .gitattributes | 63 + .github/ISSUE_TEMPLATE/01_bug_report.yml | 83 + .github/ISSUE_TEMPLATE/02_api_proposal.yml | 74 + .github/ISSUE_TEMPLATE/03_blank_issue.md | 8 + .github/ISSUE_TEMPLATE/config.yml | 14 + .gitignore | 313 + .spelling | 392 + CODE_OF_CONDUCT.md | 9 + CONTRIBUTING.md | 191 + Directory.Build.props | 107 + Directory.Build.targets | 84 + Directory.Packages.props | 6 + LICENSE | 23 + NuGet.config | 37 + README.md | 58 + SECURITY.md | 41 + azure-pipelines.yml | 286 + bench/.editorconfig | 7198 ++++++++++++++++ bench/Directory.Build.props | 18 + bench/Directory.Build.targets | 3 + .../EnumStrings.cs | 163 + ...ft.Gen.EnumStrings.PerformanceTests.csproj | 16 + .../Program.cs | 21 + .../README.md | 21 + .../Log.cs | 25 + .../LogMethod.cs | 109 + ...rosoft.Gen.Logging.PerformanceTests.csproj | 13 + .../MockLogger.cs | 82 + .../Program.cs | 21 + .../Constants.cs | 15 + ...pNetCore.Telemetry.PerformanceTests.csproj | 15 + .../Program.cs | 21 + .../RedactionBenchmark.cs | 346 + .../RouteSegment.cs | 17 + .../Benchmark.cs | 46 + .../FaultInjectionRequestBenchmarks.cs | 67 + .../HttpClientFactory.cs | 99 + ...ns.Http.Resilience.PerformanceTests.csproj | 13 + .../NoRemoteCallHandler.cs | 28 + .../Program.cs | 24 + .../BenchEnricher.cs | 22 + .../HugeHttpCLientLoggingBenchmark.cs | 202 + .../MediumHttpClientLoggingBenchmark.cs | 202 + .../SmallHttpClientLoggingBenchmark.cs | 202 + .../DropMessageLogger.cs | 29 + .../DropMessageLoggerProvider.cs | 18 + .../HttpClientFactory.cs | 180 + .../HugeBody.txt | 1 + .../MediumBody.txt | 1 + ...ons.Http.Telemetry.PerformanceTests.csproj | 17 + .../NoRemoteCallHandler.cs | 53 + .../NoRemoteCallNotSeekableHandler.cs | 53 + .../NotSeekableStream.cs | 47 + .../Program.cs | 31 + .../RedactionProcessorBench.cs | 145 + .../SmallBody.txt | 1 + .../Hedging.cs | 104 + .../Internals/HedgingUtilities.cs | 72 + ...ensions.Resilience.PerformanceTests.csproj | 11 + .../PipelineProvider.cs | 30 + .../Pipelines.cs | 77 + .../Program.cs | 23 + build.cmd | 32 + build.sh | 19 + docs/building.md | 159 + eng/AfterSolutionBuild.targets | 5 + eng/Build.props | 15 + eng/CodeCoverage.config | 37 + eng/Common.runsettings | 20 + eng/CredScanSuppressions.json | 1 + eng/MSBuild/Analyzers.props | 33 + eng/MSBuild/Analyzers.targets | 110 + eng/MSBuild/Generators.props | 21 + eng/MSBuild/Generators.targets | 10 + eng/MSBuild/LegacySupport.props | 57 + eng/MSBuild/LegacySupport.targets | 6 + ...ultiTargetRoslynComponent.targets.template | 69 + eng/MSBuild/Packaging.props | 31 + eng/MSBuild/Packaging.targets | 90 + eng/MSBuild/ProjectStaging.targets | 24 + eng/MSBuild/Shared.props | 42 + eng/MSBuild/Shared.targets | 31 + eng/MSSharedLibSN2048.snk | Bin 0 -> 288 bytes eng/PSScriptAnalyzerSettings.psd1 | 15 + eng/Packages/BuildOnly.props | 21 + eng/Packages/General-latest.props | 35 + eng/Packages/General-net462.props | 47 + eng/Packages/General-net6.0.props | 36 + eng/Packages/General-netcoreapp3.1.props | 40 + eng/Packages/General.props | 45 + eng/Packages/Packages.sidepush.config | 30 + eng/Packages/Packages.sign3rdparty.config | 9 + eng/Packages/TestOnly-latest.props | 8 + eng/Packages/TestOnly-net462.props | 8 + eng/Packages/TestOnly-net6.0.props | 8 + eng/Packages/TestOnly-netcoreapp3.1.props | 8 + eng/Packages/TestOnly.props | 46 + eng/Publishing.props | 6 + eng/Stylecop.json | 27 + eng/Version.Details.xml | 178 + eng/Versions.props | 82 + eng/_._ | 0 eng/build.proj | 12 + eng/build.ps1 | 177 + eng/build.sh | 125 + .../build-configuration.json | 4 + eng/common/CIBuild.cmd | 2 + eng/common/PSScriptAnalyzerSettings.psd1 | 11 + eng/common/README.md | 28 + eng/common/SetupNugetSources.ps1 | 167 + eng/common/SetupNugetSources.sh | 171 + eng/common/build.ps1 | 166 + eng/common/build.sh | 247 + eng/common/cibuild.sh | 16 + eng/common/cross/arm/sources.list.bionic | 11 + eng/common/cross/arm/sources.list.focal | 11 + eng/common/cross/arm/sources.list.jammy | 11 + eng/common/cross/arm/sources.list.jessie | 3 + eng/common/cross/arm/sources.list.xenial | 11 + eng/common/cross/arm/sources.list.zesty | 11 + eng/common/cross/arm/tizen/tizen.patch | 9 + eng/common/cross/arm64/sources.list.bionic | 11 + eng/common/cross/arm64/sources.list.buster | 11 + eng/common/cross/arm64/sources.list.focal | 11 + eng/common/cross/arm64/sources.list.jammy | 11 + eng/common/cross/arm64/sources.list.stretch | 12 + eng/common/cross/arm64/sources.list.xenial | 11 + eng/common/cross/arm64/sources.list.zesty | 11 + eng/common/cross/arm64/tizen/tizen.patch | 9 + eng/common/cross/armel/armel.jessie.patch | 43 + eng/common/cross/armel/sources.list.jessie | 3 + eng/common/cross/armel/tizen/tizen.patch | 9 + eng/common/cross/armv6/sources.list.buster | 2 + eng/common/cross/build-android-rootfs.sh | 131 + eng/common/cross/build-rootfs.sh | 648 ++ eng/common/cross/ppc64le/sources.list.bionic | 11 + eng/common/cross/riscv64/sources.list.sid | 1 + eng/common/cross/s390x/sources.list.bionic | 11 + eng/common/cross/tizen-build-rootfs.sh | 61 + eng/common/cross/tizen-fetch.sh | 172 + eng/common/cross/toolchain.cmake | 377 + eng/common/darc-init.ps1 | 47 + eng/common/darc-init.sh | 82 + eng/common/dotnet-install.cmd | 2 + eng/common/dotnet-install.ps1 | 28 + eng/common/dotnet-install.sh | 87 + eng/common/enable-cross-org-publishing.ps1 | 13 + eng/common/generate-locproject.ps1 | 189 + eng/common/generate-sbom-prep.ps1 | 21 + eng/common/generate-sbom-prep.sh | 34 + eng/common/helixpublish.proj | 26 + eng/common/init-tools-native.cmd | 3 + eng/common/init-tools-native.ps1 | 203 + eng/common/init-tools-native.sh | 238 + eng/common/internal-feed-operations.ps1 | 132 + eng/common/internal-feed-operations.sh | 141 + eng/common/internal/Directory.Build.props | 4 + eng/common/internal/NuGet.config | 7 + eng/common/internal/Tools.csproj | 30 + eng/common/loc/P22DotNetHtmlLocalization.lss | Bin 0 -> 3810 bytes eng/common/msbuild.ps1 | 28 + eng/common/msbuild.sh | 58 + eng/common/native/CommonLibrary.psm1 | 400 + eng/common/native/common-library.sh | 172 + eng/common/native/init-compiler.sh | 137 + eng/common/native/install-cmake-test.sh | 117 + eng/common/native/install-cmake.sh | 117 + eng/common/native/install-tool.ps1 | 132 + eng/common/pipeline-logging-functions.ps1 | 260 + eng/common/pipeline-logging-functions.sh | 206 + .../post-build/add-build-to-channel.ps1 | 48 + .../post-build/check-channel-consistency.ps1 | 40 + eng/common/post-build/nuget-validation.ps1 | 24 + eng/common/post-build/post-build-utils.ps1 | 91 + eng/common/post-build/publish-using-darc.ps1 | 54 + .../post-build/sourcelink-validation.ps1 | 319 + eng/common/post-build/symbols-validation.ps1 | 339 + .../post-build/trigger-subscriptions.ps1 | 64 + eng/common/retain-build.ps1 | 45 + eng/common/sdk-task.ps1 | 97 + eng/common/sdl/NuGet.config | 18 + eng/common/sdl/configure-sdl-tool.ps1 | 116 + eng/common/sdl/execute-all-sdl-tools.ps1 | 165 + eng/common/sdl/extract-artifact-archives.ps1 | 63 + eng/common/sdl/extract-artifact-packages.ps1 | 80 + eng/common/sdl/init-sdl.ps1 | 55 + eng/common/sdl/packages.config | 4 + eng/common/sdl/run-sdl.ps1 | 49 + eng/common/sdl/sdl.ps1 | 38 + eng/common/templates/job/execute-sdl.yml | 134 + eng/common/templates/job/job.yml | 251 + eng/common/templates/job/onelocbuild.yml | 109 + .../templates/job/publish-build-assets.yml | 151 + eng/common/templates/job/source-build.yml | 66 + .../templates/job/source-index-stage1.yml | 67 + eng/common/templates/jobs/codeql-build.yml | 31 + eng/common/templates/jobs/jobs.yml | 97 + eng/common/templates/jobs/source-build.yml | 46 + .../templates/post-build/common-variables.yml | 22 + .../templates/post-build/post-build.yml | 281 + .../post-build/setup-maestro-vars.yml | 70 + .../post-build/trigger-subscription.yml | 13 + .../templates/steps/add-build-to-channel.yml | 13 + eng/common/templates/steps/build-reason.yml | 12 + .../templates/steps/component-governance.yml | 13 + eng/common/templates/steps/execute-codeql.yml | 32 + eng/common/templates/steps/execute-sdl.yml | 88 + eng/common/templates/steps/generate-sbom.yml | 48 + eng/common/templates/steps/publish-logs.yml | 23 + eng/common/templates/steps/retain-build.yml | 28 + eng/common/templates/steps/run-on-unix.yml | 7 + eng/common/templates/steps/run-on-windows.yml | 7 + .../steps/run-script-ifequalelse.yml | 33 + eng/common/templates/steps/send-to-helix.yml | 91 + eng/common/templates/steps/source-build.yml | 114 + eng/common/templates/steps/telemetry-end.yml | 102 + .../templates/steps/telemetry-start.yml | 241 + .../templates/variables/pool-providers.yml | 57 + .../templates/variables/sdl-variables.yml | 7 + eng/common/tools.ps1 | 945 +++ eng/common/tools.sh | 587 ++ eng/pipelines/templates/BuildAndTest.yml | 108 + .../templates/TestCoverageReport.yml | 75 + eng/spellchecking_exclusions.dic | Bin 0 -> 26 bytes eng/stryker-config.json | 30 + eng/xunit.runner.json | 4 + global.json | 24 + restore.cmd | 9 + restore.sh | 6 + scripts/Slngen.Tests.ps1 | 175 + scripts/Slngen.ps1 | 224 + scripts/SlngenReferencing.ps1 | 59 + scripts/ValidateProjectCoverage.ps1 | 143 + src/.editorconfig | 7222 ++++++++++++++++ src/Analyzers/Directory.Build.props | 9 + src/Analyzers/Directory.Build.targets | 8 + .../AsyncCallInsideUsingBlockAnalyzer.cs | 222 + .../Common/AsyncMethodWithoutCancellation.cs | 166 + .../Common/CallAnalysis/Arrays.cs | 129 + .../CallAnalysis/CallAnalyzer.Handlers.cs | 134 + .../CallAnalysis/CallAnalyzer.Registrar.cs | 241 + .../Common/CallAnalysis/CallAnalyzer.State.cs | 34 + .../Common/CallAnalysis/CallAnalyzer.cs | 61 + .../Common/CallAnalysis/EnumStrings.cs | 44 + .../Fixers/LegacyLoggingFixer.FixDetails.cs | 345 + .../CallAnalysis/Fixers/LegacyLoggingFixer.cs | 640 ++ .../Common/CallAnalysis/LegacyCollection.cs | 37 + .../Common/CallAnalysis/LegacyLogging.cs | 42 + .../Common/CallAnalysis/NullChecks.cs | 56 + .../Common/CallAnalysis/Split.cs | 25 + .../Common/CallAnalysis/StartsEndsWith.cs | 64 + .../Common/CallAnalysis/StaticTime.cs | 62 + .../Common/CallAnalysis/StringFormat.cs | 68 + .../Common/CallAnalysis/ValueTuple.cs | 52 + .../Common/CoalesceAnalyzer.cs | 86 + .../Common/ConditionalAccessAnalyzer.cs | 132 + .../DataClassificationStaticAnalysisCommon.cs | 99 + .../Common/DiagDescriptors.cs | 368 + .../FixAllProviders/ISequentialFixer.cs | 12 + .../SequentialFixAllCodeAction.cs | 99 + .../SequentialFixAllProvider.cs | 92 + .../Common/MakeExeTypesInternalAnalyzer.cs | 141 + .../Common/MakeExeTypesInternalFixer.cs | 43 + .../Common/OptimizeArraysAnalyzer.cs | 144 + .../Common/Resources.Designer.cs | 963 +++ .../Common/Resources.resx | 420 + .../Common/StringFormatFixer.cs | 178 + .../UsingExcessiveDictionaryLookupAnalyzer.cs | 321 + .../UsingExcessiveDictionaryLookupFixer.cs | 241 + .../Common/UsingExcessiveSetLookupAnalyzer.cs | 225 + .../Common/UsingExcessiveSetLookupFixer.cs | 95 + .../Common/UsingExperimentalApiAnalyzer.cs | 65 + .../Common/UsingToStringInLoggersAnalyzer.cs | 73 + .../Common/Utilities/CompilationExtensions.cs | 15 + .../Common/Utilities/OperationExtensions.cs | 30 + .../Common/Utilities/SymbolExtensions.cs | 122 + .../Utilities/SyntaxEditorExtensions.cs | 34 + .../Common/Utilities/SyntaxNodeExtensions.cs | 137 + .../Directory.Build.props | 33 + ...Extensions.ExtraAnalyzers.Roslyn3.8.csproj | 22 + ...Extensions.ExtraAnalyzers.Roslyn4.0.csproj | 16 + .../ApiLifecycle/ApiLifecycleAnalyzer.cs | 169 + .../ApiLifecycle/ApiLifecycleFixer.cs | 74 + .../ApiLifecycle/AssemblyAnalysis.cs | 303 + .../ApiLifecycle/Json/JsonArray.cs | 139 + .../ApiLifecycle/Json/JsonObject.cs | 212 + .../ApiLifecycle/Json/JsonObjectExtensions.cs | 29 + .../ApiLifecycle/Json/JsonParseException.cs | 73 + .../ApiLifecycle/Json/JsonReader.cs | 389 + .../ApiLifecycle/Json/JsonValue.cs | 622 ++ .../ApiLifecycle/Json/JsonValueType.cs | 42 + .../ApiLifecycle/Json/ParsingError.cs | 33 + .../ApiLifecycle/Json/TextPosition.cs | 30 + .../ApiLifecycle/Json/TextScanner.cs | 217 + .../ApiLifecycle/Model/Assembly.cs | 27 + .../ApiLifecycle/Model/Field.cs | 24 + .../ApiLifecycle/Model/Method.cs | 29 + .../ApiLifecycle/Model/Prop.cs | 24 + .../ApiLifecycle/Model/Stage.cs | 22 + .../ApiLifecycle/Model/TypeDef.cs | 31 + .../ApiLifecycle/ModelLoader.cs | 82 + .../ApiLifecycle/Utils.cs | 105 + .../CallAnalysis/CallAnalyzer.Handlers.cs | 134 + .../CallAnalysis/CallAnalyzer.Registrar.cs | 241 + .../CallAnalysis/CallAnalyzer.State.cs | 34 + .../CallAnalysis/CallAnalyzer.cs | 44 + .../CallAnalysis/Throws.cs | 70 + .../CallAnalysis/ToInvariantString.cs | 58 + .../DiagDescriptors.cs | 568 ++ ...Microsoft.Extensions.LocalAnalyzers.csproj | 38 + .../Resources.Designer.cs | 1467 ++++ .../Resources.resx | 588 ++ .../Utilities/CompilationExtensions.cs | 15 + .../Utilities/OperationExtensions.cs | 30 + .../Utilities/SymbolExtensions.cs | 209 + .../Utilities/SyntaxEditorExtensions.cs | 34 + .../Utilities/SyntaxNodeExtensions.cs | 137 + src/Directory.Build.props | 19 + src/Directory.Build.targets | 13 + src/Generators/Directory.Build.props | 8 + src/Generators/Directory.Build.targets | 8 + .../Common/DiagDescriptors.cs | 121 + .../Common/Emitter.cs | 434 + .../Common/Generator.cs | 107 + .../Common/Model/BodyContentTypeParam.cs | 10 + .../Model/BodyContentTypeParamExtensions.cs | 17 + .../Common/Model/RestApiMethod.cs | 18 + .../Common/Model/RestApiMethodParameter.cs | 18 + .../Common/Model/RestApiType.cs | 19 + .../Microsoft.Gen.AutoClient/Common/Parser.cs | 643 ++ .../Common/Resources.Designer.cs | 387 + .../Common/Resources.resx | 228 + .../Common/SymbolHolder.cs | 21 + .../Common/SymbolLoader.cs | 65 + .../Directory.Build.props | 33 + .../Microsoft.Gen.AutoClient.Roslyn3.8.csproj | 28 + .../Microsoft.Gen.AutoClient.Roslyn4.0.csproj | 29 + .../Common/Emitter.cs | 181 + .../Common/Generator.cs | 90 + .../Common/Model/Classification.cs | 13 + .../Common/Model/ClassifiedItem.cs | 19 + .../Common/Model/ClassifiedLogMethod.cs | 16 + .../Common/Model/ClassifiedType.cs | 16 + .../Common/Parser.cs | 247 + .../Common/SymbolHolder.cs | 13 + .../Common/SymbolLoader.cs | 30 + .../Directory.Build.props | 30 + ...oft.Gen.ComplianceReports.Roslyn3.8.csproj | 19 + ...oft.Gen.ComplianceReports.Roslyn4.0.csproj | 20 + .../Common/ContextReceiver.cs | 56 + .../Common/DiagDescriptors.cs | 37 + .../Common/Emitter.cs | 43 + .../Common/Generator.cs | 115 + .../Common/Model/OptionsContextType.cs | 34 + .../Common/Parser.cs | 93 + .../Common/Resources.Designer.cs | 135 + .../Common/Resources.resx | 144 + .../Common/SymbolHolder.cs | 8 + .../Common/SymbolLoader.cs | 23 + .../Directory.Build.props | 35 + ...oft.Gen.ContextualOptions.Roslyn3.8.csproj | 27 + ...oft.Gen.ContextualOptions.Roslyn4.0.csproj | 28 + .../Common/DiagDescriptors.cs | 42 + .../Common/Emitter.cs | 474 ++ .../Common/Generator.cs | 113 + .../Common/Model/ToStringMethod.cs | 17 + .../Common/Parser.cs | 246 + .../Common/Resources.Designer.cs | 153 + .../Common/Resources.resx | 150 + .../Common/SymbolHolder.cs | 14 + .../Common/SymbolLoader.cs | 43 + .../Directory.Build.props | 33 + ...Microsoft.Gen.EnumStrings.Roslyn3.8.csproj | 28 + ...Microsoft.Gen.EnumStrings.Roslyn4.0.csproj | 29 + .../Common/Emission/Emitter.Method.cs | 489 ++ .../Common/Emission/Emitter.Utils.cs | 153 + .../Common/Emission/Emitter.cs | 179 + .../Common/Emission/StringBuilderPool.cs | 31 + .../Microsoft.Gen.Logging/Common/Generator.cs | 86 + .../Common/Model/LoggingMethod.cs | 37 + .../Common/Model/LoggingMethodParameter.cs | 45 + .../Model/LoggingMethodParameterExtensions.cs | 53 + .../Common/Model/LoggingProperty.cs | 26 + .../Common/Model/LoggingPropertyProvider.cs | 11 + .../Common/Model/LoggingType.cs | 18 + .../Common/Parsing/AttributeProcessors.cs | 117 + .../Common/Parsing/DiagDescriptors.cs | 260 + .../Common/Parsing/LogParserUtilities.cs | 388 + .../Parsing/LogPropertiesProcessingResult.cs | 11 + .../Parsing/LogPropertiesProviderValidator.cs | 143 + .../MultipleDataClassesAppliedException.cs | 20 + .../Common/Parsing/Parser.cs | 657 ++ .../Common/Parsing/PropertyHiddenException.cs | 20 + .../Common/Parsing/Resources.Designer.cs | 729 ++ .../Common/Parsing/Resources.resx | 342 + .../Common/Parsing/SymbolHolder.cs | 24 + .../Common/Parsing/SymbolLoader.cs | 132 + .../Common/Parsing/TemplateExtractor.cs | 107 + .../Parsing/TransitiveTypeCycleException.cs | 23 + .../Directory.Build.props | 33 + .../Microsoft.Gen.Logging.Roslyn3.8.csproj | 28 + .../Microsoft.Gen.Logging.Roslyn4.0.csproj | 29 + .../Common/DiagDescriptors.cs | 116 + .../Microsoft.Gen.Metering/Common/Emitter.cs | 342 + .../Common/Generator.cs | 96 + .../Common/MetricFactoryEmitter.cs | 154 + .../Common/Model/InstrumentKind.cs | 14 + .../Common/Model/MetricMethod.cs | 23 + .../Common/Model/MetricParameter.cs | 11 + .../Common/Model/MetricType.cs | 17 + .../Common/Model/StrongTypeConfig.cs | 42 + .../Model/StrongTypeMetricObjectType.cs | 12 + .../Microsoft.Gen.Metering/Common/Parser.cs | 828 ++ .../Common/Resources.Designer.cs | 369 + .../Common/Resources.resx | 222 + .../Common/SymbolHolder.cs | 16 + .../Common/SymbolLoader.cs | 47 + .../Directory.Build.props | 33 + .../Microsoft.Gen.Metering.Roslyn3.8.csproj | 27 + .../Microsoft.Gen.Metering.Roslyn4.0.csproj | 28 + .../Common/MetricDefinitionEmitter.cs | 185 + .../Common/MetricDefinitionGenerator.cs | 114 + .../Common/ReportedMetricClass.cs | 11 + .../Directory.Build.props | 42 + ...osoft.Gen.MeteringReports.Roslyn3.8.csproj | 27 + ...osoft.Gen.MeteringReports.Roslyn4.0.csproj | 28 + .../Common/DiagDescriptors.cs | 98 + .../Common/Emitter.cs | 368 + .../Common/Generator.cs | 100 + .../Common/Model/ValidatedMember.cs | 19 + .../Common/Model/ValidatedModel.cs | 12 + .../Common/Model/ValidationAttributeInfo.cs | 12 + .../Common/Model/ValidatorType.cs | 15 + .../Common/Parser.cs | 696 ++ .../Common/Resources.Designer.cs | 297 + .../Common/Resources.resx | 198 + .../Common/SymbolHolder.cs | 20 + .../Common/SymbolLoader.cs | 69 + .../Directory.Build.props | 33 + ...oft.Gen.OptionsValidation.Roslyn3.8.csproj | 28 + ...oft.Gen.OptionsValidation.Roslyn4.0.csproj | 29 + .../Shared/ClassDeclarationSyntaxReceiver.cs | 35 + src/Generators/Shared/DiagDescriptorsBase.cs | 41 + src/Generators/Shared/EmitterBase.cs | 105 + src/Generators/Shared/GeneratorUtilities.cs | 144 + src/Generators/Shared/ParserUtilities.cs | 79 + src/Generators/Shared/StringBuilderPool.cs | 36 + src/Generators/Shared/SymbolHelpers.cs | 19 + .../Shared/TypeDeclarationSyntaxReceiver.cs | 47 + .../BitOperations/BitOperations.cs | 47 + src/LegacySupport/BitOperations/README.md | 7 + .../CallerArgumentExpressionAttribute.cs | 32 + .../CallerFilePathAttribute.cs | 18 + .../CallerLineNumberAttribute.cs | 18 + .../CallerMemberNameAttribute.cs | 18 + src/LegacySupport/CallerAttributes/README.md | 7 + .../NullableAttributes.cs | 177 + .../DiagnosticAttributes/README.md | 7 + .../DictionaryExtensions.cs | 35 + .../DictionaryExtensions/README.md | 7 + .../ExperimentalAttribute.cs | 53 + .../GetOrAdd/GetOrAddExtensions.cs | 35 + src/LegacySupport/GetOrAdd/README.md | 7 + .../IsExternalInit/IsExternalInit.cs | 13 + src/LegacySupport/IsExternalInit/README.md | 9 + src/LegacySupport/README.md | 8 + .../SkipLocalsInitAttribute/README.md | 7 + .../SkipLocalsInitAttribute.cs | 38 + .../StringBuilderExtensions/README.md | 7 + .../StringBuilderExtensions.cs | 27 + src/LegacySupport/StringHash/README.md | 7 + src/LegacySupport/StringHash/StringHash.cs | 30 + .../StringSyntaxAttribute/README.md | 7 + .../StringSyntaxAttribute.cs | 74 + src/LegacySupport/TaskWaitAsync/README.md | 7 + .../TaskWaitAsync/TaskExtensions.cs | 49 + .../DynamicDependencyAttribute.cs | 139 + .../DynamicallyAccessedMemberTypes.cs | 100 + .../DynamicallyAccessedMembersAttribute.cs | 60 + src/LegacySupport/TrimAttributes/README.md | 7 + .../RequiresAssemblyFilesAttribute.cs | 58 + .../RequiresUnreferencedCodeAttribute.cs | 50 + .../UnconditionalSuppressMessageAttribute.cs | 94 + src/LegacySupport/xxH3/README.md | 7 + src/LegacySupport/xxH3/XxHash3.cs | 1069 +++ src/LegacySupport/xxH3/XxHash32.State.cs | 18 + src/LegacySupport/xxH3/XxHash64.State.cs | 31 + src/Libraries/Directory.Build.props | 16 + .../AsyncContextHttpContext.cs | 59 + .../AsyncStateHttpContextExtensions.cs | 36 + .../Microsoft.AspNetCore.AsyncState.csproj | 30 + .../TypeWrapper.cs | 21 + .../ConnectionTimeoutDelegate.cs | 45 + .../ConnectionTimeoutExtensions.cs | 78 + .../ConnectionTimeoutOptions.cs | 24 + .../ConnectionTimeoutValidator.cs | 35 + ...rosoft.AspNetCore.ConnectionTimeout.csproj | 37 + .../CommonHeaders.cs | 101 + .../HeaderKey.cs | 72 + .../HeaderParser.cs | 26 + .../HeaderParsingExtensions.cs | 137 + .../HeaderParsingFeature.cs | 208 + .../HeaderParsingOptions.cs | 43 + .../HeaderParsingOptionsManualValidator.cs | 26 + .../HeaderParsingOptionsValidator.cs | 12 + .../HeaderRegistry.cs | 66 + .../HeaderSetup.cs | 63 + .../HostHeaderValue.cs | 121 + .../IHeaderRegistry.cs | 20 + .../Metric.cs | 16 + .../Microsoft.AspNetCore.HeaderParsing.csproj | 50 + .../Parsers/CacheControlHeaderValueParser.cs | 27 + .../ContentDispositionHeaderValueParser.cs | 27 + .../Parsers/CookieHeaderValueListParser.cs | 28 + .../Parsers/DateTimeOffsetParser.cs | 28 + .../Parsers/EntityTagHeaderValueListParser.cs | 28 + .../Parsers/HostHeaderValueParser.cs | 26 + .../Parsers/IPAddressListParser.cs | 63 + .../Parsers/MediaTypeHeaderValueListParser.cs | 28 + .../Parsers/MediaTypeHeaderValueParser.cs | 27 + .../RangeConditionHeaderValueParser.cs | 27 + .../Parsers/RangeHeaderValueParser.cs | 27 + .../StringWithQualityHeaderValueListParser.cs | 28 + .../Parsers/UriParser.cs | 27 + .../ParsingResult.cs | 25 + .../AddServerTimingHeaderMiddleware.cs | 52 + .../CapturePipelineEntryMiddleware.cs | 31 + .../CapturePipelineEntryStartupFilter.cs | 29 + .../CapturePipelineExitMiddleware.cs | 36 + .../CaptureResponseTimeMiddleware.cs | 55 + .../LatencyContextControlExtensions.cs | 28 + .../Checkpoint/RequestCheckpointConstants.cs | 47 + .../Checkpoint/RequestCheckpointExtensions.cs | 67 + .../RequestLatencyTelemetryMiddleware.cs | 79 + ...RequestLatencyTelemetryOptionsValidator.cs | 16 + .../RequestLatencyTelemetryExtensions.cs | 95 + .../Latency/RequestLatencyTelemetryOptions.cs | 25 + .../Logging/HttpLoggingDimensions.cs | 76 + .../Logging/HttpLoggingServiceExtensions.cs | 130 + .../Logging/IHttpLogEnricher.cs | 21 + .../Logging/IncomingPathLoggingMode.cs | 20 + .../Logging/Internal/HeaderReader.cs | 49 + .../Internal/HttpLogPropertiesProvider.cs | 71 + .../Logging/Internal/HttpLoggingMiddleware.cs | 461 + .../Logging/Internal/HttpRequestBodyReader.cs | 58 + .../Internal/IncomingRequestLogRecord.cs | 71 + .../Logging/Internal/Log.cs | 127 + .../Internal/LoggingOptionsValidator.cs | 12 + .../Internal/MediaTypeSetExtensions.cs | 28 + .../Logging/Internal/PipeReaderExtensions.cs | 74 + .../Internal/ResponseInterceptingStream.cs | 224 + .../ResponseInterceptingStreamPool.cs | 47 + .../Logging/LoggingOptions.cs | 203 + .../Metering/HttpMeteringBuilder.cs | 26 + .../Metering/HttpMeteringExtensions.cs | 88 + .../IIncomingRequestMetricEnricher.cs | 18 + .../Internal/HttpContextExtensions.cs | 24 + .../Internal/HttpMeteringMiddleware.cs | 153 + .../Metering/Internal/Metric.cs | 42 + ...oft.AspNetCore.Telemetry.Middleware.csproj | 46 + .../RequestHeadersEnricherExtensions.cs | 92 + .../RequestHeadersLogEnricher.cs | 73 + .../RequestHeadersLogEnricherOptions.cs | 27 + ...questHeadersLogEnricherOptionsValidator.cs | 12 + .../Microsoft.AspNetCore.Telemetry.csproj | 55 + .../HttpUtilityExtensions.cs | 70 + .../IIncomingHttpRouteUtility.cs | 23 + .../IncomingHttpRouteUtility.cs | 71 + .../Tracing/Constants.cs | 22 + .../Tracing/HttpTraceEnrichmentProcessor.cs | 31 + .../HttpTraceEnrichmentProcessor.netfx.cs | 48 + .../Tracing/HttpTracingExtensions.cs | 183 + .../Tracing/HttpTracingOptions.cs | 62 + .../Tracing/HttpUrlRedactionProcessor.cs | 136 + .../Tracing/IHttpTraceEnricher.cs | 21 + ...nfigureAspNetCoreInstrumentationOptions.cs | 49 + .../Internal/HttpTracingOptionsValidator.cs | 12 + .../Tracing/Internal/Log.cs | 24 + .../FakeCertificateHttpClientHandler.cs | 24 + .../Internal/FakeCertificateOptions.cs | 11 + .../Internal/FakeSslCertificateFactory.cs | 57 + .../Internal/FakeStartup.cs | 22 + .../Microsoft.AspNetCore.Testing.csproj | 27 + .../ServiceFakesExtensions.cs | 131 + .../ApplicationMetadata.cs | 34 + .../ApplicationMetadataExtensions.cs | 104 + .../ApplicationMetadataSource.cs | 52 + .../ApplicationMetadataValidator.cs | 12 + ...ensions.AmbientMetadata.Application.csproj | 32 + .../AsyncContext.cs | 31 + .../AsyncState.cs | 109 + .../AsyncStateExtensions.cs | 64 + .../AsyncStateToken.cs | 70 + .../FeaturesPooledPolicy.cs | 27 + .../IAsyncContext.cs | 46 + .../IAsyncLocalContext.cs | 21 + .../IAsyncState.cs | 58 + .../Microsoft.Extensions.AsyncState.csproj | 30 + .../Classification/DataClassification.cs | 151 + .../DataClassificationAttribute.cs | 44 + .../NoDataClassificationAttribute.cs | 18 + .../UnknownDataClassificationAttribute.cs | 18 + ....Extensions.Compliance.Abstractions.csproj | 31 + .../Redaction/IRedactionBuilder.cs | 37 + .../Redaction/IRedactorProvider.cs | 19 + .../RedactionAbstractionsExtensions.cs | 68 + .../Redaction/Redactor.cs | 201 + .../ErasingRedactor.cs | 23 + ...oft.Extensions.Compliance.Redaction.csproj | 34 + .../NullRedactor.cs | 36 + .../NullRedactorProvider.cs | 20 + .../RedactionBuilder.cs | 54 + .../RedactionExtensions.cs | 99 + .../RedactionExtensions.xxHash.cs | 62 + .../RedactorProvider.cs | 68 + .../RedactorProviderOptions.cs | 24 + .../XXHash3Redactor.cs | 84 + .../XXHash3RedactorOptions.cs | 22 + .../Attributes/PrivateDataAttribute.cs | 20 + .../Attributes/PublicDataAttribute.cs | 20 + .../FakeRedactionCollector.cs | 101 + .../FakeRedactionExtensions.cs | 152 + .../FakeRedactor.cs | 93 + .../FakeRedactorOptions.cs | 26 + .../FakeRedactorOptionsAutoValidator.cs | 12 + .../FakeRedactorOptionsCustomValidator.cs | 33 + .../FakeRedactorProvider.cs | 43 + ...osoft.Extensions.Compliance.Testing.csproj | 38 + .../RedactedData.cs | 83 + .../RedactorRequested.cs | 76 + .../SimpleClassifications.cs | 27 + .../SimpleTaxonomy.cs | 34 + .../SimpleTaxonomyExtensions.cs | 29 + .../Abstractions/ExceptionSummary.cs | 116 + .../IExceptionSummarizationBuilder.cs | 26 + .../Abstractions/IExceptionSummarizer.cs | 19 + .../Abstractions/IExceptionSummaryProvider.cs | 40 + .../ExceptionSummarizationBuilder.cs | 25 + .../ExceptionSummarizationExtensions.cs | 57 + .../Implementation/ExceptionSummarizer.cs | 105 + .../HttpExceptionSummaryProvider.cs | 111 + ....Diagnostics.ExceptionSummarization.csproj | 33 + .../ApplicationLifecycleHealthCheck.cs | 50 + ...thChecksExtensions.ApplicationLifecycle.cs | 46 + ...lthChecksExtensions.KubernetesPublisher.cs | 69 + .../CoreHealthChecksExtensions.Manual.cs | 58 + ...althChecksExtensions.TelemetryPublisher.cs | 20 + .../IManualHealthCheck.cs | 25 + .../IManualHealthCheckTracker.cs | 32 + .../KubernetesHealthCheckPublisher.cs | 69 + .../KubernetesHealthCheckPublisherOptions.cs | 35 + .../Log.cs | 22 + .../ManualHealthCheck.cs | 57 + .../ManualHealthCheckService.cs | 38 + .../ManualHealthCheckTracker.cs | 66 + .../Metric.cs | 26 + ...sions.Diagnostics.HealthChecks.Core.csproj | 36 + .../TelemetryHealthCheckPublisher.cs | 81 + ...cs.HealthChecks.ResourceUtilization.csproj | 32 + .../ResourceUsageThresholds.cs | 32 + .../ResourceUtilizationHealthCheck.cs | 64 + .../ResourceUtilizationHealthCheckOptions.cs | 47 + ...eUtilizationHealthCheckOptionsValidator.cs | 12 + ...sourceUtilizationHealthChecksExtensions.cs | 138 + .../IResourceUtilizationPublisher.cs | 21 + .../IResourceUtilizationTracker.cs | 22 + .../IResourceUtilizationTrackerBuilder.cs | 25 + .../Internal/Calculator.cs | 40 + .../Internal/CircularBuffer.cs | 52 + .../Internal/ISnapshotProvider.cs | 23 + .../Internal/Log.cs | 20 + .../Internal/ResourceUtilizationBuilder.cs | 30 + .../Internal/ResourceUtilizationSnapshot.cs | 82 + ...tilizationTrackerOptionsManualValidator.cs | 24 + ...ourceUtilizationTrackerOptionsValidator.cs | 12 + .../ResourceUtilizationTrackerService.cs | 166 + .../Linux/Internal/IFileSystem.cs | 32 + .../Linux/Internal/IOperatingSystem.cs | 15 + .../Linux/Internal/IUserHz.cs | 15 + .../Linux/Internal/IsOperatingSystem.cs | 11 + ...urceUtilizationProviderOptionsValidator.cs | 12 + .../Linux/Internal/LinuxUtilizationParser.cs | 398 + .../Internal/LinuxUtilizationProvider.cs | 158 + .../Linux/Internal/OSFileSystem.cs | 67 + .../Linux/Internal/UserHz.cs | 35 + .../Linux/LinuxResourceUtilizationCounters.cs | 29 + ...LinuxResourceUtilizationProviderOptions.cs | 37 + .../Linux/LinuxUtilizationExtensions.cs | 79 + ...ions.Diagnostics.ResourceMonitoring.csproj | 49 + .../NullResourceUtilizationTrackerService.cs | 14 + .../Null/NullSnapshotProvider.cs | 31 + .../ResourceMonitoringExtensions.cs | 144 + .../ResourceUtilizationTrackerOptions.cs | 53 + .../SystemResources.cs | 65 + .../Utilization.cs | 80 + .../Windows/CountersSetup.cs | 79 + .../Windows/Internal/Interop/IJobHandle.cs | 31 + .../Windows/Internal/Interop/IMemoryInfo.cs | 16 + .../Windows/Internal/Interop/IProcessInfo.cs | 18 + .../Windows/Internal/Interop/ISystemInfo.cs | 16 + .../Internal/Interop/JobHandleWrapper.cs | 41 + .../Windows/Internal/Interop/JobObjectInfo.cs | 400 + .../Windows/Internal/Interop/MemoryInfo.cs | 47 + .../Internal/Interop/MemoryStatusEx.cs | 23 + .../Windows/Internal/Interop/ProcessInfo.cs | 88 + .../Internal/Interop/ProcessInfoWrapper.cs | 15 + .../Windows/Internal/Interop/SYSTEM_INFO.cs | 26 + .../Windows/Internal/Interop/SystemInfo.cs | 34 + .../Windows/Internal/Log.cs | 22 + .../Windows/Internal/Network/MIB_TCPROW.cs | 26 + .../Windows/Internal/Network/MIB_TCPTABLE.cs | 13 + .../Windows/Internal/Network/MIB_TCP_STATE.cs | 44 + .../Windows/Internal/Network/NTSTATUS.cs | 22 + .../Windows/Internal/Network/TcpStateInfo.cs | 46 + .../Windows/Internal/Network/TcpTableInfo.cs | 165 + .../WindowsContainerSnapshotProvider.cs | 159 + .../Windows/Internal/WindowsCounters.cs | 123 + .../WindowsCountersOptionsCustomValidator.cs | 27 + .../WindowsCountersOptionsValidator.cs | 12 + .../Internal/WindowsPerfCounterConstants.cs | 35 + .../Internal/WindowsPerfCounterPublisher.cs | 64 + .../Windows/Internal/WindowsPerfCounters.cs | 27 + .../Internal/WindowsSnapshotProvider.cs | 37 + .../Windows/WindowsCountersOptions.cs | 39 + .../Windows/WindowsUtilizationExtensions.cs | 153 + .../EnumStringsAttribute.cs | 85 + .../Microsoft.Extensions.EnumStrings.csproj | 27 + .../Microsoft.Extensions.EnumStrings.props | 2 + .../Microsoft.Extensions.EnumStrings.targets | 3 + .../FakeHost.cs | 126 + .../FakeHostOptions.cs | 53 + .../HostingFakesExtensions.cs | 187 + .../Internal/FakeConfigurationSource.cs | 15 + .../Internal/FakeHostBuilder.cs | 96 + .../Internal/HostTerminatorService.cs | 69 + ...icrosoft.Extensions.Hosting.Testing.csproj | 35 + .../AutoClientAttribute.cs | 64 + .../AutoClientException.cs | 60 + .../AutoClientHttpError.cs | 86 + .../AutoClientOptions.cs | 32 + .../BodyAttribute.cs | 56 + .../BodyContentType.cs | 29 + .../HeaderAttribute.cs | 44 + .../Methods/DeleteAttribute.cs | 49 + .../Methods/GetAttribute.cs | 49 + .../Methods/HeadAttribute.cs | 49 + .../Methods/OptionsAttribute.cs | 49 + .../Methods/PatchAttribute.cs | 49 + .../Methods/PostAttribute.cs | 49 + .../Methods/PutAttribute.cs | 49 + ...icrosoft.Extensions.Http.AutoClient.csproj | 38 + .../QueryAttribute.cs | 54 + .../RequestNameAttribute.cs | 47 + .../StaticHeaderAttribute.cs | 55 + ...Microsoft.Extensions.Http.AutoClient.props | 2 + ...crosoft.Extensions.Http.AutoClient.targets | 3 + .../HttpClientFaultInjectionExtensions.cs | 222 + .../HttpFaultInjectionOptionsBuilder.cs | 112 + .../IHttpClientChaosPolicyFactory.cs | 24 + .../FaultInjectionContextMessageHandler.cs | 37 + .../FaultInjectionEventMeterDimensions.cs | 30 + .../FaultInjectionTelemetryHandler.cs | 20 + ...onWeightAssignmentContextMessageHandler.cs | 38 + .../Internal/HttpClientChaosPolicyFactory.cs | 138 + .../Internal/HttpContentOptions.cs | 11 + .../Internal/HttpContentOptionsRegistry.cs | 29 + .../Internal/IHttpContentOptionsRegistry.cs | 24 + .../FaultInjection/Internal/Log.cs | 22 + .../FaultInjection/Internal/Metric.cs | 17 + .../FaultInjection/PolicyContextExtensions.cs | 51 + .../Hedging/HedgingEndpointOptions.cs | 52 + ...ingHttpClientBuilderExtensions.Standard.cs | 108 + .../HedgingHttpClientBuilderExtensions.cs | 46 + .../HttpClientHedgingResiliencePredicates.cs | 29 + .../Hedging/HttpHedgingPolicyOptions.cs | 26 + .../HttpStandardHedgingResilienceOptions.cs | 61 + .../Hedging/IStandardHedgingHandlerBuilder.cs | 35 + .../Hedging/Internals/HedgingConstants.cs | 11 + .../Internals/HedgingContextExtensions.cs | 62 + ...HttpResiliencePipelineBuilderExtensions.cs | 32 + .../Hedging/Internals/IRandomizer.cs | 24 + .../Hedging/Internals/Randomizer.cs | 19 + .../Internals/RequestMessageSnapshotPolicy.cs | 40 + .../Routing/DefaultRoutingStrategyFactory.cs | 24 + .../IPooledRequestRoutingStrategyFactory.cs | 14 + .../OrderedGroupsRoutingOptionsValidator.cs | 12 + .../OrderedGroupsRoutingStrategy.cs | 67 + .../OrderedGroupsRoutingStrategyFactory.cs | 17 + .../Routing/PooledRoutingStrategyFactory.cs | 47 + .../Internals/Routing/RoutingHelper.cs | 37 + .../Internals/Routing/RoutingPolicy.cs | 62 + .../Routing/RoutingStrategyBuilder.cs | 19 + .../WeightedGroupsRoutingOptionsValidator.cs | 12 + .../WeightedGroupsRoutingStrategy.cs | 91 + .../WeightedGroupsRoutingStrategyFactory.cs | 17 + .../StandardHedgingHandlerBuilder.cs | 25 + .../Internals/StandardHedgingPolicyNames.cs | 17 + ...HedgingResilienceOptionsCustomValidator.cs | 61 + ...andardHedgingResilienceOptionsValidator.cs | 12 + .../Hedging/Routing/Endpoint.cs | 30 + .../Hedging/Routing/EndpointGroup.cs | 29 + .../Routing/IRequestRoutingStrategy.cs | 24 + .../Routing/IRequestRoutingStrategyFactory.cs | 20 + .../Routing/IRoutingStrategyBuilder.cs | 22 + .../Routing/OrderedGroupsRoutingOptions.cs | 29 + .../RoutingStrategyBuilderExtensions.cs | 164 + .../Hedging/Routing/WeightedEndpoint.cs | 35 + .../Hedging/Routing/WeightedEndpointGroup.cs | 26 + .../Routing/WeightedGroupSelectionMode.cs | 21 + .../Routing/WeightedGroupsRoutingOptions.cs | 35 + ...StandardHedgingHandlerBuilderExtensions.cs | 111 + ...icrosoft.Extensions.Http.Resilience.csproj | 47 + .../Polly/HttpBulkheadPolicyOptions.cs | 13 + .../Polly/HttpCircuitBreakerPolicyOptions.cs | 26 + .../Polly/HttpClientResilienceGenerators.cs | 20 + .../Polly/HttpClientResiliencePredicates.cs | 49 + .../Polly/HttpFallbackPolicyOptions.cs | 26 + .../Polly/HttpRetryPolicyOptions.cs | 46 + .../Polly/HttpTimeoutPolicyOptions.cs | 13 + ...olicyFactoryServiceCollectionExtensions.cs | 63 + .../Polly/Internal/RetryAfterHelper.cs | 49 + .../FallbackClientHandlerOptions.cs | 28 + .../HttpClientBuilderExtensions.Fallback.cs | 85 + .../HttpClientBuilderExtensions.Resilience.cs | 85 + ...entBuilderExtensions.StandardResilience.cs | 94 + ...HttpResiliencePipelineBuilderExtensions.cs | 49 + .../HttpStandardResilienceOptions.cs | 81 + ...dardResiliencePipelineBuilderExtensions.cs | 111 + .../IHttpResiliencePipelineBuilder.cs | 16 + .../IHttpStandardResiliencePipelineBuilder.cs | 22 + .../ByAuthorityPipelineKeyProvider.cs | 49 + .../ByCustomSelectorPipelineKeyProvider.cs | 21 + .../Resilience/Internal/ContextExtensions.cs | 93 + .../Internal/DefaultRequestCloner.cs | 99 + .../Resilience/Internal/FallbackHelper.cs | 53 + .../Internal/HttpRequestMessageExtensions.cs | 27 + .../Internal/HttpResiliencePipelineBuilder.cs | 21 + .../HttpStandardResiliencePipelineBuilder.cs | 19 + .../Internal/IHttpRequestMessageSnapshot.cs | 19 + .../Internal/IPipelineKeyProvider.cs | 19 + .../Internal/IRequestClonerInternal.cs | 19 + .../Internal/PipelineKeyProviderHelper.cs | 42 + .../Resilience/Internal/PipelineNameHelper.cs | 12 + .../Resilience/Internal/ResilienceHandler.cs | 58 + .../Internal/ServiceCollectionExtensions.cs | 24 + .../Internal/StandardPolicyNames.cs | 17 + .../Resilience/Internal/UriExtensions.cs | 43 + .../FallbackClientHandlerOptionsValidator.cs | 43 + ...ttpCircuitBreakerPolicyOptionsValidator.cs | 12 + .../HttpFallbackPolicyOptionsValidator.cs | 12 + .../HttpRetryPolicyOptionsValidator.cs | 12 + ...tandardResilienceOptionsCustomValidator.cs | 48 + .../HttpStandardResilienceOptionsValidator.cs | 12 + .../Internal/Validators/ValidationHelper.cs | 61 + .../Resilience/PipelineKeySelector.cs | 17 + .../HttpClientLatencyTelemetryExtensions.cs | 99 + .../HttpClientLatencyTelemetryOptions.cs | 21 + .../Latency/Internal/HttpCheckpoints.cs | 56 + .../Internal/HttpClientLatencyContext.cs | 27 + .../Internal/HttpClientLatencyLogEnricher.cs | 82 + .../Internal/HttpLatencyTelemetryHandler.cs | 56 + .../Internal/HttpRequestLatencyListener.cs | 163 + .../Logging/HttpClientLoggingDimensions.cs | 76 + .../Logging/HttpClientLoggingExtensions.cs | 264 + .../Logging/IHttpClientLogEnricher.cs | 21 + .../Logging/Internal/Constants.cs | 11 + .../Logging/Internal/HttpHeadersReader.cs | 65 + .../Logging/Internal/HttpLoggingHandler.cs | 173 + .../Logging/Internal/HttpRequestBodyReader.cs | 128 + .../Logging/Internal/HttpRequestReader.cs | 144 + .../Internal/HttpResponseBodyReader.cs | 147 + .../Logging/Internal/IHttpHeadersReader.cs | 27 + .../Logging/Internal/IHttpRequestReader.cs | 39 + .../Logging/Internal/Log.cs | 77 + .../LogPropertyCollectorExtensions.cs | 44 + .../Logging/Internal/LogRecord.cs | 64 + .../Internal/LogRecordPooledObjectPolicy.cs | 26 + .../Internal/LoggingOptionsValidator.cs | 12 + .../Internal/MediaTypeCollectionExtensions.cs | 44 + .../Logging/LoggingOptions.cs | 131 + .../Logging/OutgoingPathLoggingMode.cs | 20 + .../Metering/HttpClientMeteringExtensions.cs | 111 + .../Metering/HttpMeteringHandler.cs | 186 + .../IOutgoingRequestMetricEnricher.cs | 18 + .../Internal/HttpClientMeteringConstants.cs | 22 + .../Metering/Internal/Metric.cs | 44 + ...Microsoft.Extensions.Http.Telemetry.csproj | 53 + .../Tracing/Constants.cs | 22 + .../Tracing/HttpClientRedactionProcessor.cs | 136 + .../HttpClientTraceEnrichmentProcessor.cs | 27 + .../Tracing/HttpClientTracingConstants.cs | 19 + .../Tracing/HttpClientTracingExtensions.cs | 160 + .../Tracing/HttpClientTracingOptions.cs | 42 + .../Tracing/IHttpClientTraceEnricher.cs | 25 + .../Tracing/IHttpPathRedactor.cs | 25 + ...nfigureHttpClientInstrumentationOptions.cs | 39 + .../HttpClientTracingOptionsValidator.cs | 12 + .../Tracing/Internal/HttpPathRedactor.cs | 44 + .../Internal/HttpTracingEventSource.cs | 24 + .../Tracing/Internal/Log.cs | 30 + .../ConfigureContextualOptions.cs | 44 + .../ContextualOptions.cs | 37 + .../ContextualOptionsFactory.cs | 143 + ...xtualOptionsServiceCollectionExtensions.cs | 161 + .../IConfigureContextualOptions.cs | 20 + .../IContextualOptions.cs | 25 + .../IContextualOptionsFactory.cs | 26 + .../ILoadContextualOptions.cs | 26 + .../INamedContextualOptions.cs | 26 + .../IOptionsContext.cs | 18 + .../IOptionsContextReceiver.cs | 18 + .../IPostConfigureContextualOptions.cs | 22 + .../IValidateContextualOptions.cs | 22 + .../LoadContextualOptions.cs | 53 + ...osoft.Extensions.Options.Contextual.csproj | 37 + .../NullConfigureContextualOptions.cs | 20 + .../NullConfigureContextualOptions_1.cs | 30 + .../OptionsContextAttribute.cs | 16 + .../PostConfigureContextualOptions.cs | 50 + .../ValidateContextualOptions.cs | 26 + ...rosoft.Extensions.Options.Contextual.props | 2 + ...soft.Extensions.Options.Contextual.targets | 3 + ...osoft.Extensions.Options.Validation.csproj | 38 + .../OptionsValidatorAttribute.cs | 16 + .../ValidateEnumeratedItemsAttribute.cs | 45 + .../ValidateObjectMembersAttribute.cs | 43 + ...rosoft.Extensions.Options.Validation.props | 2 + ...soft.Extensions.Options.Validation.targets | 3 + .../FaultInjectionExtensions.cs | 188 + .../FaultInjectionOptionsBuilder.cs | 116 + .../FaultInjection/IChaosPolicyFactory.cs | 35 + .../IFaultInjectionOptionsProvider.cs | 28 + .../FaultInjection/InjectedFaultException.cs | 41 + .../Internals/ChaosPolicyFactory.cs | 196 + .../Internals/ExceptionRegistry.cs | 38 + .../FaultInjectionEventMeterDimensions.cs | 25 + .../FaultInjectionOptionsProvider.cs | 44 + .../FaultInjectionOptionsValidator.cs | 61 + .../FaultInjectionTelemetryHandler.cs | 16 + .../Internals/IExceptionRegistry.cs | 24 + .../FaultInjection/Internals/Log.cs | 20 + .../FaultInjection/Internals/Metric.cs | 17 + .../Internals/WeightAssignmentHelper.cs | 29 + .../Options/ChaosPolicyOptionsBase.cs | 43 + .../Options/ChaosPolicyOptionsGroup.cs | 39 + .../Options/ExceptionPolicyOptions.cs | 25 + .../Options/FaultInjectionExceptionOptions.cs | 11 + .../Options/FaultInjectionOptions.cs | 19 + .../FaultPolicyWeightAssignmentsOptions.cs | 24 + .../HttpResponseInjectionPolicyOptions.cs | 32 + .../Options/LatencyPolicyOptions.cs | 25 + .../Microsoft.Extensions.Resilience.csproj | 51 + .../Polly/FailureResultContext.cs | 50 + .../Polly/FallbackScenarioTaskArguments.cs | 37 + .../Polly/FallbackScenarioTaskProvider.cs | 17 + .../Polly/FallbackScenarioTaskProviderT.cs | 20 + .../Polly/GlobalSuppressions.cs | 6 + .../Polly/HedgedTaskProvider.cs | 14 + .../Polly/HedgedTaskProviderT.cs | 17 + .../Polly/Hedging/AsyncHedgingPolicy.cs | 64 + .../Polly/Hedging/AsyncHedgingPolicyT.cs | 59 + .../Polly/Hedging/AsyncHedgingSyntax.cs | 85 + .../Polly/Hedging/EmptyStruct.cs | 15 + .../Polly/Hedging/HedgingEngine.WhenAny.cs | 57 + .../Polly/Hedging/HedgingEngine.cs | 309 + .../Polly/Hedging/HedgingEngineOptions.cs | 37 + .../Polly/HedgingTaskProviderArguments.cs | 49 + .../Polly/Internals/ContextExtensions.cs | 15 + .../Internals/FailureEventMetricsOptions.cs | 11 + .../Polly/Internals/FailureReasonResolver.cs | 63 + .../Polly/Internals/IPolicyFactory.cs | 146 + .../Polly/Internals/IPolicyMetering.cs | 38 + .../Polly/Internals/Log.cs | 80 + .../Polly/Internals/Metric.cs | 24 + .../Polly/Internals/PipelineId.cs | 43 + .../Polly/Internals/PolicyEvents.cs | 15 + .../Polly/Internals/PolicyFactory.cs | 446 + ...olicyFactoryServiceCollectionExtensions.cs | 38 + .../Polly/Internals/PolicyFactoryUtility.cs | 57 + .../Polly/Internals/PolicyMetering.cs | 157 + .../Internals/RetryPolicyOptionsExtensions.cs | 53 + .../Polly/Internals/TelemetryHelper.cs | 21 + .../BulkheadPolicyOptionsValidator.cs | 13 + .../CircuitBreakerPolicyOptionsValidator.cs | 13 + .../CircuitBreakerPolicyOptionsValidatorT.cs | 13 + .../HedgingPolicyOptionsValidator.cs | 13 + .../HedgingPolicyOptionsValidatorT.cs | 13 + .../Generated/RetryPolicyOptionsValidator.cs | 13 + .../Generated/RetryPolicyOptionsValidatorT.cs | 13 + .../TimeoutPolicyOptionsValidator.cs | 13 + .../RetryPolicyOptionsCustomValidator.cs | 51 + .../Polly/Options/BackoffType.cs | 49 + .../Polly/Options/BreakActionArguments.cs | 57 + .../Polly/Options/BreakActionArgumentsT.cs | 60 + .../Polly/Options/BulkheadPolicyOptions.cs | 49 + .../Polly/Options/BulkheadTaskArguments.cs | 39 + .../Options/CircuitBreakerPolicyOptions.cs | 101 + .../Options/CircuitBreakerPolicyOptionsT.cs | 42 + .../Polly/Options/FallbackPolicyOptions.cs | 43 + .../Polly/Options/FallbackPolicyOptionsT.cs | 45 + .../Polly/Options/FallbackTaskArguments.cs | 48 + .../Polly/Options/FallbackTaskArgumentsT.cs | 50 + .../Polly/Options/HedgingDelayArguments.cs | 44 + .../Polly/Options/HedgingPolicyOptions.cs | 89 + .../Polly/Options/HedgingPolicyOptionsT.cs | 44 + .../Polly/Options/HedgingTaskArguments.cs | 56 + .../Polly/Options/HedgingTaskArgumentsT.cs | 58 + .../Polly/Options/IPolicyEventArguments.cs | 24 + .../Polly/Options/IPolicyEventArgumentsT.cs | 21 + .../Polly/Options/ResetActionArguments.cs | 39 + .../Polly/Options/RetryActionArguments.cs | 65 + .../Polly/Options/RetryActionArgumentsT.cs | 68 + .../Polly/Options/RetryDelayArguments.cs | 48 + .../Polly/Options/RetryPolicyOptions.cs | 90 + .../Polly/Options/RetryPolicyOptionsT.cs | 55 + .../Polly/Options/TimeoutPolicyOptions.cs | 50 + .../Polly/Options/TimeoutTaskArguments.cs | 39 + .../Polly/PollyServiceCollectionExtensions.cs | 32 + .../Polly/ResilienceDimensions.cs | 112 + .../Resilience/GlobalSuppressions.cs | 6 + .../Resilience/IResiliencePipelineBuilder.cs | 24 + .../Resilience/IResiliencePipelineProvider.cs | 47 + .../Internal/AsyncDynamicPipelineT.cs | 73 + .../Internal/AsyncPolicyPipeline.Args.cs | 70 + .../Internal/AsyncPolicyPipeline.cs | 211 + .../Internal/AsyncPolicyPipelineT.Args.cs | 63 + .../Internal/AsyncPolicyPipelineT.cs | 201 + .../Internal/IOnChangeListenersHandler.cs | 22 + .../Resilience/Internal/IPipelineMetering.cs | 27 + .../Internal/IPolicyPipelineBuilder.cs | 106 + .../Internal/IPolicyPipelineBuilderT.cs | 116 + .../Internal/IResiliencePipelineFactory.cs | 28 + .../Resilience/Internal/Log.cs | 32 + .../Resilience/Internal/Metric.cs | 22 + .../Resilience/Internal/NoopChangeToken.cs | 27 + .../Internal/OnChangeListenersHandler.cs | 57 + .../Internal/OptionsBuilderExtensions.cs | 50 + .../Resilience/Internal/OptionsNameHelper.cs | 8 + .../PipelineConfigurationChangeTokenSource.cs | 54 + .../Resilience/Internal/PipelineMetering.cs | 83 + .../Resilience/Internal/PipelineTelemetry.cs | 119 + .../Internal/PolicyPipelineBuilder.cs | 132 + .../Internal/PolicyPipelineBuilderT.cs | 142 + .../Internal/ResiliencePipelineBuilder.cs | 23 + .../ResiliencePipelineBuilderExtensions.cs | 154 + .../Internal/ResiliencePipelineFactory.cs | 50 + .../ResiliencePipelineFactoryOptions.cs | 25 + ...iliencePipelineFactoryOptionsValidatorT.cs | 12 + ...liencePipelineFactoryTokenSourceOptions.cs | 14 + .../Internal/ResiliencePipelineProvider.cs | 56 + .../Resilience/Internal/SupportedPolicies.cs | 40 + .../Resilience/Internal/TelemetryHelper.cs | 14 + ...encePipelineBuilderExtensions.BulkheadT.cs | 109 + ...pelineBuilderExtensions.CircuitBreakerT.cs | 109 + ...encePipelineBuilderExtensions.FallbackT.cs | 128 + ...iencePipelineBuilderExtensions.HedgingT.cs | 122 + ...iliencePipelineBuilderExtensions.RetryT.cs | 112 + ...iencePipelineBuilderExtensions.TimeoutT.cs | 112 + .../Resilience/ResilienceWrapperAttribute.cs | 15 + .../Resilience/ServiceCollectionExtensions.cs | 52 + .../Enrichment/EnricherExtensions.cs | 74 + .../Enrichment/IEnrichmentPropertyBag.cs | 75 + .../Enrichment/ILogEnricher.cs | 16 + .../Enrichment/IMetricEnricher.cs | 16 + .../Enrichment/ITraceEnricher.cs | 24 + .../Http/HttpRouteParameterRedactionMode.cs | 31 + .../Http/IDownstreamDependencyMetadata.cs | 27 + .../Http/IOutgoingRequestContext.cs | 15 + .../Http/RequestMetadata.cs | 83 + .../Http/TelemetryConstants.cs | 39 + .../Latency/Checkpoint.cs | 89 + .../Latency/ILatencyContext.cs | 69 + .../Latency/ILatencyContextProvider.cs | 16 + .../Latency/ILatencyDataExporter.cs | 22 + .../Latency/LatencyData.cs | 60 + .../Latency/Measure.cs | 80 + .../Latency/NullLatencyContext.cs | 54 + .../Latency/NullLatencyContextExtensions.cs | 31 + .../Latency/Registration/CheckpointToken.cs | 37 + .../ILatencyContextTokenIssuer.cs | 37 + .../LatencyContextRegistrationOptions.cs | 43 + .../Registration/LatencyRegistryExtensions.cs | 85 + .../Latency/Registration/MeasureToken.cs | 37 + .../Latency/Registration/TagToken.cs | 37 + .../Latency/Tag.cs | 34 + .../Logging/ILogPropertyCollector.cs | 26 + .../Logging/LogMethodAttribute.cs | 314 + .../Logging/LogMethodHelper.cs | 211 + .../Logging/LogPropertiesAttribute.cs | 116 + .../Logging/LogPropertyIgnoreAttribute.cs | 17 + .../Metering/CounterAttribute.cs | 79 + .../Metering/CounterAttributeT.cs | 86 + .../Metering/DimensionAttribute.cs | 44 + .../Metering/GaugeAttribute.cs | 79 + .../Metering/HistogramAttribute.cs | 79 + .../Metering/HistogramAttributeT.cs | 90 + .../Metering/MeterT.cs | 26 + .../Metering/MeteringExtensions.cs | 26 + ...t.Extensions.Telemetry.Abstractions.csproj | 42 + ...ft.Extensions.Telemetry.Abstractions.props | 2 + ....Extensions.Telemetry.Abstractions.targets | 3 + .../Latency/LarencyConsoleOptions.cs | 34 + .../Latency/LatencyConsoleExporter.cs | 135 + .../Latency/LatencyConsoleExtensions.cs | 73 + .../Logging/Internal/ColorSet.cs | 102 + .../Logging/Internal/Colors.cs | 832 ++ .../Internal/LogEntryCompositeState.cs | 27 + .../Logging/Internal/LogFormatter.cs | 207 + .../Logging/Internal/LogFormatterOptions.cs | 73 + .../Logging/Internal/LogFormatterTheme.cs | 38 + .../Logging/Internal/LogLevelExtensions.cs | 43 + .../Logging/Internal/TextWriterExtensions.cs | 221 + .../Logging/LoggingConsoleExporter.cs | 243 + .../Logging/LoggingConsoleExtensions.cs | 86 + .../Logging/LoggingConsoleOptions.cs | 127 + ...rosoft.Extensions.Telemetry.Console.csproj | 32 + .../Logging/FakeLogCollector.cs | 138 + .../Logging/FakeLogCollectorDebugView.cs | 23 + .../Logging/FakeLogCollectorOptions.cs | 64 + .../Logging/FakeLogRecord.cs | 128 + .../Logging/FakeLogger.cs | 165 + .../Logging/FakeLoggerExtensions.cs | 98 + .../Logging/FakeLoggerProvider.cs | 100 + .../Logging/FakeLoggerT.cs | 52 + .../Metering/Internal/AggregationType.cs | 11 + .../Metering/MetricCollector.MeterListener.cs | 152 + .../Metering/MetricCollector.cs | 395 + .../Metering/MetricCollectorT.cs | 30 + .../Metering/MetricValue.cs | 112 + .../Metering/MetricValuesHolder.cs | 226 + ...rosoft.Extensions.Telemetry.Testing.csproj | 34 + .../ProcessEnricherDimensions.cs | 34 + .../ProcessEnricherExtensions.cs | 85 + .../Enrichment.Process/ProcessLogEnricher.cs | 57 + .../ProcessLogEnricherOptions.cs | 26 + .../ServiceEnricherDimensions.cs | 46 + .../ServiceEnricherExtensions.cs | 226 + .../Enrichment.Service/ServiceLogEnricher.cs | 54 + .../ServiceLogEnricherOptions.cs | 44 + .../ServiceMetricEnricher.cs | 54 + .../ServiceMetricEnricherOptions.cs | 44 + .../ServiceTraceEnricher.cs | 73 + .../ServiceTraceEnricherOptions.cs | 44 + .../Latency/Internal/CheckpointTracker.cs | 78 + .../Latency/Internal/LatencyContext.cs | 116 + .../Latency/Internal/LatencyContextPool.cs | 58 + .../Internal/LatencyContextProvider.cs | 25 + .../Internal/LatencyContextRegistrySet.cs | 64 + .../Internal/LatencyContextTokenIssuer.cs | 41 + .../Internal/LatencyInstrumentProvider.cs | 29 + .../Latency/Internal/MeasureTracker.cs | 136 + .../Latency/Internal/Registry.cs | 79 + .../Latency/Internal/ResetOnGetObjectPool.cs | 33 + .../Latency/Internal/TagCollection.cs | 72 + .../Latency/LatencyContextExtensions.cs | 72 + .../Latency/LatencyContextOptions.cs | 17 + .../Logging/Logger.cs | 244 + .../Logging/LoggerProvider.cs | 127 + .../Logging/LoggingEventSource.cs | 28 + .../Logging/LoggingExtensions.cs | 93 + .../Logging/LoggingOptions.cs | 62 + .../Logging/LoggingOptionsValidator.cs | 12 + .../EventCountersCollectorOptions.cs | 68 + .../EventCountersExtensions.cs | 97 + .../EventCountersListener.cs | 243 + .../Internal/CustomConfigurationBinder.cs | 510 ++ .../Internal/CustomConfigureNamedOptions.cs | 28 + .../EventCountersCollectorOptionsValidator.cs | 12 + .../Internal/EventCountersValidator.cs | 54 + .../Internal/Log.cs | 89 + .../Metering/MeteringOptions.cs | 55 + .../Metering/MeteringState.cs | 20 + .../Metering/OTelMeteringExtensions.cs | 141 + .../Microsoft.Extensions.Telemetry.csproj | 52 + .../Telemetry.Internal/HttpHeadersRedactor.cs | 150 + .../Telemetry.Internal/HttpRouteFormatter.cs | 258 + .../Telemetry.Internal/HttpRouteParameter.cs | 40 + .../Telemetry.Internal/HttpRouteParser.cs | 284 + .../IDownstreamDependencyMetadataManager.cs | 28 + .../IHttpHeadersRedactor.cs | 22 + .../Telemetry.Internal/IHttpRouteFormatter.cs | 37 + .../Telemetry.Internal/IHttpRouteParser.cs | 39 + .../MetricEnrichmentPropertyBag.cs | 59 + .../OutgoingRequestContext.cs | 18 + .../Telemetry.Internal/ParsedRouteSegments.cs | 56 + .../Telemetry.Internal/Segment.cs | 72 + .../Telemetry.Internal/SelfDiagnostics.cs | 68 + .../SelfDiagnosticsConfigParser.cs | 157 + .../SelfDiagnosticsConfigParserRegex.cs | 36 + .../SelfDiagnosticsConfigRefresher.cs | 331 + .../SelfDiagnosticsEventListener.cs | 356 + .../SelfDiagnosticsEventSource.cs | 31 + .../TelemetryCommonExtensions.cs | 70 + .../Telemetry/Constants.cs | 34 + .../DownstreamDependencyMetadataManager.cs | 398 + ...ownstreamDependencyMetadataManagerRegex.cs | 26 + .../FrozenRequestMetadataTrieNode.cs | 13 + .../Telemetry/HostSuffixTrieNode.cs | 12 + .../Telemetry/RequestMetadataTrieNode.cs | 25 + .../Telemetry/TelemetryExtensions.cs | 134 + .../Internal/SamplingOptionsAutoValidator.cs | 12 + .../SamplingOptionsCustomValidator.cs | 41 + .../ParentBasedSamplerOptions.cs | 18 + .../Tracing.Sampling/SamplerType.cs | 30 + .../Tracing.Sampling/SamplingExtensions.cs | 101 + .../Tracing.Sampling/SamplingOptions.cs | 37 + .../TraceIdRatioBasedSamplerOptions.cs | 21 + .../Tracing/TraceEnrichmentProcessor.cs | 37 + .../Tracing/TracingEnricherExtensions.cs | 101 + .../FakeTimeProvider.cs | 185 + .../FakeTimeProviderTimer.cs | 146 + ...oft.Extensions.TimeProvider.Testing.csproj | 25 + .../Exceptions/DatabaseClientException.cs | 43 + .../Exceptions/DatabaseException.cs | 90 + .../Exceptions/DatabaseRetryableException.cs | 71 + .../Exceptions/DatabaseServerException.cs | 68 + .../IDocumentDatabase.cs | 165 + .../IDocumentReader.cs | 100 + .../IDocumentWriter.cs | 180 + .../Model/BatchItem.cs | 57 + .../Model/BatchOperation.cs | 35 + .../Model/ConsistencyLevel.cs | 59 + .../Model/DatabaseOptions.cs | 112 + .../Model/FetchMode.cs | 42 + .../Model/IDatabaseResponse.cs | 67 + .../Model/ITableLocator.cs | 36 + .../Model/PatchOperation.cs | 106 + .../Model/PatchOperationType.cs | 35 + .../Model/Query.cs | 46 + .../Model/QueryRequestOptions.cs | 100 + .../Model/RegionalDatabaseOptions.cs | 52 + .../Model/RequestInfo.cs | 49 + .../Model/RequestOptions.cs | 94 + .../Model/TableInfo.cs | 111 + .../Model/TableOptions.cs | 74 + .../Model/Throughput.cs | 39 + ...ystem.Cloud.DocumentDb.Abstractions.csproj | 29 + .../MessageCancelledTokenFeatureExtensions.cs | 56 + .../MessageCompleteActionFeatureExtensions.cs | 53 + .../MessageDestinationFeatureExtensions.cs | 61 + ...sageDestinationPayloadFeatureExtensions.cs | 115 + .../MessageLatencyContextFeatureExtensions.cs | 58 + .../MessagePostponeActionFeatureExtensions.cs | 54 + .../MessageSourceFeatureExtensions.cs | 61 + .../MessageSourcePayloadFeatureExtensions.cs | 115 + ...MessageVisibilityDelayFeatureExtensions.cs | 55 + ...rializedMessagePayloadFeatureExtensions.cs | 77 + .../LatencyRecorderMiddlewareExtensions.cs | 79 + ...syncProcessingPipelineBuilderExtensions.cs | 303 + .../IAsyncProcessingPipelineBuilder.cs | 22 + .../Startup/IPipelineDelegateFactory.cs | 20 + .../Startup/ServiceCollectionExtensions.cs | 53 + .../Features/IMessageCompleteActionFeature.cs | 20 + .../Features/IMessageDestinationFeatures.cs | 17 + .../Features/IMessagePayloadFeature.cs | 17 + .../Features/IMessagePostponeActionFeature.cs | 22 + .../Features/IMessageSourceFeatures.cs | 17 + .../IMessageVisibilityDelayFeature.cs | 17 + .../ISerializedMessagePayloadFeature.cs | 17 + .../Implementations/BaseMessageConsumer.cs | 184 + .../Interfaces/IMessageConsumer.cs | 20 + .../Interfaces/IMessageDelegate.cs | 26 + .../Interfaces/IMessageDestination.cs | 22 + .../Interfaces/IMessageMiddleware.cs | 26 + .../Interfaces/IMessageSource.cs | 26 + .../Interfaces/MessageContext.cs | 37 + .../PipelineMessageDelegateStitcher.cs | 35 + .../Extensions/MessageMiddlewareExtensions.cs | 28 + .../Features/MessageDestinationFeatures.cs | 24 + .../Features/MessagePayloadFeature.cs | 33 + .../Features/MessageSourceFeatures.cs | 24 + .../Features/MessageVisibilityDelayFeature.cs | 24 + .../SerializedMessagePayloadFeature.cs | 23 + .../LatencyContextProviderMiddleware.cs | 64 + .../Middlewares/LatencyRecorderMiddleware.cs | 70 + .../Startup/AsyncProcessingPipelineBuilder.cs | 28 + .../Startup/ConsumerBackgroundService.cs | 34 + .../Startup/PipelineDelegateFactory.cs | 74 + .../Internal/Utilities/Log.cs | 35 + .../Internal/Utilities/UTF8ConverterUtils.cs | 36 + ...System.Cloud.Messaging.Abstractions.csproj | 43 + src/Packages/Directory.Build.props | 12 + src/Packages/Directory.Build.targets | 3 + .../EmptyInternalClass.cs | 10 + .../Microsoft.Extensions.AuditReports.csproj | 28 + .../Microsoft.Extensions.AuditReports.props | 18 + .../Microsoft.Extensions.AuditReports.targets | 11 + .../EmptyInternalClass.cs | 10 + ...Microsoft.Extensions.ExtraAnalyzers.csproj | 30 + .../Microsoft.Extensions.ExtraAnalyzers.props | 2 + ...icrosoft.Extensions.ExtraAnalyzers.targets | 3 + ...Microsoft.Extensions.StaticAnalysis.csproj | 41 + src/Shared/BufferWriterPool/BufferWriter.cs | 192 + .../BufferWriterPool/BufferWriterPool.cs | 38 + .../BufferWriterPooledObjectPolicy.cs | 74 + src/Shared/BufferWriterPool/README.md | 11 + .../ExclusiveRangeAttribute.cs | 87 + src/Shared/Data.Validation/LengthAttribute.cs | 189 + src/Shared/Data.Validation/README.md | 11 + .../Data.Validation/TimeSpanAttribute.cs | 160 + .../ValidationContextExtensions.cs | 29 + src/Shared/Debugger/AttachedDebugger.cs | 28 + src/Shared/Debugger/DebuggerExtensions.cs | 62 + src/Shared/Debugger/DebuggerState.cs | 31 + src/Shared/Debugger/DetachedDebugger.cs | 28 + src/Shared/Debugger/IDebuggerState.cs | 17 + src/Shared/Debugger/README.md | 11 + src/Shared/Debugger/SystemDebugger.cs | 30 + src/Shared/EmptyCollections/Empty.cs | 49 + .../EmptyCollectionExtensions.cs | 140 + .../EmptyCollections/EmptyReadOnlyList.cs | 58 + .../EmptyReadonlyDictionary.cs | 63 + src/Shared/EmptyCollections/README.md | 11 + src/Shared/Memoization/Memoize.cs | 80 + src/Shared/Memoization/MemoizedFunction.cs | 237 + src/Shared/Memoization/README.md | 53 + .../NumericExtensions/NumericExtensions.cs | 108 + src/Shared/NumericExtensions/README.md | 11 + src/Shared/Pools/NoopPooledObjectPolicy.cs | 24 + src/Shared/Pools/PoolFactory.cs | 192 + .../PooledCancellationTokenSourcePolicy.cs | 36 + src/Shared/Pools/PooledDictionaryPolicy.cs | 31 + src/Shared/Pools/PooledListPolicy.cs | 29 + src/Shared/Pools/PooledSetPolicy.cs | 31 + src/Shared/Pools/README.md | 11 + src/Shared/RentedSpan/README.md | 11 + src/Shared/RentedSpan/RentedSpan.cs | 99 + src/Shared/Shared.csproj | 42 + src/Shared/Text.Formatting/CompositeFormat.cs | 915 ++ .../Text.Formatting/FormatExtensions.cs | 143 + src/Shared/Text.Formatting/README.md | 11 + src/Shared/Text.Formatting/Segment.cs | 44 + .../StringBuilderExtensions.cs | 98 + src/Shared/Text.Formatting/StringMaker.cs | 436 + src/Shared/Throw/README.md | 11 + src/Shared/Throw/Throw.cs | 974 +++ .../AutoActivationExtensions.cs | 296 + .../AutoActivationHostedService.cs | 39 + .../AutoActivatorOptions.cs | 12 + .../DependencyInjection.AutoActivation.csproj | 27 + .../DependencyInjection.NamedService.csproj | 24 + .../INamedServiceProvider.cs | 34 + .../NamedServiceCollectionExtensions.cs | 117 + .../NamedServiceDescriptor.cs | 26 + .../NamedServiceProvider.cs | 84 + .../NamedServiceProviderExtensions.cs | 38 + .../NamedServiceProviderOptions.cs | 12 + .../DependencyInjectedPolicy.cs | 36 + .../DependencyInjection.Pools.csproj | 27 + .../DependencyInjectionExtensions.cs | 98 + .../DependencyInjection.Pools/PoolOptions.cs | 19 + src/ToBeMoved/Directory.Build.props | 16 + .../Hosting.StartupInitialization.csproj | 39 + .../IStartupInitializationBuilder.cs | 43 + .../IStartupInitializer.cs | 21 + .../Internal/FunctionDerivedInitializer.cs | 23 + .../Internal/StartupHostedService.cs | 67 + .../Internal/StartupInitializationBuilder.cs | 71 + .../StartupInitializationOptionsValidator.cs | 12 + .../StartupInitializationExtensions.cs | 80 + .../StartupInitializationOptions.cs | 22 + .../HttpClient.SocketHandling.csproj | 33 + .../HttpClientSocketHandlingExtensions.cs | 68 + .../SocketsHttpHandlerBuilder.cs | 149 + .../SocketsHttpHandlerOptions.cs | 103 + .../SocketsHttpHandlerOptionsValidator.cs | 12 + src/ToBeRemoved/Directory.Build.props | 18 + .../Options.ValidateOnStart.csproj | 31 + .../OptionsBuilderExtensions.cs | 113 + .../ValidateOptionsResultExtensions.cs | 29 + .../ValidationHostedService.cs | 119 + .../ValidatorOptions.cs | 23 + start-code.cmd | 34 + start-code.sh | 28 + start-vs.cmd | 42 + test/.editorconfig | 7450 +++++++++++++++++ test/Directory.Build.props | 11 + test/Directory.Build.targets | 20 + test/Generators/Directory.Build.props | 16 + .../Generated/Common/BasicRequestsTests.cs | 253 + .../Generated/Common/BodyTests.cs | 86 + .../Generated/Common/HeadersTests.cs | 134 + .../Generated/Common/PathTests.cs | 81 + .../Generated/Common/QueryTests.cs | 123 + .../Common/RestApiClientOptionsTests.cs | 74 + .../Common/SpecialReturnTypeTests.cs | 154 + .../Generated/Common/TelemetryTests.cs | 167 + .../Generated/Directory.Build.props | 31 + ...utoClient.Roslyn3.8.Generated.Tests.csproj | 5 + ...utoClient.Roslyn4.0.Generated.Tests.csproj | 6 + .../TestClasses/IBasicTestClient.cs | 37 + .../TestClasses/IBodyTestClient.cs | 26 + .../ICustomRequestMetadataTestClient.cs | 20 + .../TestClasses/IParamHeaderTestClient.cs | 24 + .../TestClasses/IPathTestClient.cs | 19 + .../TestClasses/IQueryTestClient.cs | 30 + .../TestClasses/IRequestMetadataTestApi.cs | 16 + .../TestClasses/IRequestMetadataTestClient.cs | 19 + .../TestClasses/IRestApiClientOptionsApi.cs | 17 + .../TestClasses/IReturnTypesTestClient.cs | 27 + .../TestClasses/IStaticHeaderTestClient.cs | 22 + .../BodyContentTypeParamExtensionsTests.cs | 18 + .../Unit/Common/DiagDescriptorsTests.cs | 31 + .../Unit/Common/EmitterTests.cs | 43 + .../Unit/Common/ParserTests.cs | 495 ++ .../Common/RestApiMethodParameterTests.cs | 24 + .../Unit/Common/RestApiMethodTests.cs | 23 + .../Unit/Common/SymbolLoaderTests.cs | 17 + .../Unit/Directory.Build.props | 29 + ...Gen.AutoClient.Roslyn3.8.Unit.Tests.csproj | 5 + ...Gen.AutoClient.Roslyn4.0.Unit.Tests.csproj | 6 + .../GoldenReports/Basic.json | 119 + .../GoldenReports/Inheritance.json | 63 + .../GoldenReports/LogMethod.json | 34 + .../TestClasses/Basic.cs | 34 + .../TestClasses/Inheritance.cs | 21 + .../TestClasses/LogMethod.cs | 12 + .../Unit/Common/GeneratorTests.cs | 150 + .../Unit/Directory.Build.props | 39 + ...plianceReports.Roslyn3.8.Unit.Tests.csproj | 5 + ...plianceReports.Roslyn4.0.Unit.Tests.csproj | 6 + .../Common/ContextualOptionsTests.cs | 62 + .../Generated/Directory.Build.props | 29 + ...alOptions.Roslyn3.8.Generated.Tests.csproj | 5 + ...alOptions.Roslyn4.0.Generated.Tests.csproj | 6 + .../TestClasses/Class1.cs | 13 + .../TestClasses/Class2A.cs | 13 + .../TestClasses/Class2B.cs | 13 + .../TestClasses/ClassWithNoAttribute.cs | 20 + .../ClassWithUnusableProperties.txt | 22 + .../TestClasses/NamespacelessRecord.cs | 7 + .../TestClasses/NonPartialClass.txt | 12 + .../TestClasses/NonPublicStruct.cs | 14 + .../TestClasses/Record1.cs | 10 + .../TestClasses/RefStruct.txt | 12 + .../TestClasses/StaticClass.txt | 12 + .../TestClasses/Struct1.cs | 14 + .../Unit/Common/ContextualOptionsTests.cs | 55 + .../Unit/Common/DiagDescriptorsTests.cs | 31 + .../Unit/Common/EmitterTests.cs | 153 + .../Unit/Common/ParserTests.cs | 108 + .../Unit/Common/SyntaxContextReceiverTests.cs | 90 + .../Unit/Directory.Build.props | 29 + ...textualOptions.Roslyn3.8.Unit.Tests.csproj | 5 + ...textualOptions.Roslyn4.0.Unit.Tests.csproj | 6 + .../Generated/Common/EnumStringsTests.cs | 93 + .../Generated/Directory.Build.props | 33 + ...umStrings.Roslyn3.8.Generated.Tests.csproj | 5 + ...umStrings.Roslyn4.0.Generated.Tests.csproj | 6 + .../TestClasses/AssemblyLevel.cs | 24 + .../TestClasses/Flags.cs | 102 + .../TestClasses/Negative.cs | 41 + .../TestClasses/Nested.cs | 18 + .../TestClasses/Options.cs | 21 + .../TestClasses/Overlapping.cs | 38 + .../TestClasses/Sizes.cs | 78 + .../TestClasses/UnderlyingTypes.cs | 203 + .../Unit/Common/EmitterTests.cs | 59 + .../Unit/Common/ParserTests.cs | 111 + .../Unit/Directory.Build.props | 30 + ...en.EnumStrings.Roslyn3.8.Unit.Tests.csproj | 5 + ...en.EnumStrings.Roslyn4.0.Unit.Tests.csproj | 6 + .../Common/LogMethodAttributeTests.cs | 139 + .../Generated/Common/LogMethodTests.cs | 916 ++ .../Common/LogPropertiesProviderTests.cs | 382 + .../Common/LogPropertiesRedactionTests.cs | 162 + .../Generated/Common/LogPropertiesTests.cs | 487 ++ .../Generated/Common/StarRedactor.cs | 24 + .../Generated/Common/StarRedactorProvider.cs | 13 + .../Generated/Directory.Build.props | 29 + ...n.Logging.Roslyn3.8.Generated.Tests.csproj | 5 + ...n.Logging.Roslyn4.0.Generated.Tests.csproj | 6 + .../TestClasses/ArgTestExtensions.cs | 44 + .../TestClasses/AtSymbolsTestExtensions.cs | 26 + .../TestClasses/AttributeTestExtensions.cs | 73 + .../TestClasses/CollectionTestExtensions.cs | 43 + .../TestClasses/ConstraintsTestExtensions.cs | 89 + .../ConstructorVariationsTestExtensions.cs | 35 + .../TestClasses/CustomToStringTestClass.cs | 10 + .../TestClasses/EnumerableTestExtensions.cs | 67 + .../TestClasses/EventNameTestExtensions.cs | 17 + .../TestClasses/ExceptionTestExtensions.cs | 26 + .../TestClasses/FormattableTestExtensions.cs | 31 + .../TestClasses/InParameterTestExtensions.cs | 19 + .../TestClasses/InvariantTestExtensions.cs | 15 + .../TestClasses/LevelTestExtensions.cs | 51 + .../TestClasses/LogPropertiesExtensions.cs | 226 + .../LogPropertiesNullHandlingExtensions.cs | 30 + ...ogPropertiesOmitParameterNameExtensions.cs | 47 + .../LogPropertiesProviderExtensions.cs | 117 + ...gPropertiesProviderWithObjectExtensions.cs | 45 + .../LogPropertiesRedactionExtensions.cs | 65 + .../LogPropertiesSimpleExtensions.cs | 29 + .../LogPropertiesSpecialTypesExtensions.cs | 45 + .../TestClasses/MessageTestExtensions.cs | 52 + .../TestClasses/MiscTestExtensions.cs | 38 + .../TestClasses/NamespaceTestExtensions.cs | 17 + .../TestClasses/NestedClassTestExtensions.cs | 88 + .../TestClasses/NonStaticNullableTestClass.cs | 25 + .../TestClasses/NonStaticTestClass.cs | 66 + .../TestClasses/NullableTestExtensions.cs | 33 + .../TestClasses/OverloadsTestExtensions.cs | 17 + .../TestClasses/RecordTestExtensions.cs | 14 + .../TestClasses/SignatureTestExtensions.cs | 83 + .../SkipEnabledCheckTestExtensions.cs | 21 + .../TestClasses/StructTestExtensions.cs | 14 + .../TestClasses/TemplateTestExtensions.cs | 25 + .../TestClasses/TestInstances.cs | 29 + .../Unit/Common/AttributeParserTests.cs | 310 + .../Unit/Common/DiagDescriptorsTests.cs | 32 + .../Unit/Common/EmitterTests.cs | 72 + .../Unit/Common/EmitterUtilsTests.cs | 91 + .../Unit/Common/LogParserUtilitiesTests.cs | 120 + .../Common/LoggingMethodParameterTests.cs | 81 + .../Unit/Common/LoggingMethodTests.cs | 39 + .../Unit/Common/LoggingTypeTests.cs | 19 + .../Unit/Common/ParserTests.LogProperties.cs | 718 ++ .../Unit/Common/ParserTests.cs | 786 ++ .../Unit/Common/ParserUtilitiesTests.cs | 113 + .../Unit/Common/SymbolLoaderTests.cs | 57 + .../Unit/Common/TemplatesExtractorTests.cs | 23 + .../Unit/Directory.Build.props | 32 + ...ft.Gen.Logging.Roslyn3.8.Unit.Tests.csproj | 5 + ...ft.Gen.Logging.Roslyn4.0.Unit.Tests.csproj | 6 + .../Generated/Common/MetricTests.Ext.cs | 307 + .../Generated/Common/MetricTests.cs | 613 ++ .../Generated/Directory.Build.props | 30 + ....Metering.Roslyn3.8.Generated.Tests.csproj | 5 + ....Metering.Roslyn4.0.Generated.Tests.csproj | 6 + .../TestClasses/AttributedWithoutNamespace.cs | 19 + .../TestClasses/CounterDimensions.cs | 92 + .../TestClasses/CounterTestExtensions.cs | 122 + .../FileScopedNamespaceExtensions.cs | 23 + .../TestClasses/HistogramTestExtensions.cs | 175 + .../TestClasses/MeterTExtensions.cs | 62 + .../TestClasses/MetricConstants.cs | 14 + .../MetricRecordClassTestExtensions.cs | 14 + .../MetricRecordStructTestExtensions.cs | 18 + .../TestClasses/MetricStructTestExtensions.cs | 14 + .../TestClasses/NestedClassMetrics.cs | 37 + .../TestClasses/NestedRecordClassMetrics.cs | 25 + .../TestClasses/NestedRecordStructMetrics.cs | 29 + .../TestClasses/NestedStructMetrics.cs | 25 + .../OverlappingNamesTestExtensions.cs | 39 + .../TestClasses/Public.cs | 22 + .../Unit/Common/DiagDescriptorsTests.cs | 31 + .../Unit/Common/EmitterTests.cs | 101 + .../Unit/Common/MetricMethodTests.cs | 20 + .../Unit/Common/MetricParameterTests.cs | 18 + .../Unit/Common/MetricTypeTests.cs | 21 + .../Unit/Common/ParserTests.StrongTypes.cs | 555 ++ .../Unit/Common/ParserTests.cs | 658 ++ .../Unit/Common/StrongTypeConfigTests.cs | 19 + .../Unit/Directory.Build.props | 26 + ...t.Gen.Metering.Roslyn3.8.Unit.Tests.csproj | 5 + ...t.Gen.Metering.Roslyn4.0.Unit.Tests.csproj | 6 + .../Unit/Common/EmitterTests.cs | 176 + .../Unit/Common/GeneratorTests.cs | 22 + .../Unit/Directory.Build.props | 26 + ...eteringReports.Roslyn3.8.Unit.Tests.csproj | 5 + ...eteringReports.Roslyn4.0.Unit.Tests.csproj | 6 + .../Generated/Common/CustomAttrTests.cs | 39 + .../Generated/Common/EnumerationTests.cs | 93 + .../Generated/Common/FieldTests.cs | 62 + .../Generated/Common/FunnyStringsTests.cs | 37 + .../Generated/Common/GenericsTests.cs | 49 + .../Common/MultiModelValidatorTests.cs | 49 + .../Generated/Common/NestedTests.cs | 67 + .../Generated/Common/NoNamespaceTests.cs | 60 + .../Common/OptionsValidationTests.cs | 442 + .../Generated/Common/RandomMembersTests.cs | 37 + .../Generated/Common/RecordTypesTests.cs | 67 + .../Generated/Common/RepeatedTypesTests.cs | 61 + .../Generated/Common/SelfValidationTests.cs | 37 + .../Generated/Common/TestResource.Designer.cs | 72 + .../Generated/Common/TestResource.resx | 123 + .../Generated/Common/Utils.cs | 31 + .../Generated/Common/ValueTypesTests.cs | 53 + .../Generated/Directory.Build.props | 33 + ...alidation.Roslyn3.8.Generated.Tests.csproj | 13 + ...alidation.Roslyn4.0.Generated.Tests.csproj | 14 + .../TestClasses/CustomAttr.cs | 69 + .../TestClasses/Enumeration.cs | 94 + .../TestClasses/Fields.cs | 72 + .../TestClasses/FileScopedNamespace.cs | 22 + .../TestClasses/FunnyStrings.cs | 23 + .../TestClasses/Generics.cs | 36 + .../TestClasses/Models.cs | 252 + .../TestClasses/MultiModelValidator.cs | 34 + .../TestClasses/Nested.cs | 106 + .../TestClasses/NoNamespace.cs | 46 + .../TestClasses/RandomMembers.cs | 35 + .../TestClasses/RecordTypes.cs | 68 + .../TestClasses/RepeatedTypes.cs | 47 + .../TestClasses/SelfValidation.cs | 34 + .../TestClasses/ValueTypes.cs | 46 + .../Unit/Common/EmitterTests.cs | 58 + .../Unit/Common/ParserTests.Enumeration.cs | 212 + .../Unit/Common/ParserTests.cs | 930 ++ .../Unit/Common/SymbolLoaderTests.cs | 80 + .../Unit/Directory.Build.props | 31 + ...ionsValidation.Roslyn3.8.Unit.Tests.csproj | 5 + ...ionsValidation.Roslyn4.0.Unit.Tests.csproj | 6 + test/Generators/Shared/RoslynTestUtils.cs | 546 ++ test/Libraries/Directory.Build.props | 8 + .../AsyncContextHttpContextOfTTests.cs | 102 + .../AsyncStateHttpContextExtensionsTests.cs | 34 + ...crosoft.AspNetCore.AsyncState.Tests.csproj | 14 + .../Mock/IThing.cs | 9 + .../Mock/Thing.cs | 12 + .../AcceptanceTest.cs | 77 + .../ConnectionTimeoutDelegateTests.cs | 115 + .../ConnectionTimeoutExtensionsTests.cs | 49 + .../ConnectionTimeoutValidatorTests.cs | 44 + ....AspNetCore.ConnectionTimeout.Tests.csproj | 17 + .../Startup.cs | 34 + .../StaticFakeClockExecution.cs | 11 + .../CommonHeadersTests.cs | 147 + .../HeaderKeyTests.cs | 56 + .../HeaderParsingExtensionsTests.cs | 141 + .../HeaderParsingFeatureTests.cs | 202 + .../HeaderParsingOptionsTests.cs | 29 + .../HeaderRegistryTests.cs | 100 + .../HeaderSetupTests.cs | 33 + .../HostHeaderValueTests.cs | 86 + ...soft.AspNetCore.HeaderParsing.Tests.csproj | 17 + .../ParserTests.cs | 431 + .../Latency/AcceptanceTest.cs | 95 + .../Latency/Checkpoint/AcceptanceTest.cs | 137 + .../AddServerTimingHeaderMiddlewareTest.cs | 84 + .../LatencyContextControlExtensionsTest.cs | 40 + .../RequestCheckpointExtensionsTest.cs | 29 + .../RequestLatencyTelemetryMiddlewareTest.cs | 140 + ...estLatencyTelemetryOptionsValidatorTest.cs | 29 + .../RequestLatencyTelemetryExtensionsTest.cs | 101 + .../Logging/AcceptanceTest.Mvc.cs | 253 + .../Logging/AcceptanceTest.Routing.cs | 196 + .../Logging/AcceptanceTest.cs | 878 ++ .../Controllers/ApiRoutingController.cs | 35 + .../Controllers/AttributeRoutingController.cs | 28 + .../ConventionalRoutingController.cs | 18 + .../Controllers/MixedRoutingController.cs | 25 + .../Logging/HeaderReaderTest.cs | 56 + .../HttpLoggingServiceExtensionsTest.cs | 68 + .../Logging/HttpRequestBodyReaderTest.cs | 51 + .../Logging/IncomingHttpDimensionsTest.cs | 20 + .../Logging/IncomingRequestLogRecordTest.cs | 19 + .../Logging/IncomingRequestStructTest.cs | 32 + .../Logging/Internal/InfiniteStream.cs | 51 + .../Internal/RequestBodyErrorPipeFeature.cs | 27 + .../RequestBodyMultiSegmentPipeFeature.cs | 59 + .../Internal/TestBodyPipeFeatureMiddleware.cs | 26 + .../Logging/Internal/TestHttpLogEnricher.cs | 22 + .../Logging/LoggingMiddlewareTest.cs | 52 + .../Logging/LoggingOptionsValidationTest.cs | 104 + .../Logging/MediaTypeSetExtensionsTest.cs | 42 + .../Logging/PipeReaderExtensionsTest.cs | 61 + .../Logging/ResponseInterceptingStreamTest.cs | 204 + .../Metering/HttpMeteringTests.cs | 596 ++ .../Metering/Internals/NullRequestEnricher.cs | 18 + .../Internals/PropertyBagEdgeCaseEnricher.cs | 18 + .../Internals/SameDefaultDimEnricher.cs | 17 + .../Metering/Internals/TestEnricher.cs | 46 + ...pNetCore.Telemetry.Middleware.Tests.csproj | 30 + .../appsettings.json | 21 + .../Internals/TestExtensions.cs | 17 + .../RequestHeadersEnricherExtensionsTests.cs | 120 + .../RequestHeadersEnricherTests.cs | 248 + ...icrosoft.AspNetCore.Telemetry.Tests.csproj | 37 + .../HttpUtilityExtensionsTests.cs | 132 + .../IncomingHttpRouteUtilityTests.cs | 491 ++ .../Telemetry.Internal.Http/TestController.cs | 60 + .../HttpTracingExtensionsTests.cs | 171 + .../HttpTracingOptionsValidationTests.cs | 70 + .../HttpUrlRedactionProcessorTests.cs | 551 ++ .../Internal/ConfigurationExtensions.cs | 32 + .../Tracing.Http/Internal/TestEnricher.cs | 24 + .../Tracing.Http/Internal/TestEnricher2.cs | 16 + .../Internal/TestEventListener.cs | 21 + .../Tracing.Http/Internal/TestExporter.cs | 18 + .../Internal/TestHttpClientProvider.cs | 134 + .../Internal/TestHttpTraceEnricher.cs | 24 + .../Internal/TestTraceProcessor.cs | 20 + .../Internal/TracerProviderExtensions.cs | 24 + .../WrappedActivityExportProcessor.cs | 27 + .../appsettings.json | 10 + .../FakesExtensionsTest.cs | 255 + .../Internal/FakeCertificateFactoryTest.cs | 42 + .../FakeCertificateHttpClientHandlerTest.cs | 64 + .../Internal/FakeStartupTest.cs | 28 + .../Microsoft.AspNetCore.Testing.Tests.csproj | 15 + .../ReturningHttpClientHandler.cs | 24 + .../TestResources/Startup.cs | 25 + .../TestResources/TestHandler.cs | 18 + .../AcceptanceTests.cs | 63 + .../ApplicationMetadataExtensionsTests.cs | 144 + .../ApplicationMetadataSourceTests.cs | 67 + .../ApplicationMetadataTests.cs | 74 + .../ApplicationMetadataValidatorTests.cs | 47 + ...s.AmbientMetadata.Application.Tests.csproj | 15 + ...ContextServiceCollectionExtensionsTests.cs | 111 + .../AsyncContextTests.cs | 256 + .../AsyncStateTests.cs | 232 + .../AsyncStateTokenTests.cs | 44 + .../FeaturesPooledPolicyTests.cs | 32 + ...crosoft.Extensions.AsyncState.Tests.csproj | 14 + .../Mock/AnotherThing.cs | 12 + .../Mock/IThing.cs | 9 + .../Mock/Thing.cs | 12 + .../DataClassificationAttributeTest.cs | 31 + .../Classification/DataClassificationTest.cs | 71 + .../NoDataClassificationAttributeTest.cs | 16 + .../UnknownDataClassificationAttributeTest.cs | 16 + ...sions.Compliance.Abstractions.Tests.csproj | 10 + .../Redaction/FakeFormattable.cs | 18 + .../Redaction/FakeObject.cs | 16 + .../Redaction/FakeSpanFormattable.cs | 44 + .../Redaction/NullRedactor.cs | 41 + .../RedactorAbstractionsExtensionsTest.cs | 239 + .../ErasingRedactorTest.cs | 60 + .../FakePlaintextRedactor.cs | 17 + .../FakeStartup.cs | 17 + ...tensions.Compliance.Redaction.Tests.csproj | 16 + .../NullRedactorProvider.cs | 16 + .../NullRedactorTest.cs | 55 + .../RedactionAcceptanceTest.cs | 128 + .../RedactorProviderTest.cs | 81 + .../XXHash3RedactorExtensionsTests.cs | 89 + .../XXHash3RedactorTests.cs | 69 + .../AttributeTest.cs | 22 + .../FakeRedactorOptionsValidatorTest.cs | 45 + .../FakeRedactorProviderTest.cs | 29 + .../FakeRedactorTest.cs | 101 + .../InstancesTest.cs | 16 + ...Extensions.Compliance.Testing.Tests.csproj | 17 + .../RedactionFakesAcceptanceTest.cs | 257 + .../RedactionFakesEventCollectorTest.cs | 70 + .../Setup.cs | 24 + .../TaxonomyExtensionsTest.cs | 21 + .../Abstractions/ExceptionSummaryTest.cs | 101 + .../ExceptionSummarizerTests.cs | 297 + .../ExceptionSummaryExtensionsTests.cs | 22 + ...ExceptionSummaryProviderExtensionsTests.cs | 22 + .../HttpExceptionSummaryProviderTests.cs | 153 + .../Implementation/TestException.cs | 34 + ...ostics.ExceptionSummarization.Tests.csproj | 14 + .../ApplicationLifecycleHealthCheckTest.cs | 30 + ...tionLifecycleHealthChecksExtensionsTest.cs | 54 + ...netesHealthCheckPublisherExtensionsTest.cs | 101 + .../KubernetesHealthCheckPublisherTest.cs | 74 + .../ManualHealthCheckExtensionsTest.cs | 50 + .../ManualHealthCheckTest.cs | 118 + ...Diagnostics.HealthChecks.Core.Tests.csproj | 17 + .../MockHostApplicationLifetime.cs | 50 + ...etryHealthChecksPublisherExtensionsTest.cs | 32 + .../TelemetryHealthChecksPublisherTest.cs | 139 + ...lthChecks.ResourceUtilization.Tests.csproj | 16 + .../ResourceHealthCheckExtensionsTest.cs | 169 + .../ResourceHealthCheckTest.cs | 158 + .../Abstractions/Helpers/DummyProvider.cs | 34 + .../Abstractions/Helpers/DummyTracker.cs | 17 + ...ResourceUtilizationSnapshotProviderTest.cs | 30 + .../IResourceUtilizationTrackerTest.cs | 25 + ...llResourceUtilizationTrackerServiceTest.cs | 24 + .../NullSnapshotProviderTest.cs | 41 + ...ceUtilizationAbstractionsExtensionsTest.cs | 43 + .../ResourceUtilizationSnapshotTest.cs | 40 + .../Abstractions/SystemResourcesTest.cs | 38 + .../Abstractions/UtilizationTest.cs | 31 + .../Core/CalculatorTest.cs | 196 + .../Core/CircularBufferTest.cs | 85 + .../Providers/ConditionallyFaultProvider.cs | 41 + .../Core/Providers/FakeProvider.cs | 29 + .../Core/Providers/FaultProvider.cs | 28 + .../Core/Publishers/AnotherPublisher.cs | 19 + .../Core/Publishers/EmptyPublisher.cs | 19 + .../Core/Publishers/FaultPublisher.cs | 21 + .../Core/Publishers/GenericPublisher.cs | 27 + .../Core/ResourceUtilizationBuilderTest.cs | 52 + ...esourceUtilizationTrackerExtensionsTest.cs | 174 + ...zationTrackerOptionsManualValidatorTest.cs | 52 + .../ResourceUtilizationTrackerOptionsTest.cs | 23 + ...eUtilizationTrackerOptionsValidatorTest.cs | 118 + .../ResourceUtilizationTrackerServiceTest.cs | 715 ++ .../AcceptanceTestResourceUtilizationLinux.cs | 241 + .../Linux/LinuxCountersTest.cs | 63 + .../Linux/LinuxUtilizationExtensionsTest.cs | 26 + .../Linux/LinuxUtilizationParserTest.cs | 323 + .../Linux/LinuxUtilizationProviderTest.cs | 18 + .../Linux/OSFileSystemTest.cs | 59 + .../Linux/Resources/FakeOperatingSystem.cs | 16 + .../Linux/Resources/FakeUserHz.cs | 16 + .../Resources/FileNamesOnlyFileSystem.cs | 49 + .../Linux/Resources/GenericPublisher.cs | 27 + .../Resources/HardcodedValueFileSystem.cs | 80 + .../Resources/PathReturningFileSystem.cs | 35 + .../Linux/Resources/TestResources.cs | 81 + ...iagnostics.ResourceMonitoring.Tests.csproj | 29 + .../Windows/MemoryInfoTest.cs | 24 + .../Windows/SystemInfoTest.cs | 27 + .../Windows/TcpTableInfoTest.cs | 262 + .../WindowsContainerSnapshotProviderTest.cs | 236 + ...ndowsCountersOptionsCustomValidatorTest.cs | 38 + .../Windows/WindowsCountersTest.cs | 104 + .../Windows/WindowsSnapshotProviderTest.cs | 39 + .../WindowsUtilizationExtensionsTest.cs | 104 + .../fixtures/FileWithRChars | 1 + .../fixtures/cpu.cfs_period_us | 1 + .../fixtures/cpu.cfs_quota_us | 1 + .../fixtures/cpuacct.stat | 2 + .../fixtures/cpuset.cpus | 1 + .../fixtures/meminfo | 50 + .../fixtures/memory.limit_in_bytes | 1 + .../fixtures/memory.usage_in_bytes | 1 + .../fixtures/status | 56 + .../fixtures/test.cpuacct.stat | 2 + .../EnumStringsAttributeTests.cs | 45 + ...rosoft.Extensions.EnumStrings.Tests.csproj | 10 + .../FakeConfigurationSourceTest.cs | 40 + .../FakeHostBuilderTest.cs | 204 + .../FakeHostTest.cs | 191 + .../HostTerminatorServiceTest.cs | 75 + .../HostingFakesExtensionsTest.cs | 353 + ...ft.Extensions.Hosting.Testing.Tests.csproj | 12 + .../TestResources/DependentClass.cs | 11 + .../TestResources/InnerClass.cs | 8 + .../TestResources/OuterClass.cs | 11 + .../InterfaceAttributesTests.cs | 28 + .../MethodAttributesTests.cs | 59 + ...ft.Extensions.Http.AutoClient.Tests.csproj | 14 + .../ParameterAttributesTests.cs | 43 + .../RestApiExceptionTests.cs | 49 + .../Core/FallbackClientHandlerOptionsTests.cs | 45 + .../Core/Helpers/ConfigurationStubFactory.cs | 22 + .../Core/Helpers/OptionsUtilities.cs | 59 + ...ClientBuilderExtensionsTests.BySelector.cs | 99 + ...tpClientBuilderExtensionsTests.Fallback.cs | 134 + ...ClientBuilderExtensionsTests.Resilience.cs | 250 + ...tpClientBuilderExtensionsTests.Standard.cs | 195 + .../HttpStandardResilienceOptionsTests.cs | 32 + .../Core/Internal/ContextExtensionsTest.cs | 83 + .../Internal/DefaultRequestClonerTests.cs | 109 + ...lbackClientHandlerOptionsValidatorTests.cs | 99 + .../Core/Internal/FallbackTests.cs | 91 + .../HttpRequestMessageExtensionsTests.cs | 40 + .../HttpResiliencePipelineBuilderTest.cs | 24 + .../Core/Internal/PipelineNameHelperTest.cs | 15 + .../Core/Internal/ResilienceHandlerTest.cs | 104 + ...rdResilienceOptionsCustomValidatorTests.cs | 105 + .../Validators/ValidationHelperTests.cs | 59 + .../Core/TestHandlerStub.cs | 37 + .../HttpClientBuilderExtensionsTest.cs | 276 + .../HttpClientFaultInjectionExtensionsTest.cs | 238 + .../HttpFaultInjectionOptionsBuilderTest.cs | 175 + .../FaultInjectionTelemetryHandlerTests.cs | 70 + ...ightAssignmentContextMessageHandlerTest.cs | 83 + .../HttpClientChaosPolicyFactoryTest.cs | 195 + .../HttpContentOptionsRegistryTest.cs | 56 + .../PolicyContextExtensionsTest.cs | 52 + .../Hedging/HedgingTests.cs | 291 + .../Helpers/ConfigurationStubFactory.cs | 22 + .../Hedging/Helpers/OptionsUtilities.cs | 59 + .../Hedging/Helpers/TestHandlerStub.cs | 24 + ...pClientHedgingResiliencePredicatesTests.cs | 22 + .../Hedging/HttpHedgingPolicyOptionsTests.cs | 66 + .../DefaultRoutingStrategyFactoryTests.cs | 57 + .../Internal/HedgingContextExtensionsTests.cs | 44 + .../Hedging/Internal/IStubRoutingService.cs | 11 + .../Hedging/Internal/MockRoutingStrategy.cs | 28 + .../Hedging/Internal/RandomizerTest.cs | 49 + .../RequestMessageSnapshotPolicyTests.cs | 35 + .../Hedging/Internal/RoutingHelperTest.cs | 36 + ...ngResilienceOptionsCustomValidatorTests.cs | 130 + .../Routing/OrderedRoutingStrategyTest.cs | 117 + .../Hedging/Routing/RoutingStrategyTest.cs | 199 + .../Routing/WeightedRoutingStrategyTest.cs | 177 + .../Hedging/StandardHedgingTests.cs | 211 + ...ft.Extensions.Http.Resilience.Tests.csproj | 38 + .../HttpCircuitBreakerPolicyOptionsTests.cs | 147 + .../HttpClientResiliencePredicatesTests.cs | 63 + .../Polly/HttpFallbackPolicyOptionsTests.cs | 63 + .../HttpResponseMessageExtensionsTests.cs | 46 + .../Polly/HttpRetryPolicyOptionTests.cs | 189 + .../configs/appsettings.json | 23 + ...ttpClientLatencyTelemetryExtensionsTest.cs | 119 + .../Latency/Internal/HttpCheckpointsTest.cs | 17 + .../HttpClientLatencyLogEnricherTest.cs | 74 + .../HttpLatencyTelemetryHandlerTest.cs | 99 + .../Latency/Internal/HttpMockProvider.cs | 64 + .../HttpRequestLatencyListenerTest.cs | 214 + .../HttpClientLoggingAcceptanceTest.cs | 73 + .../HttpClientLoggingDimensionsTest.cs | 31 + .../HttpClientLoggingExtensionsTest.cs | 858 ++ .../Logging/HttpHeadersReaderTest.cs | 126 + .../Logging/HttpLoggingHandlerTest.cs | 1074 +++ .../Logging/HttpRequestBodyReaderTest.cs | 263 + .../Logging/HttpRequestReaderTest.cs | 605 ++ .../Logging/HttpResponseBodyReaderTest.cs | 185 + .../Logging/Internal/EmptyEnricher.cs | 15 + .../Logging/Internal/EnricherWithCounter.cs | 16 + .../Logging/Internal/HelperExtensions.cs | 23 + .../Logging/Internal/ITestHttpClient1.cs | 12 + .../Logging/Internal/ITestHttpClient2.cs | 12 + .../Logging/Internal/LogRecordExtensions.cs | 36 + .../Logging/Internal/MockedLogger.cs | 30 + .../Logging/Internal/MockedRequestReader.cs | 31 + .../Logging/Internal/NoRemoteCallHandler.cs | 42 + .../Logging/Internal/NotSeekableStream.cs | 50 + .../Logging/Internal/RandomStringGenerator.cs | 22 + .../Logging/Internal/TestConfiguration.cs | 28 + .../Logging/Internal/TestEnricher.cs | 30 + .../Logging/Internal/TestHttpClient1.cs | 22 + .../Logging/Internal/TestHttpClient2.cs | 22 + .../Internal/TestHttpMessageHandlerBuilder.cs | 29 + .../Logging/Internal/TestingHandlerStub.cs | 21 + .../LogRecordPooledObjectPolicyTest.cs | 49 + .../Logging/LoggingOptionsTest.cs | 133 + .../Logging/LoggingOptionsValidatorTest.cs | 47 + .../MediaTypeCollectionExtensionsTest.cs | 58 + .../Metering/HttpMeteringHandlerTests.cs | 729 ++ .../Metering/Internal/HelperExtensions.cs | 23 + .../Metering/Internal/NoRemoteCallHandler.cs | 21 + .../NullOutgoingRequestMetricEnricher.cs | 17 + .../Internal/PropertyBagEdgeCaseEnricher.cs | 18 + .../Internal/SameDefaultDimEnricher.cs | 17 + .../TestDownstreamDependencyMetadata.cs | 26 + .../Metering/Internal/TestEnricher.cs | 46 + .../Metering/TestHandlerStub.cs | 24 + ...oft.Extensions.Http.Telemetry.Tests.csproj | 35 + .../Text.txt | 1 + .../HttpClientRedactionProcessorTests.cs | 505 ++ ...HttpClientTraceEnrichmentProcessorTests.cs | 127 + .../HttpClientTracingExtensionsTests.cs | 330 + ...HttpClientTracingOptionsValidationTests.cs | 54 + .../Tracing/Internal/FakeHttpWebResponse.cs | 35 + .../Tracing/Internal/TestEventListener.cs | 21 + .../Tracing/Internal/TestExtensions.cs | 21 + .../Internal/TestHttpClientTraceEnricher.cs | 48 + .../Tracing/Internal/TestHttpPathRedactor.cs | 17 + .../Tracing/Internal/TestHttpServer.cs | 113 + .../Tracing/Internal/TestTraceProcessor.cs | 20 + .../appsettings.json | 4 + .../AcceptanceTest.cs | 108 + .../ContextualOptionsFactoryTests.cs | 235 + ...OptionsServiceCollectionExtensionsTests.cs | 103 + ...Extensions.Options.Contextual.Tests.csproj | 20 + ...Extensions.Options.Validation.Tests.csproj | 22 + .../ValidateEnumeratedItemsAttributeTests.cs | 20 + .../ValidateObjectMembersAttributeTest.cs | 20 + .../FaultInjectionExtensionsTest.cs | 267 + .../FaultInjectionOptionsBuilderTest.cs | 128 + .../InjectedFaultExceptionTests.cs | 41 + .../Internals/ChaosPolicyFactoryTest.cs | 270 + .../Internals/ExceptionRegistryTest.cs | 54 + .../FaultInjectionOptionsProviderTest.cs | 100 + .../FaultInjectionTelemetryHandlerTests.cs | 40 + .../Internals/WeightAssignmentHelperTest.cs | 64 + .../Options/ChaosPolicyOptionsGroupTest.cs | 49 + .../Options/ExceptionPolicyOptionTest.cs | 58 + .../Options/FaultInjectionOptionsTest.cs | 29 + .../HttpResponseInjectionPolicyOptionTest.cs | 59 + .../Options/LatencyPolicyOptionTest.cs | 59 + .../Options/OptionsValidationTests.cs | 133 + ...crosoft.Extensions.Resilience.Tests.csproj | 37 + .../Polly/GlobalSuppressions.cs | 6 + .../Polly/Hedging/AsyncHedgingPolicyTests.cs | 545 ++ .../AsyncHedgingPolicyTestsNonGeneric.cs | 401 + .../Polly/Hedging/AsyncHedgingSyntaxTests.cs | 108 + .../Hedging/FakeTimeProviderExtensions.cs | 20 + .../Polly/Hedging/HedgingEngineTest.cs | 305 + .../Polly/Hedging/HedgingTestUtilities.cs | 137 + .../Polly/Hedging/TaskHelper.cs | 56 + .../HedgingTaskProviderArgumentsTests.cs | 40 + .../Polly/Helpers/AssertionFailure.cs | 27 + .../Polly/Helpers/CustomObject.cs | 21 + .../Polly/Helpers/DisposableResult.cs | 21 + .../Helpers/FailureResultContextHelper.cs | 19 + .../Polly/Internals/ContextExtensionsTests.cs | 18 + .../Internals/FailureReasonResolverTest.cs | 56 + .../Polly/Internals/PipelineIdTests.cs | 49 + .../Polly/Internals/PolicyFactoryTests.cs | 1487 ++++ .../Polly/Internals/PolicyMeteringTests.cs | 211 + .../RetryPolicyOptionsCustomValidatorTests.cs | 40 + .../Options/BreakActionArgumentsTests.cs | 45 + .../BreakActionArgumentsTestsNonGeneric.cs | 45 + .../Options/BulkheadPolicyOptionsTests.cs | 78 + .../Options/BulkheadTaskArgumentsTests.cs | 35 + .../CircuitBreakerPolicyOptionsTests.cs | 174 + ...cuitBreakerPolicyOptionsTestsNonGeneric.cs | 151 + .../Polly/Options/Constants.cs | 49 + .../Options/FallbackPolicyOptionsTests.cs | 81 + .../FallbackPolicyOptionsTestsNonGeneric.cs | 61 + .../FallbackScenarioTaskArgumentsTests.cs | 35 + .../Options/FallbackTaskArgumentsTests.cs | 43 + .../FallbackTaskArgumentsTestsNonGeneric.cs | 43 + .../Options/HedgingDelayArgumentsTest.cs | 40 + .../Options/HedgingPolicyOptionsTests.cs | 162 + .../HedgingPolicyOptionsTestsNonGeneric.cs | 124 + .../Options/HedgingTaskArgumentsTests.cs | 45 + .../HedgingTaskArgumentsTestsNonGeneric.cs | 44 + .../Polly/Options/OptionsUtilities.cs | 59 + .../Options/ResetActionArgumentsTests.cs | 32 + .../Options/RetryActionArgumentsTests.cs | 49 + .../RetryActionArgumentsTestsNonGeneric.cs | 48 + .../Polly/Options/RetryDelayArgumentsTests.cs | 42 + .../Polly/Options/RetryPolicyOptionsTests.cs | 121 + .../RetryPolicyOptionsTestsNonGeneric.cs | 86 + .../Options/TimeoutPolicyOptionsTests.cs | 80 + .../Options/TimeoutTaskArgumentsTests.cs | 32 + .../PollyServiceCollectionExtensionsTests.cs | 83 + .../Polly/ResilienceDimensionsTests.cs | 28 + ...ResiliencePollyFakeClockTestsCollection.cs | 11 + .../Polly/RetryOptionsExtensionsTests.cs | 100 + .../Helpers/ResilienceTestHelper.cs | 87 + .../AsyncPolicyPipelineTResultTests.cs | 75 + .../Internal/AsyncPolicyPipelineTests.cs | 94 + .../Internal/NoopChangeTokenTests.cs | 22 + .../Internal/OnChangeListenersHandlerTests.cs | 81 + .../Internal/OptionsNameHelperTest.cs | 17 + .../Internal/PipelineMeteringTests.cs | 171 + .../Internal/PipelineTelemetryTests.cs | 112 + .../PolicyPipelineBuilderTResultTest.cs | 257 + .../Internal/PolicyPipelineBuilderTest.cs | 257 + ...ResiliencePipelineBuilderExtensionsTest.cs | 96 + .../ResiliencePipelineFactoryOptionsTest.cs | 17 + ...encePipelineFactoryOptionsValidatorTest.cs | 36 + .../Internal/ResiliencePipelineFactoryTest.cs | 157 + ...encePipelineProviderTest.DynamicChanges.cs | 370 + .../ResiliencePipelineProviderTest.cs | 74 + .../ResilienceFakeClockTestsCollection.cs | 11 + ...PipelineBuilderExtensionsTest.BulkheadT.cs | 105 + ...neBuilderExtensionsTest.CircuitBreakerT.cs | 105 + ...PipelineBuilderExtensionsTest.FallbackT.cs | 108 + ...ePipelineBuilderExtensionsTest.HedgingT.cs | 120 + ...ncePipelineBuilderExtensionsTest.RetryT.cs | 104 + ...ePipelineBuilderExtensionsTest.TimeoutT.cs | 105 + ...ResiliencePipelineBuilderExtensionsTest.cs | 23 + .../ResiliencePipelineBuilderTest.cs | 21 + .../ServiceCollectionExtensionsTests.cs | 85 + .../configs/appsettings.json | 80 + .../configs/optionsOnChangeTestNew.json | 12 + .../configs/optionsOnChangeTestOriginal.json | 12 + .../Enrichment/EnricherExtensionsTests.cs | 81 + .../TestLogEnrichmentPropertyBag.cs | 51 + .../TestMetricEnrichmentPropertyBag.cs | 51 + .../Http/AbstractionTests.cs | 42 + .../Latency/CheckpointTest.cs | 48 + .../Latency/LatencyDataTest.cs | 87 + .../Latency/LatencyRegistryExtensionsTest.cs | 76 + .../Latency/MeasureTest.cs | 48 + .../Latency/NoopLatencyContextTest.cs | 78 + .../Latency/TagTest.cs | 29 + .../Logging/LogMethodAttributeTests.cs | 78 + .../Logging/LogMethodHelperTests.cs | 139 + .../Logging/LogPropertiesAttributeTests.cs | 62 + .../Metering/MetricAttributeTests.cs | 162 + ...nsions.Telemetry.Abstractions.Tests.csproj | 14 + .../Latency/LatencyConsoleExporterTests.cs | 223 + .../Latency/LatencyConsoleExtensionsTests.cs | 77 + .../Latency/LatencyConsoleOptionsTests.cs | 26 + .../Logging/AcceptanceTest.cs | 253 + .../Logging/Helpers/TestException.cs | 22 + .../Logging/Internal/ColorSetTests.cs | 69 + .../Internal/LogFormatterOptionsTests.cs | 77 + .../Logging/Internal/LogFormatterTests.cs | 526 ++ .../Internal/LogLevelExtensionsTests.cs | 75 + .../Logging/Internal/TestLogEnricher.cs | 15 + .../Internal/TextWriterExtensionsTests.cs | 90 + .../Logging/LoggingConsoleExporterTests.cs | 20 + .../Logging/LoggingConsoleExtensionsTests.cs | 142 + .../Logging/LoggingConsoleOptionsTests.cs | 45 + ....Extensions.Telemetry.Console.Tests.csproj | 21 + .../appsettings.json | 12 + .../Logging/FakeLogCollectorOptionsTests.cs | 20 + .../Logging/FakeLogCollectorTests.cs | 183 + .../Logging/FakeLoggerExtensionsTests.cs | 88 + .../Logging/FakeLoggerProviderTests.cs | 52 + .../Logging/FakeLoggerTests.cs | 278 + .../Logging/TestLog.cs | 13 + .../Metering/MetricCollectorTests.Counter.cs | 210 + .../MetricCollectorTests.Histogram.cs | 209 + .../MetricCollectorTests.ObservableCounter.cs | 298 + .../MetricCollectorTests.ObservableGauge.cs | 297 + ...cCollectorTests.ObservableUpdownCounter.cs | 297 + .../MetricCollectorTests.UpDownCounter.cs | 215 + .../Metering/MetricCollectorTests.cs | 589 ++ .../Metering/MetricValueTests.cs | 20 + .../Metering/MetricValuesHolderTests.cs | 215 + ....Extensions.Telemetry.Testing.Tests.csproj | 21 + .../Internals/TestExtensions.cs | 18 + .../ProcessEnricherDimensionsTests.cs | 31 + .../ProcessEnricherExtensionsTests.cs | 92 + .../ProcessLogEnricherTests.cs | 91 + .../TestLogEnrichmentPropertyBag.cs | 52 + .../Internals/TestExtensions.cs | 18 + .../Internals/TestLogEnrichmentPropertyBag.cs | 52 + .../TestMetricEnrichmentPropertyBag.cs | 52 + .../ServiceEnricherDimensionsTests.cs | 33 + .../ServiceEnricherExtensionsTests.cs | 279 + .../ServiceEnricherOptionsTests.cs | 40 + .../ServiceLogEnricherTests.cs | 118 + .../ServiceMetricEnricherTests.cs | 130 + .../ServiceTraceEnricherTests.cs | 98 + .../Latency/Internal/CheckpointTrackerTest.cs | 90 + .../Internal/LatencyContextExtensions.cs | 14 + .../Internal/LatencyContextPoolTest.cs | 108 + .../Internal/LatencyContextProviderTest.cs | 121 + .../Internal/LatencyContextRegistrySetTest.cs | 91 + .../Latency/Internal/LatencyContextTest.cs | 330 + .../Internal/LatencyContextTokenIssuerTest.cs | 73 + .../Latency/Internal/MeasureTrackerTest.cs | 125 + .../MockLatencyContextRegistrationOptions.cs | 28 + .../Latency/Internal/RegistryTest.cs | 90 + .../Latency/Internal/TagCollectionTest.cs | 114 + .../Latency/LatencyContextExtensionTest.cs | 131 + .../Logging/Internals/AnotherEnricher.cs | 14 + .../Logging/Internals/EmptyEnricher.cs | 14 + .../Logging/Internals/EmptyStringEnricher.cs | 14 + .../Logging/Internals/FlexibleEnricher.cs | 23 + .../Logging/Internals/Helpers.cs | 87 + .../Internals/PrimitiveValuesEnricher.cs | 23 + .../Logging/Internals/SimpleEnricher.cs | 14 + .../Logging/Internals/TestExceptionThrower.cs | 150 + .../Logging/Internals/TestExporter.cs | 34 + .../Logging/Internals/TestProcessor.cs | 18 + .../Logging/Log/LoggingOptionsTests.cs | 35 + .../Logging/LogEnrichmentTests.cs | 254 + .../Logging/LoggerTests.cs | 774 ++ .../Auxiliary/TestEventSource.cs | 22 + .../Auxiliary/TestUtils.cs | 24 + .../EventCountersExtensionsTest.cs | 201 + .../EventCountersListenerTest.cs | 634 ++ .../EventCountersValidatorTest.cs | 41 + .../Metering/Internal/TestEnricher.cs | 14 + .../Metering/Internal/TestExporter.cs | 18 + .../Metering/Internal/TestExtensions.cs | 65 + .../Metering/OTelMeteringExtensionsTests.cs | 414 + ...icrosoft.Extensions.Telemetry.Tests.csproj | 37 + .../HttpHeadersRedactorTests.cs | 52 + .../Telemetry.Internal/HttpParserTests.cs | 500 ++ .../HttpRouteFormatterTests.cs | 502 ++ .../MetricEnrichmentPropertyBagTest.cs | 145 + .../SelfDiagnosticsConfigParserTest.cs | 182 + .../SelfDiagnosticsConfigRefresherTest.cs | 418 + .../SelfDiagnosticsEventListenerTest.cs | 340 + .../SelfDiagnosticsEventSourceTests.cs | 26 + .../SelfDiagnosticsTests.cs | 34 + .../Telemetry.Internal.Test.xunit.runner.json | 7 + .../TelemetryCommonExtensionsTests.cs | 141 + .../Telemetry.Internal/TestEventListener.cs | 22 + .../Telemetry.Internal/TestEventSource.cs | 54 + .../SamplingExtensionsTests.cs | 240 + .../SamplingOptionsCustomValidatorTests.cs | 194 + .../Tracing/EnricherExtensionsTests.cs | 319 + .../appsettings.json | 33 + .../FakeTimeProviderTests.cs | 289 + .../FakeTimeProviderTimerTests.cs | 274 + ...tensions.TimeProvider.Testing.Tests.csproj | 10 + .../BatchItemTests.cs | 35 + .../DatabaseOptionsTests.cs | 74 + .../ExceptionsTests.cs | 102 + .../PatchOperationTest.cs | 33 + .../QueryTest.cs | 20 + .../RegionalDatabaseOptionsTests.cs | 27 + .../RequestOptionsTests.cs | 48 + ...Cloud.DatabaseDb.Abstractions.Tests.csproj | 10 + .../TableOptionsTests.cs | 55 + .../Data/Consumers/DerivedConsumer.cs | 19 + .../Data/Consumers/OverridenConsumer.cs | 53 + .../Data/Consumers/SampleConsumer.cs | 44 + .../Data/Consumers/SingleMessageConsumer.cs | 53 + .../Data/Delegates/SampleWriterDelegate.cs | 22 + .../Data/Middlewares/SampleMiddleware.cs | 23 + .../Data/Sources/AnotherSource.cs | 53 + .../Data/Sources/SampleSource.cs | 53 + ...SerializedMessagePayloadExtensionsTests.cs | 75 + .../ServiceCollectionExtensionsTests.cs | 257 + .../BaseMessageConsumerTests.cs | 201 + .../Startup/ConsumerBackgroundServiceTests.cs | 49 + .../Startup/PipelineDelegateFactoryTests.cs | 67 + ....Cloud.Messaging.Abstractions.Tests.csproj | 16 + .../ExclusiveRangeAttributeTests.cs | 143 + .../Data.Validation/LengthAttributeTests.cs | 418 + .../Data.Validation/TimeSpanAttributeTests.cs | 255 + test/Shared/Debugger/DebuggerTest.cs | 66 + .../EmptyCollectionExtensionsTests.cs | 113 + .../EmptyReadOnlyListTests.cs | 66 + .../EmptyReadonlyDictionaryTests.cs | 64 + test/Shared/EmptyCollections/EmptyTests.cs | 18 + test/Shared/Memoization/MemoizeTests.cs | 343 + .../NumericExtensionsTests.cs | 32 + test/Shared/Pools/PoolTests.cs | 316 + test/Shared/Pools/TestResources/ITestClass.cs | 10 + test/Shared/Pools/TestResources/TestClass.cs | 25 + .../Pools/TestResources/TestDependency.cs | 13 + test/Shared/RentedSpan/RentedSpanTest.cs | 33 + test/Shared/Shared.Tests.csproj | 26 + .../Text.Formatting/CompositeFormatTests.cs | 876 ++ test/Shared/Text.Formatting/MakerTests.cs | 532 ++ test/Shared/Throw/DoubleTests.cs | 177 + test/Shared/Throw/IntegerTests.cs | 332 + test/Shared/Throw/LongTests.cs | 332 + test/Shared/Throw/ThrowTest.cs | 424 + test/TestUtilities/TestUtilities.csproj | 22 + .../XUnit/ConditionalFactAttribute.cs | 16 + .../XUnit/ConditionalFactDiscoverer.cs | 29 + .../XUnit/ConditionalTheoryAttribute.cs | 16 + .../XUnit/ConditionalTheoryDiscoverer.cs | 84 + test/TestUtilities/XUnit/ITestCondition.cs | 13 + .../XUnit/OSSkipConditionAttribute.cs | 69 + test/TestUtilities/XUnit/OperatingSystems.cs | 16 + test/TestUtilities/XUnit/SkippedTestCase.cs | 52 + .../XUnit/TestMethodExtensions.cs | 35 + .../WORKAROUND_SkippedDataRowTestCase.cs | 88 + .../AcceptanceTest.cs | 380 + .../AutoActivationExtensionsTests.cs | 82 + .../AutoActivationHostedServiceTests.cs | 18 + ...dencyInjection.AutoActivation.Tests.csproj | 13 + .../Fakes/AnotherFakeService.cs | 14 + .../Fakes/DifferentPocoClass.cs | 8 + .../Fakes/FactoryService.cs | 17 + .../Fakes/FakeOneMultipleService.cs | 14 + .../Fakes/FakeOpenGenericService.cs | 14 + .../Fakes/FakeService.cs | 14 + .../Fakes/IFactoryService.cs | 9 + .../Fakes/IFakeMultipleService.cs | 8 + .../Fakes/IFakeOpenGenericService.cs | 8 + .../Fakes/IFakeService.cs | 8 + .../Fakes/PocoClass.cs | 8 + .../Helpers/AnotherFakeServiceCounter.cs | 9 + .../Helpers/IAnotherFakeServiceCounter.cs | 9 + .../Helpers/IFactoryServiceCounter.cs | 9 + .../Helpers/IFakeMultipleCounter.cs | 9 + .../Helpers/IFakeOpenGenericCounter.cs | 9 + .../Helpers/IFakeServiceCounter.cs | 9 + .../Helpers/InstanceCreatingCounter.cs | 9 + ...endencyInjection.NamedService.Tests.csproj | 15 + .../ResolutionTests.cs | 100 + .../DependencyInjection.Pools.Tests.csproj | 16 + .../DependencyInjectionExtensionsTest.cs | 117 + .../TestResources/ITestClass.cs | 10 + .../TestResources/TestClass.cs | 25 + .../TestResources/TestDependency.cs | 13 + test/ToBeMoved/Directory.Build.props | 8 + ...Hosting.StartupInitialization.Tests.csproj | 20 + .../Internal/Database.cs | 26 + .../Internal/DatabaseInitializer.cs | 26 + .../Internal/DummyHostedService.cs | 33 + .../Internal/TestResources.cs | 24 + .../StartupInitializationAcceptanceTest.cs | 263 + .../StartupInitializationExtensionsTest.cs | 40 + .../HttpClient.SocketHandling.Tests.csproj | 19 + .../HttpClientSocketHandlingExtensionsTest.cs | 268 + .../Utils/HttpMessageHandlerBuilderHelpers.cs | 50 + test/ToBeRemoved/Directory.Build.props | 8 + .../AcceptanceTest.cs | 480 ++ .../Helpers/AnnotatedOptions.cs | 25 + .../Helpers/AnotherNestedOptionsValidator.cs | 25 + .../Helpers/ComplexOptions.cs | 42 + .../Helpers/DepValidatorAttribute.cs | 29 + .../Helpers/FailingNestedOptionsValidator.cs | 14 + .../Helpers/FromAttribute.cs | 15 + .../Helpers/HostStartStopExtension.cs | 22 + .../Helpers/NestedOptions.cs | 9 + .../Helpers/NestedOptionsValidator.cs | 25 + .../Helpers/OptionsValidationModels.cs | 74 + .../ThreeFailuresMultiErrorValidator.cs | 26 + .../ZeroFailuresMultiErrorValidator.cs | 12 + .../MultipleMessageValidatorTest.cs | 136 + .../Options.ValidateOnStart.Tests.csproj | 20 + .../OptionsBuilderExtensionsTests.cs | 73 + .../OptionsValidatorExtensionsTest.cs | 298 + .../ValidationHostedServiceTests.cs | 126 + testEnvironments.json | 10 + 2279 files changed, 208138 insertions(+) create mode 100644 .config/dotnet-tools.json create mode 100644 .devcontainer/devcontainer.json create mode 100755 .devcontainer/scripts/onCreateCommand.sh create mode 100755 .devcontainer/scripts/postCreateCommand.sh create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/01_bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/02_api_proposal.yml create mode 100644 .github/ISSUE_TEMPLATE/03_blank_issue.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .gitignore create mode 100644 .spelling create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Directory.Build.props create mode 100644 Directory.Build.targets create mode 100644 Directory.Packages.props create mode 100644 LICENSE create mode 100644 NuGet.config create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 azure-pipelines.yml create mode 100644 bench/.editorconfig create mode 100644 bench/Directory.Build.props create mode 100644 bench/Directory.Build.targets create mode 100644 bench/Generators/Microsoft.Gen.EnumStrings.PerformanceTests/EnumStrings.cs create mode 100644 bench/Generators/Microsoft.Gen.EnumStrings.PerformanceTests/Microsoft.Gen.EnumStrings.PerformanceTests.csproj create mode 100644 bench/Generators/Microsoft.Gen.EnumStrings.PerformanceTests/Program.cs create mode 100644 bench/Generators/Microsoft.Gen.EnumStrings.PerformanceTests/README.md create mode 100644 bench/Generators/Microsoft.Gen.Logging.PerformanceTests/Log.cs create mode 100644 bench/Generators/Microsoft.Gen.Logging.PerformanceTests/LogMethod.cs create mode 100644 bench/Generators/Microsoft.Gen.Logging.PerformanceTests/Microsoft.Gen.Logging.PerformanceTests.csproj create mode 100644 bench/Generators/Microsoft.Gen.Logging.PerformanceTests/MockLogger.cs create mode 100644 bench/Generators/Microsoft.Gen.Logging.PerformanceTests/Program.cs create mode 100644 bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/Constants.cs create mode 100644 bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/Microsoft.AspNetCore.Telemetry.PerformanceTests.csproj create mode 100644 bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/Program.cs create mode 100644 bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/RedactionBenchmark.cs create mode 100644 bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/RouteSegment.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/Benchmark.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/FaultInjectionRequestBenchmarks.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/HttpClientFactory.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/Microsoft.Extensions.Http.Resilience.PerformanceTests.csproj create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/NoRemoteCallHandler.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/Program.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/BenchEnricher.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Benchmarks/HugeHttpCLientLoggingBenchmark.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Benchmarks/MediumHttpClientLoggingBenchmark.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Benchmarks/SmallHttpClientLoggingBenchmark.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/DropMessageLogger.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/DropMessageLoggerProvider.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/HttpClientFactory.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/HugeBody.txt create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/MediumBody.txt create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Microsoft.Extensions.Http.Telemetry.PerformanceTests.csproj create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/NoRemoteCallHandler.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/NoRemoteCallNotSeekableHandler.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/NotSeekableStream.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Program.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/RedactionProcessorBench.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/SmallBody.txt create mode 100644 bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Hedging.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Internals/HedgingUtilities.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Microsoft.Extensions.Resilience.PerformanceTests.csproj create mode 100644 bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/PipelineProvider.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Pipelines.cs create mode 100644 bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Program.cs create mode 100644 build.cmd create mode 100755 build.sh create mode 100644 docs/building.md create mode 100644 eng/AfterSolutionBuild.targets create mode 100644 eng/Build.props create mode 100644 eng/CodeCoverage.config create mode 100644 eng/Common.runsettings create mode 100644 eng/CredScanSuppressions.json create mode 100644 eng/MSBuild/Analyzers.props create mode 100644 eng/MSBuild/Analyzers.targets create mode 100644 eng/MSBuild/Generators.props create mode 100644 eng/MSBuild/Generators.targets create mode 100644 eng/MSBuild/LegacySupport.props create mode 100644 eng/MSBuild/LegacySupport.targets create mode 100644 eng/MSBuild/MultiTargetRoslynComponent.targets.template create mode 100644 eng/MSBuild/Packaging.props create mode 100644 eng/MSBuild/Packaging.targets create mode 100644 eng/MSBuild/ProjectStaging.targets create mode 100644 eng/MSBuild/Shared.props create mode 100644 eng/MSBuild/Shared.targets create mode 100644 eng/MSSharedLibSN2048.snk create mode 100644 eng/PSScriptAnalyzerSettings.psd1 create mode 100644 eng/Packages/BuildOnly.props create mode 100644 eng/Packages/General-latest.props create mode 100644 eng/Packages/General-net462.props create mode 100644 eng/Packages/General-net6.0.props create mode 100644 eng/Packages/General-netcoreapp3.1.props create mode 100644 eng/Packages/General.props create mode 100644 eng/Packages/Packages.sidepush.config create mode 100644 eng/Packages/Packages.sign3rdparty.config create mode 100644 eng/Packages/TestOnly-latest.props create mode 100644 eng/Packages/TestOnly-net462.props create mode 100644 eng/Packages/TestOnly-net6.0.props create mode 100644 eng/Packages/TestOnly-netcoreapp3.1.props create mode 100644 eng/Packages/TestOnly.props create mode 100644 eng/Publishing.props create mode 100644 eng/Stylecop.json create mode 100644 eng/Version.Details.xml create mode 100644 eng/Versions.props create mode 100644 eng/_._ create mode 100644 eng/build.proj create mode 100644 eng/build.ps1 create mode 100755 eng/build.sh create mode 100644 eng/common/BuildConfiguration/build-configuration.json create mode 100644 eng/common/CIBuild.cmd create mode 100644 eng/common/PSScriptAnalyzerSettings.psd1 create mode 100644 eng/common/README.md create mode 100644 eng/common/SetupNugetSources.ps1 create mode 100755 eng/common/SetupNugetSources.sh create mode 100644 eng/common/build.ps1 create mode 100755 eng/common/build.sh create mode 100755 eng/common/cibuild.sh create mode 100644 eng/common/cross/arm/sources.list.bionic create mode 100644 eng/common/cross/arm/sources.list.focal create mode 100644 eng/common/cross/arm/sources.list.jammy create mode 100644 eng/common/cross/arm/sources.list.jessie create mode 100644 eng/common/cross/arm/sources.list.xenial create mode 100644 eng/common/cross/arm/sources.list.zesty create mode 100644 eng/common/cross/arm/tizen/tizen.patch create mode 100644 eng/common/cross/arm64/sources.list.bionic create mode 100644 eng/common/cross/arm64/sources.list.buster create mode 100644 eng/common/cross/arm64/sources.list.focal create mode 100644 eng/common/cross/arm64/sources.list.jammy create mode 100644 eng/common/cross/arm64/sources.list.stretch create mode 100644 eng/common/cross/arm64/sources.list.xenial create mode 100644 eng/common/cross/arm64/sources.list.zesty create mode 100644 eng/common/cross/arm64/tizen/tizen.patch create mode 100644 eng/common/cross/armel/armel.jessie.patch create mode 100644 eng/common/cross/armel/sources.list.jessie create mode 100644 eng/common/cross/armel/tizen/tizen.patch create mode 100644 eng/common/cross/armv6/sources.list.buster create mode 100755 eng/common/cross/build-android-rootfs.sh create mode 100755 eng/common/cross/build-rootfs.sh create mode 100644 eng/common/cross/ppc64le/sources.list.bionic create mode 100644 eng/common/cross/riscv64/sources.list.sid create mode 100644 eng/common/cross/s390x/sources.list.bionic create mode 100755 eng/common/cross/tizen-build-rootfs.sh create mode 100755 eng/common/cross/tizen-fetch.sh create mode 100644 eng/common/cross/toolchain.cmake create mode 100644 eng/common/darc-init.ps1 create mode 100755 eng/common/darc-init.sh create mode 100644 eng/common/dotnet-install.cmd create mode 100644 eng/common/dotnet-install.ps1 create mode 100755 eng/common/dotnet-install.sh create mode 100644 eng/common/enable-cross-org-publishing.ps1 create mode 100644 eng/common/generate-locproject.ps1 create mode 100644 eng/common/generate-sbom-prep.ps1 create mode 100755 eng/common/generate-sbom-prep.sh create mode 100644 eng/common/helixpublish.proj create mode 100644 eng/common/init-tools-native.cmd create mode 100644 eng/common/init-tools-native.ps1 create mode 100755 eng/common/init-tools-native.sh create mode 100644 eng/common/internal-feed-operations.ps1 create mode 100755 eng/common/internal-feed-operations.sh create mode 100644 eng/common/internal/Directory.Build.props create mode 100644 eng/common/internal/NuGet.config create mode 100644 eng/common/internal/Tools.csproj create mode 100644 eng/common/loc/P22DotNetHtmlLocalization.lss create mode 100644 eng/common/msbuild.ps1 create mode 100755 eng/common/msbuild.sh create mode 100644 eng/common/native/CommonLibrary.psm1 create mode 100755 eng/common/native/common-library.sh create mode 100755 eng/common/native/init-compiler.sh create mode 100755 eng/common/native/install-cmake-test.sh create mode 100755 eng/common/native/install-cmake.sh create mode 100644 eng/common/native/install-tool.ps1 create mode 100644 eng/common/pipeline-logging-functions.ps1 create mode 100755 eng/common/pipeline-logging-functions.sh create mode 100644 eng/common/post-build/add-build-to-channel.ps1 create mode 100644 eng/common/post-build/check-channel-consistency.ps1 create mode 100644 eng/common/post-build/nuget-validation.ps1 create mode 100644 eng/common/post-build/post-build-utils.ps1 create mode 100644 eng/common/post-build/publish-using-darc.ps1 create mode 100644 eng/common/post-build/sourcelink-validation.ps1 create mode 100644 eng/common/post-build/symbols-validation.ps1 create mode 100644 eng/common/post-build/trigger-subscriptions.ps1 create mode 100644 eng/common/retain-build.ps1 create mode 100644 eng/common/sdk-task.ps1 create mode 100644 eng/common/sdl/NuGet.config create mode 100644 eng/common/sdl/configure-sdl-tool.ps1 create mode 100644 eng/common/sdl/execute-all-sdl-tools.ps1 create mode 100644 eng/common/sdl/extract-artifact-archives.ps1 create mode 100644 eng/common/sdl/extract-artifact-packages.ps1 create mode 100644 eng/common/sdl/init-sdl.ps1 create mode 100644 eng/common/sdl/packages.config create mode 100644 eng/common/sdl/run-sdl.ps1 create mode 100644 eng/common/sdl/sdl.ps1 create mode 100644 eng/common/templates/job/execute-sdl.yml create mode 100644 eng/common/templates/job/job.yml create mode 100644 eng/common/templates/job/onelocbuild.yml create mode 100644 eng/common/templates/job/publish-build-assets.yml create mode 100644 eng/common/templates/job/source-build.yml create mode 100644 eng/common/templates/job/source-index-stage1.yml create mode 100644 eng/common/templates/jobs/codeql-build.yml create mode 100644 eng/common/templates/jobs/jobs.yml create mode 100644 eng/common/templates/jobs/source-build.yml create mode 100644 eng/common/templates/post-build/common-variables.yml create mode 100644 eng/common/templates/post-build/post-build.yml create mode 100644 eng/common/templates/post-build/setup-maestro-vars.yml create mode 100644 eng/common/templates/post-build/trigger-subscription.yml create mode 100644 eng/common/templates/steps/add-build-to-channel.yml create mode 100644 eng/common/templates/steps/build-reason.yml create mode 100644 eng/common/templates/steps/component-governance.yml create mode 100644 eng/common/templates/steps/execute-codeql.yml create mode 100644 eng/common/templates/steps/execute-sdl.yml create mode 100644 eng/common/templates/steps/generate-sbom.yml create mode 100644 eng/common/templates/steps/publish-logs.yml create mode 100644 eng/common/templates/steps/retain-build.yml create mode 100644 eng/common/templates/steps/run-on-unix.yml create mode 100644 eng/common/templates/steps/run-on-windows.yml create mode 100644 eng/common/templates/steps/run-script-ifequalelse.yml create mode 100644 eng/common/templates/steps/send-to-helix.yml create mode 100644 eng/common/templates/steps/source-build.yml create mode 100644 eng/common/templates/steps/telemetry-end.yml create mode 100644 eng/common/templates/steps/telemetry-start.yml create mode 100644 eng/common/templates/variables/pool-providers.yml create mode 100644 eng/common/templates/variables/sdl-variables.yml create mode 100644 eng/common/tools.ps1 create mode 100755 eng/common/tools.sh create mode 100644 eng/pipelines/templates/BuildAndTest.yml create mode 100644 eng/pipelines/templates/TestCoverageReport.yml create mode 100644 eng/spellchecking_exclusions.dic create mode 100644 eng/stryker-config.json create mode 100644 eng/xunit.runner.json create mode 100644 global.json create mode 100644 restore.cmd create mode 100755 restore.sh create mode 100644 scripts/Slngen.Tests.ps1 create mode 100644 scripts/Slngen.ps1 create mode 100644 scripts/SlngenReferencing.ps1 create mode 100644 scripts/ValidateProjectCoverage.ps1 create mode 100644 src/.editorconfig create mode 100644 src/Analyzers/Directory.Build.props create mode 100644 src/Analyzers/Directory.Build.targets create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/AsyncCallInsideUsingBlockAnalyzer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/AsyncMethodWithoutCancellation.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/Arrays.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/CallAnalyzer.Handlers.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/CallAnalyzer.Registrar.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/CallAnalyzer.State.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/CallAnalyzer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/EnumStrings.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/Fixers/LegacyLoggingFixer.FixDetails.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/Fixers/LegacyLoggingFixer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/LegacyCollection.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/LegacyLogging.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/NullChecks.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/Split.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/StartsEndsWith.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/StaticTime.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/StringFormat.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/ValueTuple.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CoalesceAnalyzer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/ConditionalAccessAnalyzer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/DataClassificationStaticAnalysisCommon.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/DiagDescriptors.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/FixAllProviders/ISequentialFixer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/FixAllProviders/SequentialFixAllCodeAction.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/FixAllProviders/SequentialFixAllProvider.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/MakeExeTypesInternalAnalyzer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/MakeExeTypesInternalFixer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/OptimizeArraysAnalyzer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Resources.Designer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Resources.resx create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/StringFormatFixer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExcessiveDictionaryLookupAnalyzer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExcessiveDictionaryLookupFixer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExcessiveSetLookupAnalyzer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExcessiveSetLookupFixer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExperimentalApiAnalyzer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingToStringInLoggersAnalyzer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/CompilationExtensions.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/OperationExtensions.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/SymbolExtensions.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/SyntaxEditorExtensions.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/SyntaxNodeExtensions.cs create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Directory.Build.props create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Microsoft.Extensions.ExtraAnalyzers.Roslyn3.8/Microsoft.Extensions.ExtraAnalyzers.Roslyn3.8.csproj create mode 100644 src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Microsoft.Extensions.ExtraAnalyzers.Roslyn4.0/Microsoft.Extensions.ExtraAnalyzers.Roslyn4.0.csproj create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/ApiLifecycleAnalyzer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/ApiLifecycleFixer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/AssemblyAnalysis.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonArray.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonObject.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonObjectExtensions.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonParseException.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonReader.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonValue.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonValueType.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/ParsingError.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/TextPosition.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/TextScanner.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Assembly.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Field.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Method.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Prop.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Stage.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/TypeDef.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/ModelLoader.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Utils.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/CallAnalyzer.Handlers.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/CallAnalyzer.Registrar.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/CallAnalyzer.State.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/CallAnalyzer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/Throws.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/ToInvariantString.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/DiagDescriptors.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Microsoft.Extensions.LocalAnalyzers.csproj create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Resources.Designer.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Resources.resx create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/CompilationExtensions.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/OperationExtensions.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/SymbolExtensions.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/SyntaxEditorExtensions.cs create mode 100644 src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/SyntaxNodeExtensions.cs create mode 100644 src/Directory.Build.props create mode 100644 src/Directory.Build.targets create mode 100644 src/Generators/Directory.Build.props create mode 100644 src/Generators/Directory.Build.targets create mode 100644 src/Generators/Microsoft.Gen.AutoClient/Common/DiagDescriptors.cs create mode 100644 src/Generators/Microsoft.Gen.AutoClient/Common/Emitter.cs create mode 100644 src/Generators/Microsoft.Gen.AutoClient/Common/Generator.cs create mode 100644 src/Generators/Microsoft.Gen.AutoClient/Common/Model/BodyContentTypeParam.cs create mode 100644 src/Generators/Microsoft.Gen.AutoClient/Common/Model/BodyContentTypeParamExtensions.cs create mode 100644 src/Generators/Microsoft.Gen.AutoClient/Common/Model/RestApiMethod.cs create mode 100644 src/Generators/Microsoft.Gen.AutoClient/Common/Model/RestApiMethodParameter.cs create mode 100644 src/Generators/Microsoft.Gen.AutoClient/Common/Model/RestApiType.cs create mode 100644 src/Generators/Microsoft.Gen.AutoClient/Common/Parser.cs create mode 100644 src/Generators/Microsoft.Gen.AutoClient/Common/Resources.Designer.cs create mode 100644 src/Generators/Microsoft.Gen.AutoClient/Common/Resources.resx create mode 100644 src/Generators/Microsoft.Gen.AutoClient/Common/SymbolHolder.cs create mode 100644 src/Generators/Microsoft.Gen.AutoClient/Common/SymbolLoader.cs create mode 100644 src/Generators/Microsoft.Gen.AutoClient/Directory.Build.props create mode 100644 src/Generators/Microsoft.Gen.AutoClient/Roslyn3.8/Microsoft.Gen.AutoClient.Roslyn3.8.csproj create mode 100644 src/Generators/Microsoft.Gen.AutoClient/Roslyn4.0/Microsoft.Gen.AutoClient.Roslyn4.0.csproj create mode 100644 src/Generators/Microsoft.Gen.ComplianceReports/Common/Emitter.cs create mode 100644 src/Generators/Microsoft.Gen.ComplianceReports/Common/Generator.cs create mode 100644 src/Generators/Microsoft.Gen.ComplianceReports/Common/Model/Classification.cs create mode 100644 src/Generators/Microsoft.Gen.ComplianceReports/Common/Model/ClassifiedItem.cs create mode 100644 src/Generators/Microsoft.Gen.ComplianceReports/Common/Model/ClassifiedLogMethod.cs create mode 100644 src/Generators/Microsoft.Gen.ComplianceReports/Common/Model/ClassifiedType.cs create mode 100644 src/Generators/Microsoft.Gen.ComplianceReports/Common/Parser.cs create mode 100644 src/Generators/Microsoft.Gen.ComplianceReports/Common/SymbolHolder.cs create mode 100644 src/Generators/Microsoft.Gen.ComplianceReports/Common/SymbolLoader.cs create mode 100644 src/Generators/Microsoft.Gen.ComplianceReports/Directory.Build.props create mode 100644 src/Generators/Microsoft.Gen.ComplianceReports/Roslyn3.8/Microsoft.Gen.ComplianceReports.Roslyn3.8.csproj create mode 100644 src/Generators/Microsoft.Gen.ComplianceReports/Roslyn4.0/Microsoft.Gen.ComplianceReports.Roslyn4.0.csproj create mode 100644 src/Generators/Microsoft.Gen.ContextualOptions/Common/ContextReceiver.cs create mode 100644 src/Generators/Microsoft.Gen.ContextualOptions/Common/DiagDescriptors.cs create mode 100644 src/Generators/Microsoft.Gen.ContextualOptions/Common/Emitter.cs create mode 100644 src/Generators/Microsoft.Gen.ContextualOptions/Common/Generator.cs create mode 100644 src/Generators/Microsoft.Gen.ContextualOptions/Common/Model/OptionsContextType.cs create mode 100644 src/Generators/Microsoft.Gen.ContextualOptions/Common/Parser.cs create mode 100644 src/Generators/Microsoft.Gen.ContextualOptions/Common/Resources.Designer.cs create mode 100644 src/Generators/Microsoft.Gen.ContextualOptions/Common/Resources.resx create mode 100644 src/Generators/Microsoft.Gen.ContextualOptions/Common/SymbolHolder.cs create mode 100644 src/Generators/Microsoft.Gen.ContextualOptions/Common/SymbolLoader.cs create mode 100644 src/Generators/Microsoft.Gen.ContextualOptions/Directory.Build.props create mode 100644 src/Generators/Microsoft.Gen.ContextualOptions/Roslyn3.8/Microsoft.Gen.ContextualOptions.Roslyn3.8.csproj create mode 100644 src/Generators/Microsoft.Gen.ContextualOptions/Roslyn4.0/Microsoft.Gen.ContextualOptions.Roslyn4.0.csproj create mode 100644 src/Generators/Microsoft.Gen.EnumStrings/Common/DiagDescriptors.cs create mode 100644 src/Generators/Microsoft.Gen.EnumStrings/Common/Emitter.cs create mode 100644 src/Generators/Microsoft.Gen.EnumStrings/Common/Generator.cs create mode 100644 src/Generators/Microsoft.Gen.EnumStrings/Common/Model/ToStringMethod.cs create mode 100644 src/Generators/Microsoft.Gen.EnumStrings/Common/Parser.cs create mode 100644 src/Generators/Microsoft.Gen.EnumStrings/Common/Resources.Designer.cs create mode 100644 src/Generators/Microsoft.Gen.EnumStrings/Common/Resources.resx create mode 100644 src/Generators/Microsoft.Gen.EnumStrings/Common/SymbolHolder.cs create mode 100644 src/Generators/Microsoft.Gen.EnumStrings/Common/SymbolLoader.cs create mode 100644 src/Generators/Microsoft.Gen.EnumStrings/Directory.Build.props create mode 100644 src/Generators/Microsoft.Gen.EnumStrings/Roslyn3.8/Microsoft.Gen.EnumStrings.Roslyn3.8.csproj create mode 100644 src/Generators/Microsoft.Gen.EnumStrings/Roslyn4.0/Microsoft.Gen.EnumStrings.Roslyn4.0.csproj create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Emission/Emitter.Method.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Emission/Emitter.Utils.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Emission/Emitter.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Emission/StringBuilderPool.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Generator.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingMethod.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingMethodParameter.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingMethodParameterExtensions.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingProperty.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingPropertyProvider.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingType.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Parsing/AttributeProcessors.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Parsing/DiagDescriptors.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Parsing/LogParserUtilities.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Parsing/LogPropertiesProcessingResult.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Parsing/LogPropertiesProviderValidator.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Parsing/MultipleDataClassesAppliedException.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Parsing/Parser.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Parsing/PropertyHiddenException.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Parsing/Resources.Designer.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Parsing/Resources.resx create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Parsing/SymbolHolder.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Parsing/SymbolLoader.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Parsing/TemplateExtractor.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Common/Parsing/TransitiveTypeCycleException.cs create mode 100644 src/Generators/Microsoft.Gen.Logging/Directory.Build.props create mode 100644 src/Generators/Microsoft.Gen.Logging/Roslyn3.8/Microsoft.Gen.Logging.Roslyn3.8.csproj create mode 100644 src/Generators/Microsoft.Gen.Logging/Roslyn4.0/Microsoft.Gen.Logging.Roslyn4.0.csproj create mode 100644 src/Generators/Microsoft.Gen.Metering/Common/DiagDescriptors.cs create mode 100644 src/Generators/Microsoft.Gen.Metering/Common/Emitter.cs create mode 100644 src/Generators/Microsoft.Gen.Metering/Common/Generator.cs create mode 100644 src/Generators/Microsoft.Gen.Metering/Common/MetricFactoryEmitter.cs create mode 100644 src/Generators/Microsoft.Gen.Metering/Common/Model/InstrumentKind.cs create mode 100644 src/Generators/Microsoft.Gen.Metering/Common/Model/MetricMethod.cs create mode 100644 src/Generators/Microsoft.Gen.Metering/Common/Model/MetricParameter.cs create mode 100644 src/Generators/Microsoft.Gen.Metering/Common/Model/MetricType.cs create mode 100644 src/Generators/Microsoft.Gen.Metering/Common/Model/StrongTypeConfig.cs create mode 100644 src/Generators/Microsoft.Gen.Metering/Common/Model/StrongTypeMetricObjectType.cs create mode 100644 src/Generators/Microsoft.Gen.Metering/Common/Parser.cs create mode 100644 src/Generators/Microsoft.Gen.Metering/Common/Resources.Designer.cs create mode 100644 src/Generators/Microsoft.Gen.Metering/Common/Resources.resx create mode 100644 src/Generators/Microsoft.Gen.Metering/Common/SymbolHolder.cs create mode 100644 src/Generators/Microsoft.Gen.Metering/Common/SymbolLoader.cs create mode 100644 src/Generators/Microsoft.Gen.Metering/Directory.Build.props create mode 100644 src/Generators/Microsoft.Gen.Metering/Roslyn3.8/Microsoft.Gen.Metering.Roslyn3.8.csproj create mode 100644 src/Generators/Microsoft.Gen.Metering/Roslyn4.0/Microsoft.Gen.Metering.Roslyn4.0.csproj create mode 100644 src/Generators/Microsoft.Gen.MeteringReports/Common/MetricDefinitionEmitter.cs create mode 100644 src/Generators/Microsoft.Gen.MeteringReports/Common/MetricDefinitionGenerator.cs create mode 100644 src/Generators/Microsoft.Gen.MeteringReports/Common/ReportedMetricClass.cs create mode 100644 src/Generators/Microsoft.Gen.MeteringReports/Directory.Build.props create mode 100644 src/Generators/Microsoft.Gen.MeteringReports/Roslyn3.8/Microsoft.Gen.MeteringReports.Roslyn3.8.csproj create mode 100644 src/Generators/Microsoft.Gen.MeteringReports/Roslyn4.0/Microsoft.Gen.MeteringReports.Roslyn4.0.csproj create mode 100644 src/Generators/Microsoft.Gen.OptionsValidation/Common/DiagDescriptors.cs create mode 100644 src/Generators/Microsoft.Gen.OptionsValidation/Common/Emitter.cs create mode 100644 src/Generators/Microsoft.Gen.OptionsValidation/Common/Generator.cs create mode 100644 src/Generators/Microsoft.Gen.OptionsValidation/Common/Model/ValidatedMember.cs create mode 100644 src/Generators/Microsoft.Gen.OptionsValidation/Common/Model/ValidatedModel.cs create mode 100644 src/Generators/Microsoft.Gen.OptionsValidation/Common/Model/ValidationAttributeInfo.cs create mode 100644 src/Generators/Microsoft.Gen.OptionsValidation/Common/Model/ValidatorType.cs create mode 100644 src/Generators/Microsoft.Gen.OptionsValidation/Common/Parser.cs create mode 100644 src/Generators/Microsoft.Gen.OptionsValidation/Common/Resources.Designer.cs create mode 100644 src/Generators/Microsoft.Gen.OptionsValidation/Common/Resources.resx create mode 100644 src/Generators/Microsoft.Gen.OptionsValidation/Common/SymbolHolder.cs create mode 100644 src/Generators/Microsoft.Gen.OptionsValidation/Common/SymbolLoader.cs create mode 100644 src/Generators/Microsoft.Gen.OptionsValidation/Directory.Build.props create mode 100644 src/Generators/Microsoft.Gen.OptionsValidation/Roslyn3.8/Microsoft.Gen.OptionsValidation.Roslyn3.8.csproj create mode 100644 src/Generators/Microsoft.Gen.OptionsValidation/Roslyn4.0/Microsoft.Gen.OptionsValidation.Roslyn4.0.csproj create mode 100644 src/Generators/Shared/ClassDeclarationSyntaxReceiver.cs create mode 100644 src/Generators/Shared/DiagDescriptorsBase.cs create mode 100644 src/Generators/Shared/EmitterBase.cs create mode 100644 src/Generators/Shared/GeneratorUtilities.cs create mode 100644 src/Generators/Shared/ParserUtilities.cs create mode 100644 src/Generators/Shared/StringBuilderPool.cs create mode 100644 src/Generators/Shared/SymbolHelpers.cs create mode 100644 src/Generators/Shared/TypeDeclarationSyntaxReceiver.cs create mode 100644 src/LegacySupport/BitOperations/BitOperations.cs create mode 100644 src/LegacySupport/BitOperations/README.md create mode 100644 src/LegacySupport/CallerAttributes/CallerArgumentExpressionAttribute.cs create mode 100644 src/LegacySupport/CallerAttributes/CallerFilePathAttribute.cs create mode 100644 src/LegacySupport/CallerAttributes/CallerLineNumberAttribute.cs create mode 100644 src/LegacySupport/CallerAttributes/CallerMemberNameAttribute.cs create mode 100644 src/LegacySupport/CallerAttributes/README.md create mode 100644 src/LegacySupport/DiagnosticAttributes/NullableAttributes.cs create mode 100644 src/LegacySupport/DiagnosticAttributes/README.md create mode 100644 src/LegacySupport/DictionaryExtensions/DictionaryExtensions.cs create mode 100644 src/LegacySupport/DictionaryExtensions/README.md create mode 100644 src/LegacySupport/ExperimentalAttribute/ExperimentalAttribute.cs create mode 100644 src/LegacySupport/GetOrAdd/GetOrAddExtensions.cs create mode 100644 src/LegacySupport/GetOrAdd/README.md create mode 100644 src/LegacySupport/IsExternalInit/IsExternalInit.cs create mode 100644 src/LegacySupport/IsExternalInit/README.md create mode 100644 src/LegacySupport/README.md create mode 100644 src/LegacySupport/SkipLocalsInitAttribute/README.md create mode 100644 src/LegacySupport/SkipLocalsInitAttribute/SkipLocalsInitAttribute.cs create mode 100644 src/LegacySupport/StringBuilderExtensions/README.md create mode 100644 src/LegacySupport/StringBuilderExtensions/StringBuilderExtensions.cs create mode 100644 src/LegacySupport/StringHash/README.md create mode 100644 src/LegacySupport/StringHash/StringHash.cs create mode 100644 src/LegacySupport/StringSyntaxAttribute/README.md create mode 100644 src/LegacySupport/StringSyntaxAttribute/StringSyntaxAttribute.cs create mode 100644 src/LegacySupport/TaskWaitAsync/README.md create mode 100644 src/LegacySupport/TaskWaitAsync/TaskExtensions.cs create mode 100644 src/LegacySupport/TrimAttributes/DynamicDependencyAttribute.cs create mode 100644 src/LegacySupport/TrimAttributes/DynamicallyAccessedMemberTypes.cs create mode 100644 src/LegacySupport/TrimAttributes/DynamicallyAccessedMembersAttribute.cs create mode 100644 src/LegacySupport/TrimAttributes/README.md create mode 100644 src/LegacySupport/TrimAttributes/RequiresAssemblyFilesAttribute.cs create mode 100644 src/LegacySupport/TrimAttributes/RequiresUnreferencedCodeAttribute.cs create mode 100644 src/LegacySupport/TrimAttributes/UnconditionalSuppressMessageAttribute.cs create mode 100644 src/LegacySupport/xxH3/README.md create mode 100644 src/LegacySupport/xxH3/XxHash3.cs create mode 100644 src/LegacySupport/xxH3/XxHash32.State.cs create mode 100644 src/LegacySupport/xxH3/XxHash64.State.cs create mode 100644 src/Libraries/Directory.Build.props create mode 100644 src/Libraries/Microsoft.AspNetCore.AsyncState/AsyncContextHttpContext.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.AsyncState/AsyncStateHttpContextExtensions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.AsyncState/Microsoft.AspNetCore.AsyncState.csproj create mode 100644 src/Libraries/Microsoft.AspNetCore.AsyncState/TypeWrapper.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/ConnectionTimeoutDelegate.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/ConnectionTimeoutExtensions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/ConnectionTimeoutOptions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/ConnectionTimeoutValidator.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/Microsoft.AspNetCore.ConnectionTimeout.csproj create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/CommonHeaders.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderKey.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParser.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingExtensions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingFeature.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingOptions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingOptionsManualValidator.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderRegistry.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderSetup.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/HostHeaderValue.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/IHeaderRegistry.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/Metric.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/Microsoft.AspNetCore.HeaderParsing.csproj create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/CacheControlHeaderValueParser.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/ContentDispositionHeaderValueParser.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/CookieHeaderValueListParser.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/DateTimeOffsetParser.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/EntityTagHeaderValueListParser.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/HostHeaderValueParser.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/IPAddressListParser.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/MediaTypeHeaderValueListParser.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/MediaTypeHeaderValueParser.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/RangeConditionHeaderValueParser.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/RangeHeaderValueParser.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/StringWithQualityHeaderValueListParser.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/UriParser.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.HeaderParsing/ParsingResult.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/AddServerTimingHeaderMiddleware.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/CapturePipelineEntryMiddleware.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/CapturePipelineEntryStartupFilter.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/CapturePipelineExitMiddleware.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/CaptureResponseTimeMiddleware.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/LatencyContextControlExtensions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/RequestCheckpointConstants.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/RequestCheckpointExtensions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Internal/RequestLatencyTelemetryMiddleware.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Internal/RequestLatencyTelemetryOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/RequestLatencyTelemetryExtensions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/RequestLatencyTelemetryOptions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/HttpLoggingDimensions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/HttpLoggingServiceExtensions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/IHttpLogEnricher.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/IncomingPathLoggingMode.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/HeaderReader.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/HttpLogPropertiesProvider.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/HttpLoggingMiddleware.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/HttpRequestBodyReader.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/IncomingRequestLogRecord.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/Log.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/LoggingOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/MediaTypeSetExtensions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/PipeReaderExtensions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/ResponseInterceptingStream.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/ResponseInterceptingStreamPool.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/LoggingOptions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/HttpMeteringBuilder.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/HttpMeteringExtensions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/IIncomingRequestMetricEnricher.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/Internal/HttpContextExtensions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/Internal/HttpMeteringMiddleware.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/Internal/Metric.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Microsoft.AspNetCore.Telemetry.Middleware.csproj create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry/Enrichment.RequestHeaders/RequestHeadersEnricherExtensions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry/Enrichment.RequestHeaders/RequestHeadersLogEnricher.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry/Enrichment.RequestHeaders/RequestHeadersLogEnricherOptions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry/Enrichment.RequestHeaders/RequestHeadersLogEnricherOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry/Microsoft.AspNetCore.Telemetry.csproj create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry/Telemetry.Internal.Http/HttpUtilityExtensions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry/Telemetry.Internal.Http/IIncomingHttpRouteUtility.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry/Telemetry.Internal.Http/IncomingHttpRouteUtility.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/Constants.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpTraceEnrichmentProcessor.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpTraceEnrichmentProcessor.netfx.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpTracingExtensions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpTracingOptions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpUrlRedactionProcessor.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/IHttpTraceEnricher.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/Internal/ConfigureAspNetCoreInstrumentationOptions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/Internal/HttpTracingOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/Internal/Log.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Testing/Internal/FakeCertificateHttpClientHandler.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Testing/Internal/FakeCertificateOptions.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Testing/Internal/FakeSslCertificateFactory.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Testing/Internal/FakeStartup.cs create mode 100644 src/Libraries/Microsoft.AspNetCore.Testing/Microsoft.AspNetCore.Testing.csproj create mode 100644 src/Libraries/Microsoft.AspNetCore.Testing/ServiceFakesExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadata.cs create mode 100644 src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataSource.cs create mode 100644 src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/Microsoft.Extensions.AmbientMetadata.Application.csproj create mode 100644 src/Libraries/Microsoft.Extensions.AsyncState/AsyncContext.cs create mode 100644 src/Libraries/Microsoft.Extensions.AsyncState/AsyncState.cs create mode 100644 src/Libraries/Microsoft.Extensions.AsyncState/AsyncStateExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.AsyncState/AsyncStateToken.cs create mode 100644 src/Libraries/Microsoft.Extensions.AsyncState/FeaturesPooledPolicy.cs create mode 100644 src/Libraries/Microsoft.Extensions.AsyncState/IAsyncContext.cs create mode 100644 src/Libraries/Microsoft.Extensions.AsyncState/IAsyncLocalContext.cs create mode 100644 src/Libraries/Microsoft.Extensions.AsyncState/IAsyncState.cs create mode 100644 src/Libraries/Microsoft.Extensions.AsyncState/Microsoft.Extensions.AsyncState.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Classification/DataClassification.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Classification/DataClassificationAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Classification/NoDataClassificationAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Classification/UnknownDataClassificationAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Microsoft.Extensions.Compliance.Abstractions.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Redaction/IRedactionBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Redaction/IRedactorProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Redaction/RedactionAbstractionsExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Redaction/Redactor.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Redaction/ErasingRedactor.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Redaction/Microsoft.Extensions.Compliance.Redaction.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Redaction/NullRedactor.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Redaction/NullRedactorProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactionBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactionExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactionExtensions.xxHash.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactorProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactorProviderOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Redaction/XXHash3Redactor.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Redaction/XXHash3RedactorOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Testing/Attributes/PrivateDataAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Testing/Attributes/PublicDataAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactionCollector.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactionExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactor.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactorOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactorOptionsAutoValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactorOptionsCustomValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactorProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Testing/Microsoft.Extensions.Compliance.Testing.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Testing/RedactedData.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Testing/RedactorRequested.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Testing/SimpleClassifications.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Testing/SimpleTaxonomy.cs create mode 100644 src/Libraries/Microsoft.Extensions.Compliance.Testing/SimpleTaxonomyExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Abstractions/ExceptionSummary.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Abstractions/IExceptionSummarizationBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Abstractions/IExceptionSummarizer.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Abstractions/IExceptionSummaryProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Implementation/ExceptionSummarizationBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Implementation/ExceptionSummarizationExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Implementation/ExceptionSummarizer.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Implementation/HttpExceptionSummaryProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Microsoft.Extensions.Diagnostics.ExceptionSummarization.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ApplicationLifecycleHealthCheck.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.ApplicationLifecycle.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.KubernetesPublisher.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.Manual.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.TelemetryPublisher.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/IManualHealthCheck.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/IManualHealthCheckTracker.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/KubernetesHealthCheckPublisher.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/KubernetesHealthCheckPublisherOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheck.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheckService.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheckTracker.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Metric.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Microsoft.Extensions.Diagnostics.HealthChecks.Core.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/TelemetryHealthCheckPublisher.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUsageThresholds.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheck.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheckOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheckOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthChecksExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceUtilizationPublisher.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceUtilizationTracker.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceUtilizationTrackerBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/Calculator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/CircularBuffer.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ISnapshotProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationSnapshot.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationTrackerOptionsManualValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationTrackerOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationTrackerService.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/IFileSystem.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/IOperatingSystem.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/IUserHz.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/IsOperatingSystem.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/LinuxResourceUtilizationProviderOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/LinuxUtilizationParser.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/LinuxUtilizationProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/OSFileSystem.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/UserHz.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxResourceUtilizationCounters.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxResourceUtilizationProviderOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Null/NullResourceUtilizationTrackerService.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Null/NullSnapshotProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceUtilizationTrackerOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Utilization.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/CountersSetup.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/IJobHandle.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/IMemoryInfo.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/IProcessInfo.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/ISystemInfo.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/JobHandleWrapper.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/JobObjectInfo.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/MemoryInfo.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/MemoryStatusEx.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/ProcessInfo.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/ProcessInfoWrapper.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/SYSTEM_INFO.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/SystemInfo.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/MIB_TCPROW.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/MIB_TCPTABLE.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/MIB_TCP_STATE.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/NTSTATUS.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/TcpStateInfo.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/TcpTableInfo.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsContainerSnapshotProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsCounters.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsCountersOptionsCustomValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsCountersOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsPerfCounterConstants.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsPerfCounterPublisher.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsPerfCounters.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsSnapshotProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsCountersOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsUtilizationExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.EnumStrings/EnumStringsAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.EnumStrings/Microsoft.Extensions.EnumStrings.csproj create mode 100644 src/Libraries/Microsoft.Extensions.EnumStrings/buildTransitive/Microsoft.Extensions.EnumStrings.props create mode 100644 src/Libraries/Microsoft.Extensions.EnumStrings/buildTransitive/Microsoft.Extensions.EnumStrings.targets create mode 100644 src/Libraries/Microsoft.Extensions.Hosting.Testing/FakeHost.cs create mode 100644 src/Libraries/Microsoft.Extensions.Hosting.Testing/FakeHostOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Hosting.Testing/HostingFakesExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Hosting.Testing/Internal/FakeConfigurationSource.cs create mode 100644 src/Libraries/Microsoft.Extensions.Hosting.Testing/Internal/FakeHostBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Hosting.Testing/Internal/HostTerminatorService.cs create mode 100644 src/Libraries/Microsoft.Extensions.Hosting.Testing/Microsoft.Extensions.Hosting.Testing.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/AutoClientAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/AutoClientException.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/AutoClientHttpError.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/AutoClientOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/BodyAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/BodyContentType.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/HeaderAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/DeleteAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/GetAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/HeadAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/OptionsAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/PatchAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/PostAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/PutAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/Microsoft.Extensions.Http.AutoClient.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/QueryAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/RequestNameAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/StaticHeaderAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/buildTransitive/Microsoft.Extensions.Http.AutoClient.props create mode 100644 src/Libraries/Microsoft.Extensions.Http.AutoClient/buildTransitive/Microsoft.Extensions.Http.AutoClient.targets create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/HttpClientFaultInjectionExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/HttpFaultInjectionOptionsBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/IHttpClientChaosPolicyFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/FaultInjectionContextMessageHandler.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/FaultInjectionEventMeterDimensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/FaultInjectionTelemetryHandler.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/FaultInjectionWeightAssignmentContextMessageHandler.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/HttpClientChaosPolicyFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/HttpContentOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/HttpContentOptionsRegistry.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/IHttpContentOptionsRegistry.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/Metric.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/PolicyContextExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HedgingEndpointOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HedgingHttpClientBuilderExtensions.Standard.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HedgingHttpClientBuilderExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpClientHedgingResiliencePredicates.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpHedgingPolicyOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpStandardHedgingResilienceOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/IStandardHedgingHandlerBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HedgingConstants.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HedgingContextExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HttpResiliencePipelineBuilderExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/IRandomizer.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Randomizer.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/RequestMessageSnapshotPolicy.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/DefaultRoutingStrategyFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/IPooledRequestRoutingStrategyFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/OrderedGroups/OrderedGroupsRoutingOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/OrderedGroups/OrderedGroupsRoutingStrategy.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/OrderedGroups/OrderedGroupsRoutingStrategyFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/PooledRoutingStrategyFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/RoutingHelper.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/RoutingPolicy.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/RoutingStrategyBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/WeightedGroups/WeightedGroupsRoutingOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/WeightedGroups/WeightedGroupsRoutingStrategy.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/WeightedGroups/WeightedGroupsRoutingStrategyFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/StandardHedgingHandlerBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/StandardHedgingPolicyNames.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Validators/HttpStandardHedgingResilienceOptionsCustomValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Validators/HttpStandardHedgingResilienceOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/Endpoint.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/EndpointGroup.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/IRequestRoutingStrategy.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/IRequestRoutingStrategyFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/IRoutingStrategyBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/OrderedGroupsRoutingOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/RoutingStrategyBuilderExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/WeightedEndpoint.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/WeightedEndpointGroup.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/WeightedGroupSelectionMode.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/WeightedGroupsRoutingOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/StandardHedgingHandlerBuilderExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpBulkheadPolicyOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpCircuitBreakerPolicyOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpClientResilienceGenerators.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpClientResiliencePredicates.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpFallbackPolicyOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRetryPolicyOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpTimeoutPolicyOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/Internal/HttpPolicyFactoryServiceCollectionExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/Internal/RetryAfterHelper.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/FallbackClientHandlerOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.Fallback.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.Resilience.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.StandardResilience.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpResiliencePipelineBuilderExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpStandardResilienceOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpStandardResiliencePipelineBuilderExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpResiliencePipelineBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpStandardResiliencePipelineBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByAuthorityPipelineKeyProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByCustomSelectorPipelineKeyProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ContextExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/DefaultRequestCloner.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/FallbackHelper.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpRequestMessageExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpResiliencePipelineBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpStandardResiliencePipelineBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IHttpRequestMessageSnapshot.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IPipelineKeyProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IRequestClonerInternal.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/PipelineKeyProviderHelper.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/PipelineNameHelper.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ResilienceHandler.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ServiceCollectionExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StandardPolicyNames.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/UriExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/FallbackClientHandlerOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpCircuitBreakerPolicyOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpFallbackPolicyOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpRetryPolicyOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpStandardResilienceOptionsCustomValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpStandardResilienceOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/ValidationHelper.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/PipelineKeySelector.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/HttpClientLatencyTelemetryExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/HttpClientLatencyTelemetryOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpCheckpoints.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpClientLatencyContext.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpClientLatencyLogEnricher.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpLatencyTelemetryHandler.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpRequestLatencyListener.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/HttpClientLoggingDimensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/HttpClientLoggingExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/IHttpClientLogEnricher.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/Constants.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpHeadersReader.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpLoggingHandler.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpRequestBodyReader.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpRequestReader.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpResponseBodyReader.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/IHttpHeadersReader.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/IHttpRequestReader.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/LogPropertyCollectorExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/LogRecord.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/LogRecordPooledObjectPolicy.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/LoggingOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/MediaTypeCollectionExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/LoggingOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/OutgoingPathLoggingMode.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HttpClientMeteringExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HttpMeteringHandler.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/IOutgoingRequestMetricEnricher.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/HttpClientMeteringConstants.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/Metric.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Microsoft.Extensions.Http.Telemetry.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Constants.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientRedactionProcessor.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientTraceEnrichmentProcessor.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientTracingConstants.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientTracingExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientTracingOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/IHttpClientTraceEnricher.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/IHttpPathRedactor.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/ConfigureHttpClientInstrumentationOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/HttpClientTracingOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/HttpPathRedactor.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/HttpTracingEventSource.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/ConfigureContextualOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/ContextualOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/ContextualOptionsFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/ContextualOptionsServiceCollectionExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/IConfigureContextualOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/IContextualOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/IContextualOptionsFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/ILoadContextualOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/INamedContextualOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/IOptionsContext.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/IOptionsContextReceiver.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/IPostConfigureContextualOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/IValidateContextualOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/LoadContextualOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/Microsoft.Extensions.Options.Contextual.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/NullConfigureContextualOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/NullConfigureContextualOptions_1.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/OptionsContextAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/PostConfigureContextualOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/ValidateContextualOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/buildTransitive/Microsoft.Extensions.Options.Contextual.props create mode 100644 src/Libraries/Microsoft.Extensions.Options.Contextual/buildTransitive/Microsoft.Extensions.Options.Contextual.targets create mode 100644 src/Libraries/Microsoft.Extensions.Options.Validation/Microsoft.Extensions.Options.Validation.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Options.Validation/OptionsValidatorAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Validation/ValidateEnumeratedItemsAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Validation/ValidateObjectMembersAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Options.Validation/buildTransitive/Microsoft.Extensions.Options.Validation.props create mode 100644 src/Libraries/Microsoft.Extensions.Options.Validation/buildTransitive/Microsoft.Extensions.Options.Validation.targets create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/FaultInjectionExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/FaultInjectionOptionsBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/IChaosPolicyFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/IFaultInjectionOptionsProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/InjectedFaultException.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/ChaosPolicyFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/ExceptionRegistry.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/FaultInjectionEventMeterDimensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/FaultInjectionOptionsProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/FaultInjectionOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/FaultInjectionTelemetryHandler.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/IExceptionRegistry.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/Metric.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/WeightAssignmentHelper.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/ChaosPolicyOptionsBase.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/ChaosPolicyOptionsGroup.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/ExceptionPolicyOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/FaultInjectionExceptionOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/FaultInjectionOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/FaultPolicyWeightAssignmentsOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/HttpResponseInjectionPolicyOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/LatencyPolicyOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Microsoft.Extensions.Resilience.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/FailureResultContext.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/FallbackScenarioTaskArguments.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/FallbackScenarioTaskProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/FallbackScenarioTaskProviderT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/GlobalSuppressions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/HedgedTaskProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/HedgedTaskProviderT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/AsyncHedgingPolicy.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/AsyncHedgingPolicyT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/AsyncHedgingSyntax.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/EmptyStruct.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/HedgingEngine.WhenAny.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/HedgingEngine.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/HedgingEngineOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/HedgingTaskProviderArguments.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/ContextExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/FailureEventMetricsOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/FailureReasonResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/IPolicyFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/IPolicyMetering.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Metric.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PipelineId.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyEvents.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyFactoryServiceCollectionExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyFactoryUtility.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyMetering.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/RetryPolicyOptionsExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/TelemetryHelper.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/BulkheadPolicyOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/CircuitBreakerPolicyOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/CircuitBreakerPolicyOptionsValidatorT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/HedgingPolicyOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/HedgingPolicyOptionsValidatorT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/RetryPolicyOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/RetryPolicyOptionsValidatorT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/TimeoutPolicyOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/RetryPolicyOptionsCustomValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BackoffType.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BreakActionArguments.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BreakActionArgumentsT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BulkheadPolicyOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BulkheadTaskArguments.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/CircuitBreakerPolicyOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/CircuitBreakerPolicyOptionsT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/FallbackPolicyOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/FallbackPolicyOptionsT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/FallbackTaskArguments.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/FallbackTaskArgumentsT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingDelayArguments.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingPolicyOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingPolicyOptionsT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingTaskArguments.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingTaskArgumentsT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/IPolicyEventArguments.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/IPolicyEventArgumentsT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/ResetActionArguments.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryActionArguments.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryActionArgumentsT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryDelayArguments.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryPolicyOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryPolicyOptionsT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/TimeoutPolicyOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/TimeoutTaskArguments.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/PollyServiceCollectionExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Polly/ResilienceDimensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/GlobalSuppressions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/IResiliencePipelineBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/IResiliencePipelineProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncDynamicPipelineT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncPolicyPipeline.Args.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncPolicyPipeline.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncPolicyPipelineT.Args.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncPolicyPipelineT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IOnChangeListenersHandler.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IPipelineMetering.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IPolicyPipelineBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IPolicyPipelineBuilderT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IResiliencePipelineFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/Metric.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/NoopChangeToken.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/OnChangeListenersHandler.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/OptionsBuilderExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/OptionsNameHelper.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PipelineConfigurationChangeTokenSource.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PipelineMetering.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PipelineTelemetry.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PolicyPipelineBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PolicyPipelineBuilderT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineBuilder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineBuilderExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineFactoryOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineFactoryOptionsValidatorT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineFactoryTokenSourceOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/SupportedPolicies.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/TelemetryHelper.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.BulkheadT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.CircuitBreakerT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.FallbackT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.HedgingT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.RetryT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.TimeoutT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResilienceWrapperAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Resilience/Resilience/ServiceCollectionExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/EnricherExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/IEnrichmentPropertyBag.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/ILogEnricher.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/IMetricEnricher.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/ITraceEnricher.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/HttpRouteParameterRedactionMode.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/IDownstreamDependencyMetadata.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/IOutgoingRequestContext.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/RequestMetadata.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/TelemetryConstants.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Checkpoint.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/ILatencyContext.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/ILatencyContextProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/ILatencyDataExporter.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/LatencyData.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Measure.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/NullLatencyContext.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/NullLatencyContextExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/CheckpointToken.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/ILatencyContextTokenIssuer.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/LatencyContextRegistrationOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/LatencyRegistryExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/MeasureToken.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/TagToken.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Tag.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/ILogPropertyCollector.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogMethodAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogMethodHelper.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogPropertiesAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogPropertyIgnoreAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/CounterAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/CounterAttributeT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/DimensionAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/GaugeAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/HistogramAttribute.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/HistogramAttributeT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/MeterT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/MeteringExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/buildTransitive/Microsoft.Extensions.Telemetry.Abstractions.props create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/buildTransitive/Microsoft.Extensions.Telemetry.Abstractions.targets create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Console/Latency/LarencyConsoleOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Console/Latency/LatencyConsoleExporter.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Console/Latency/LatencyConsoleExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/ColorSet.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/Colors.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogEntryCompositeState.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogFormatter.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogFormatterOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogFormatterTheme.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogLevelExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/TextWriterExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/LoggingConsoleExporter.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/LoggingConsoleExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/LoggingConsoleOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Console/Microsoft.Extensions.Telemetry.Console.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogCollector.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogCollectorDebugView.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogCollectorOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogRecord.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogger.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLoggerExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLoggerProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLoggerT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/Internal/AggregationType.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricCollector.MeterListener.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricCollector.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricCollectorT.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricValue.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricValuesHolder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry.Testing/Microsoft.Extensions.Telemetry.Testing.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessEnricherDimensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessEnricherExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessLogEnricher.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessLogEnricherOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceEnricherDimensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceEnricherExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceLogEnricher.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceLogEnricherOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceMetricEnricher.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceMetricEnricherOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceTraceEnricher.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceTraceEnricherOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/CheckpointTracker.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContext.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContextPool.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContextProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContextRegistrySet.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContextTokenIssuer.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyInstrumentProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/MeasureTracker.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/Registry.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/ResetOnGetObjectPool.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/TagCollection.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Latency/LatencyContextExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Latency/LatencyContextOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Logging/Logger.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingEventSource.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/EventCountersCollectorOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/EventCountersExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/EventCountersListener.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/CustomConfigurationBinder.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/CustomConfigureNamedOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/EventCountersCollectorOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/EventCountersValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Metering/MeteringOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Metering/MeteringState.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Metering/OTelMeteringExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/HttpHeadersRedactor.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/HttpRouteFormatter.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/HttpRouteParameter.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/HttpRouteParser.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/IDownstreamDependencyMetadataManager.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/IHttpHeadersRedactor.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/IHttpRouteFormatter.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/IHttpRouteParser.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/MetricEnrichmentPropertyBag.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/OutgoingRequestContext.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/ParsedRouteSegments.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/Segment.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnostics.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsConfigParser.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsConfigParserRegex.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsConfigRefresher.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsEventListener.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsEventSource.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/TelemetryCommonExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/Constants.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/DownstreamDependencyMetadataManager.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/DownstreamDependencyMetadataManagerRegex.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/FrozenRequestMetadataTrieNode.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/HostSuffixTrieNode.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/RequestMetadataTrieNode.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/TelemetryExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/Internal/SamplingOptionsAutoValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/Internal/SamplingOptionsCustomValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/ParentBasedSamplerOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/SamplerType.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/SamplingExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/SamplingOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/TraceIdRatioBasedSamplerOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Tracing/TraceEnrichmentProcessor.cs create mode 100644 src/Libraries/Microsoft.Extensions.Telemetry/Tracing/TracingEnricherExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.TimeProvider.Testing/FakeTimeProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.TimeProvider.Testing/FakeTimeProviderTimer.cs create mode 100644 src/Libraries/Microsoft.Extensions.TimeProvider.Testing/Microsoft.Extensions.TimeProvider.Testing.csproj create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Exceptions/DatabaseClientException.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Exceptions/DatabaseException.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Exceptions/DatabaseRetryableException.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Exceptions/DatabaseServerException.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/IDocumentDatabase.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/IDocumentReader.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/IDocumentWriter.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/BatchItem.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/BatchOperation.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/ConsistencyLevel.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/DatabaseOptions.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/FetchMode.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/IDatabaseResponse.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/ITableLocator.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/PatchOperation.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/PatchOperationType.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/Query.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/QueryRequestOptions.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/RegionalDatabaseOptions.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/RequestInfo.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/RequestOptions.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/TableInfo.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/TableOptions.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/Throughput.cs create mode 100644 src/Libraries/System.Cloud.DocumentDb.Abstractions/System.Cloud.DocumentDb.Abstractions.csproj create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageCancelledTokenFeatureExtensions.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageCompleteActionFeatureExtensions.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageDestinationFeatureExtensions.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageDestinationPayloadFeatureExtensions.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageLatencyContextFeatureExtensions.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessagePostponeActionFeatureExtensions.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageSourceFeatureExtensions.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageSourcePayloadFeatureExtensions.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageVisibilityDelayFeatureExtensions.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/SerializedMessagePayloadFeatureExtensions.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Middleware/LatencyRecorderMiddlewareExtensions.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Startup/AsyncProcessingPipelineBuilderExtensions.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Startup/IAsyncProcessingPipelineBuilder.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Startup/IPipelineDelegateFactory.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Startup/ServiceCollectionExtensions.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessageCompleteActionFeature.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessageDestinationFeatures.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessagePayloadFeature.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessagePostponeActionFeature.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessageSourceFeatures.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessageVisibilityDelayFeature.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Features/ISerializedMessagePayloadFeature.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Implementations/BaseMessageConsumer.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageConsumer.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageDelegate.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageDestination.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageMiddleware.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageSource.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/MessageContext.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Delegate/PipelineMessageDelegateStitcher.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Extensions/MessageMiddlewareExtensions.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/MessageDestinationFeatures.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/MessagePayloadFeature.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/MessageSourceFeatures.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/MessageVisibilityDelayFeature.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/SerializedMessagePayloadFeature.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Middlewares/LatencyContextProviderMiddleware.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Middlewares/LatencyRecorderMiddleware.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Startup/AsyncProcessingPipelineBuilder.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Startup/ConsumerBackgroundService.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Startup/PipelineDelegateFactory.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Utilities/Log.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Utilities/UTF8ConverterUtils.cs create mode 100644 src/Libraries/System.Cloud.Messaging.Abstractions/System.Cloud.Messaging.Abstractions.csproj create mode 100644 src/Packages/Directory.Build.props create mode 100644 src/Packages/Directory.Build.targets create mode 100644 src/Packages/Microsoft.Extensions.AuditReports/EmptyInternalClass.cs create mode 100644 src/Packages/Microsoft.Extensions.AuditReports/Microsoft.Extensions.AuditReports.csproj create mode 100644 src/Packages/Microsoft.Extensions.AuditReports/buildTransitive/Microsoft.Extensions.AuditReports.props create mode 100644 src/Packages/Microsoft.Extensions.AuditReports/buildTransitive/Microsoft.Extensions.AuditReports.targets create mode 100644 src/Packages/Microsoft.Extensions.ExtraAnalyzers/EmptyInternalClass.cs create mode 100644 src/Packages/Microsoft.Extensions.ExtraAnalyzers/Microsoft.Extensions.ExtraAnalyzers.csproj create mode 100644 src/Packages/Microsoft.Extensions.ExtraAnalyzers/buildTransitive/Microsoft.Extensions.ExtraAnalyzers.props create mode 100644 src/Packages/Microsoft.Extensions.ExtraAnalyzers/buildTransitive/Microsoft.Extensions.ExtraAnalyzers.targets create mode 100644 src/Packages/Microsoft.Extensions.StaticAnalysis/Microsoft.Extensions.StaticAnalysis.csproj create mode 100644 src/Shared/BufferWriterPool/BufferWriter.cs create mode 100644 src/Shared/BufferWriterPool/BufferWriterPool.cs create mode 100644 src/Shared/BufferWriterPool/BufferWriterPooledObjectPolicy.cs create mode 100644 src/Shared/BufferWriterPool/README.md create mode 100644 src/Shared/Data.Validation/ExclusiveRangeAttribute.cs create mode 100644 src/Shared/Data.Validation/LengthAttribute.cs create mode 100644 src/Shared/Data.Validation/README.md create mode 100644 src/Shared/Data.Validation/TimeSpanAttribute.cs create mode 100644 src/Shared/Data.Validation/ValidationContextExtensions.cs create mode 100644 src/Shared/Debugger/AttachedDebugger.cs create mode 100644 src/Shared/Debugger/DebuggerExtensions.cs create mode 100644 src/Shared/Debugger/DebuggerState.cs create mode 100644 src/Shared/Debugger/DetachedDebugger.cs create mode 100644 src/Shared/Debugger/IDebuggerState.cs create mode 100644 src/Shared/Debugger/README.md create mode 100644 src/Shared/Debugger/SystemDebugger.cs create mode 100644 src/Shared/EmptyCollections/Empty.cs create mode 100644 src/Shared/EmptyCollections/EmptyCollectionExtensions.cs create mode 100644 src/Shared/EmptyCollections/EmptyReadOnlyList.cs create mode 100644 src/Shared/EmptyCollections/EmptyReadonlyDictionary.cs create mode 100644 src/Shared/EmptyCollections/README.md create mode 100644 src/Shared/Memoization/Memoize.cs create mode 100644 src/Shared/Memoization/MemoizedFunction.cs create mode 100644 src/Shared/Memoization/README.md create mode 100644 src/Shared/NumericExtensions/NumericExtensions.cs create mode 100644 src/Shared/NumericExtensions/README.md create mode 100644 src/Shared/Pools/NoopPooledObjectPolicy.cs create mode 100644 src/Shared/Pools/PoolFactory.cs create mode 100644 src/Shared/Pools/PooledCancellationTokenSourcePolicy.cs create mode 100644 src/Shared/Pools/PooledDictionaryPolicy.cs create mode 100644 src/Shared/Pools/PooledListPolicy.cs create mode 100644 src/Shared/Pools/PooledSetPolicy.cs create mode 100644 src/Shared/Pools/README.md create mode 100644 src/Shared/RentedSpan/README.md create mode 100644 src/Shared/RentedSpan/RentedSpan.cs create mode 100644 src/Shared/Shared.csproj create mode 100644 src/Shared/Text.Formatting/CompositeFormat.cs create mode 100644 src/Shared/Text.Formatting/FormatExtensions.cs create mode 100644 src/Shared/Text.Formatting/README.md create mode 100644 src/Shared/Text.Formatting/Segment.cs create mode 100644 src/Shared/Text.Formatting/StringBuilderExtensions.cs create mode 100644 src/Shared/Text.Formatting/StringMaker.cs create mode 100644 src/Shared/Throw/README.md create mode 100644 src/Shared/Throw/Throw.cs create mode 100644 src/ToBeMoved/DependencyInjection.AutoActivation/AutoActivationExtensions.cs create mode 100644 src/ToBeMoved/DependencyInjection.AutoActivation/AutoActivationHostedService.cs create mode 100644 src/ToBeMoved/DependencyInjection.AutoActivation/AutoActivatorOptions.cs create mode 100644 src/ToBeMoved/DependencyInjection.AutoActivation/DependencyInjection.AutoActivation.csproj create mode 100644 src/ToBeMoved/DependencyInjection.NamedService/DependencyInjection.NamedService.csproj create mode 100644 src/ToBeMoved/DependencyInjection.NamedService/INamedServiceProvider.cs create mode 100644 src/ToBeMoved/DependencyInjection.NamedService/NamedServiceCollectionExtensions.cs create mode 100644 src/ToBeMoved/DependencyInjection.NamedService/NamedServiceDescriptor.cs create mode 100644 src/ToBeMoved/DependencyInjection.NamedService/NamedServiceProvider.cs create mode 100644 src/ToBeMoved/DependencyInjection.NamedService/NamedServiceProviderExtensions.cs create mode 100644 src/ToBeMoved/DependencyInjection.NamedService/NamedServiceProviderOptions.cs create mode 100644 src/ToBeMoved/DependencyInjection.Pools/DependencyInjectedPolicy.cs create mode 100644 src/ToBeMoved/DependencyInjection.Pools/DependencyInjection.Pools.csproj create mode 100644 src/ToBeMoved/DependencyInjection.Pools/DependencyInjectionExtensions.cs create mode 100644 src/ToBeMoved/DependencyInjection.Pools/PoolOptions.cs create mode 100644 src/ToBeMoved/Directory.Build.props create mode 100644 src/ToBeMoved/Hosting.StartupInitialization/Hosting.StartupInitialization.csproj create mode 100644 src/ToBeMoved/Hosting.StartupInitialization/IStartupInitializationBuilder.cs create mode 100644 src/ToBeMoved/Hosting.StartupInitialization/IStartupInitializer.cs create mode 100644 src/ToBeMoved/Hosting.StartupInitialization/Internal/FunctionDerivedInitializer.cs create mode 100644 src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupHostedService.cs create mode 100644 src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupInitializationBuilder.cs create mode 100644 src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupInitializationOptionsValidator.cs create mode 100644 src/ToBeMoved/Hosting.StartupInitialization/StartupInitializationExtensions.cs create mode 100644 src/ToBeMoved/Hosting.StartupInitialization/StartupInitializationOptions.cs create mode 100644 src/ToBeMoved/HttpClient.SocketHandling/HttpClient.SocketHandling.csproj create mode 100644 src/ToBeMoved/HttpClient.SocketHandling/HttpClientSocketHandlingExtensions.cs create mode 100644 src/ToBeMoved/HttpClient.SocketHandling/SocketsHttpHandlerBuilder.cs create mode 100644 src/ToBeMoved/HttpClient.SocketHandling/SocketsHttpHandlerOptions.cs create mode 100644 src/ToBeMoved/HttpClient.SocketHandling/SocketsHttpHandlerOptionsValidator.cs create mode 100644 src/ToBeRemoved/Directory.Build.props create mode 100644 src/ToBeRemoved/Options.ValidateOnStart/Options.ValidateOnStart.csproj create mode 100644 src/ToBeRemoved/Options.ValidateOnStart/OptionsBuilderExtensions.cs create mode 100644 src/ToBeRemoved/Options.ValidateOnStart/ValidateOptionsResultExtensions.cs create mode 100644 src/ToBeRemoved/Options.ValidateOnStart/ValidationHostedService.cs create mode 100644 src/ToBeRemoved/Options.ValidateOnStart/ValidatorOptions.cs create mode 100644 start-code.cmd create mode 100755 start-code.sh create mode 100644 start-vs.cmd create mode 100644 test/.editorconfig create mode 100644 test/Directory.Build.props create mode 100644 test/Directory.Build.targets create mode 100644 test/Generators/Directory.Build.props create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Generated/Common/BasicRequestsTests.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Generated/Common/BodyTests.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Generated/Common/HeadersTests.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Generated/Common/PathTests.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Generated/Common/QueryTests.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Generated/Common/RestApiClientOptionsTests.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Generated/Common/SpecialReturnTypeTests.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Generated/Common/TelemetryTests.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Generated/Directory.Build.props create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Generated/Roslyn3.8/Microsoft.Gen.AutoClient.Roslyn3.8.Generated.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Generated/Roslyn4.0/Microsoft.Gen.AutoClient.Roslyn4.0.Generated.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.AutoClient/TestClasses/IBasicTestClient.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/TestClasses/IBodyTestClient.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/TestClasses/ICustomRequestMetadataTestClient.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/TestClasses/IParamHeaderTestClient.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/TestClasses/IPathTestClient.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/TestClasses/IQueryTestClient.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/TestClasses/IRequestMetadataTestApi.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/TestClasses/IRequestMetadataTestClient.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/TestClasses/IRestApiClientOptionsApi.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/TestClasses/IReturnTypesTestClient.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/TestClasses/IStaticHeaderTestClient.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Unit/Common/BodyContentTypeParamExtensionsTests.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Unit/Common/DiagDescriptorsTests.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Unit/Common/EmitterTests.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Unit/Common/ParserTests.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Unit/Common/RestApiMethodParameterTests.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Unit/Common/RestApiMethodTests.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Unit/Common/SymbolLoaderTests.cs create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Unit/Directory.Build.props create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Unit/Roslyn3.8/Microsoft.Gen.AutoClient.Roslyn3.8.Unit.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.AutoClient/Unit/Roslyn4.0/Microsoft.Gen.AutoClient.Roslyn4.0.Unit.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.ComplianceReports/GoldenReports/Basic.json create mode 100644 test/Generators/Microsoft.Gen.ComplianceReports/GoldenReports/Inheritance.json create mode 100644 test/Generators/Microsoft.Gen.ComplianceReports/GoldenReports/LogMethod.json create mode 100644 test/Generators/Microsoft.Gen.ComplianceReports/TestClasses/Basic.cs create mode 100644 test/Generators/Microsoft.Gen.ComplianceReports/TestClasses/Inheritance.cs create mode 100644 test/Generators/Microsoft.Gen.ComplianceReports/TestClasses/LogMethod.cs create mode 100644 test/Generators/Microsoft.Gen.ComplianceReports/Unit/Common/GeneratorTests.cs create mode 100644 test/Generators/Microsoft.Gen.ComplianceReports/Unit/Directory.Build.props create mode 100644 test/Generators/Microsoft.Gen.ComplianceReports/Unit/Roslyn3.8/Microsoft.Gen.ComplianceReports.Roslyn3.8.Unit.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.ComplianceReports/Unit/Roslyn4.0/Microsoft.Gen.ComplianceReports.Roslyn4.0.Unit.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/Generated/Common/ContextualOptionsTests.cs create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/Generated/Directory.Build.props create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/Generated/Roslyn3.8/Microsoft.Gen.ContextualOptions.Roslyn3.8.Generated.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/Generated/Roslyn4.0/Microsoft.Gen.ContextualOptions.Roslyn4.0.Generated.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Class1.cs create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Class2A.cs create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Class2B.cs create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/ClassWithNoAttribute.cs create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/ClassWithUnusableProperties.txt create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/NamespacelessRecord.cs create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/NonPartialClass.txt create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/NonPublicStruct.cs create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Record1.cs create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/RefStruct.txt create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/StaticClass.txt create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Struct1.cs create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/ContextualOptionsTests.cs create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/DiagDescriptorsTests.cs create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/EmitterTests.cs create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/ParserTests.cs create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/SyntaxContextReceiverTests.cs create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/Unit/Directory.Build.props create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/Unit/Roslyn3.8/Microsoft.Gen.ContextualOptions.Roslyn3.8.Unit.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.ContextualOptions/Unit/Roslyn4.0/Microsoft.Gen.ContextualOptions.Roslyn4.0.Unit.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.EnumStrings/Generated/Common/EnumStringsTests.cs create mode 100644 test/Generators/Microsoft.Gen.EnumStrings/Generated/Directory.Build.props create mode 100644 test/Generators/Microsoft.Gen.EnumStrings/Generated/Roslyn3.8/Microsoft.Gen.EnumStrings.Roslyn3.8.Generated.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.EnumStrings/Generated/Roslyn4.0/Microsoft.Gen.EnumStrings.Roslyn4.0.Generated.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.EnumStrings/TestClasses/AssemblyLevel.cs create mode 100644 test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Flags.cs create mode 100644 test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Negative.cs create mode 100644 test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Nested.cs create mode 100644 test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Options.cs create mode 100644 test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Overlapping.cs create mode 100644 test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Sizes.cs create mode 100644 test/Generators/Microsoft.Gen.EnumStrings/TestClasses/UnderlyingTypes.cs create mode 100644 test/Generators/Microsoft.Gen.EnumStrings/Unit/Common/EmitterTests.cs create mode 100644 test/Generators/Microsoft.Gen.EnumStrings/Unit/Common/ParserTests.cs create mode 100644 test/Generators/Microsoft.Gen.EnumStrings/Unit/Directory.Build.props create mode 100644 test/Generators/Microsoft.Gen.EnumStrings/Unit/Roslyn3.8/Microsoft.Gen.EnumStrings.Roslyn3.8.Unit.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.EnumStrings/Unit/Roslyn4.0/Microsoft.Gen.EnumStrings.Roslyn4.0.Unit.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.Logging/Generated/Common/LogMethodAttributeTests.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Generated/Common/LogMethodTests.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Generated/Common/LogPropertiesProviderTests.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Generated/Common/LogPropertiesRedactionTests.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Generated/Common/LogPropertiesTests.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Generated/Common/StarRedactor.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Generated/Common/StarRedactorProvider.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Generated/Directory.Build.props create mode 100644 test/Generators/Microsoft.Gen.Logging/Generated/Roslyn3.8/Microsoft.Gen.Logging.Roslyn3.8.Generated.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.Logging/Generated/Roslyn4.0/Microsoft.Gen.Logging.Roslyn4.0.Generated.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/ArgTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/AtSymbolsTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/AttributeTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/CollectionTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/ConstraintsTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/ConstructorVariationsTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/CustomToStringTestClass.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/EnumerableTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/EventNameTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/ExceptionTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/FormattableTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/InParameterTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/InvariantTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/LevelTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesNullHandlingExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesOmitParameterNameExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesProviderExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesProviderWithObjectExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesRedactionExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesSimpleExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesSpecialTypesExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/MessageTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/MiscTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/NamespaceTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/NestedClassTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/NonStaticNullableTestClass.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/NonStaticTestClass.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/NullableTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/OverloadsTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/RecordTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/SignatureTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/SkipEnabledCheckTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/StructTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/TemplateTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/TestClasses/TestInstances.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Unit/Common/AttributeParserTests.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Unit/Common/DiagDescriptorsTests.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Unit/Common/EmitterTests.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Unit/Common/EmitterUtilsTests.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Unit/Common/LogParserUtilitiesTests.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Unit/Common/LoggingMethodParameterTests.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Unit/Common/LoggingMethodTests.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Unit/Common/LoggingTypeTests.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Unit/Common/ParserTests.LogProperties.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Unit/Common/ParserTests.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Unit/Common/ParserUtilitiesTests.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Unit/Common/SymbolLoaderTests.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Unit/Common/TemplatesExtractorTests.cs create mode 100644 test/Generators/Microsoft.Gen.Logging/Unit/Directory.Build.props create mode 100644 test/Generators/Microsoft.Gen.Logging/Unit/Roslyn3.8/Microsoft.Gen.Logging.Roslyn3.8.Unit.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.Logging/Unit/Roslyn4.0/Microsoft.Gen.Logging.Roslyn4.0.Unit.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.Metering/Generated/Common/MetricTests.Ext.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/Generated/Common/MetricTests.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/Generated/Directory.Build.props create mode 100644 test/Generators/Microsoft.Gen.Metering/Generated/Roslyn3.8/Microsoft.Gen.Metering.Roslyn3.8.Generated.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.Metering/Generated/Roslyn4.0/Microsoft.Gen.Metering.Roslyn4.0.Generated.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.Metering/TestClasses/AttributedWithoutNamespace.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/TestClasses/CounterDimensions.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/TestClasses/CounterTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/TestClasses/FileScopedNamespaceExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/TestClasses/HistogramTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/TestClasses/MeterTExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/TestClasses/MetricConstants.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/TestClasses/MetricRecordClassTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/TestClasses/MetricRecordStructTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/TestClasses/MetricStructTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/TestClasses/NestedClassMetrics.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/TestClasses/NestedRecordClassMetrics.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/TestClasses/NestedRecordStructMetrics.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/TestClasses/NestedStructMetrics.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/TestClasses/OverlappingNamesTestExtensions.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/TestClasses/Public.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/Unit/Common/DiagDescriptorsTests.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/Unit/Common/EmitterTests.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/Unit/Common/MetricMethodTests.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/Unit/Common/MetricParameterTests.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/Unit/Common/MetricTypeTests.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/Unit/Common/ParserTests.StrongTypes.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/Unit/Common/ParserTests.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/Unit/Common/StrongTypeConfigTests.cs create mode 100644 test/Generators/Microsoft.Gen.Metering/Unit/Directory.Build.props create mode 100644 test/Generators/Microsoft.Gen.Metering/Unit/Roslyn3.8/Microsoft.Gen.Metering.Roslyn3.8.Unit.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.Metering/Unit/Roslyn4.0/Microsoft.Gen.Metering.Roslyn4.0.Unit.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.MeteringReports/Unit/Common/EmitterTests.cs create mode 100644 test/Generators/Microsoft.Gen.MeteringReports/Unit/Common/GeneratorTests.cs create mode 100644 test/Generators/Microsoft.Gen.MeteringReports/Unit/Directory.Build.props create mode 100644 test/Generators/Microsoft.Gen.MeteringReports/Unit/Roslyn3.8/Microsoft.Gen.MeteringReports.Roslyn3.8.Unit.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.MeteringReports/Unit/Roslyn4.0/Microsoft.Gen.MeteringReports.Roslyn4.0.Unit.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/CustomAttrTests.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/EnumerationTests.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/FieldTests.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/FunnyStringsTests.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/GenericsTests.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/MultiModelValidatorTests.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/NestedTests.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/NoNamespaceTests.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/OptionsValidationTests.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/RandomMembersTests.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/RecordTypesTests.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/RepeatedTypesTests.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/SelfValidationTests.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/TestResource.Designer.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/TestResource.resx create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/Utils.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/ValueTypesTests.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Directory.Build.props create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Roslyn3.8/Microsoft.Gen.OptionsValidation.Roslyn3.8.Generated.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Generated/Roslyn4.0/Microsoft.Gen.OptionsValidation.Roslyn4.0.Generated.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/CustomAttr.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Enumeration.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Fields.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/FileScopedNamespace.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/FunnyStrings.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Generics.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Models.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/MultiModelValidator.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Nested.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/NoNamespace.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/RandomMembers.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/RecordTypes.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/RepeatedTypes.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/SelfValidation.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/ValueTypes.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Unit/Common/EmitterTests.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Unit/Common/ParserTests.Enumeration.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Unit/Common/ParserTests.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Unit/Common/SymbolLoaderTests.cs create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Unit/Directory.Build.props create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Unit/Roslyn3.8/Microsoft.Gen.OptionsValidation.Roslyn3.8.Unit.Tests.csproj create mode 100644 test/Generators/Microsoft.Gen.OptionsValidation/Unit/Roslyn4.0/Microsoft.Gen.OptionsValidation.Roslyn4.0.Unit.Tests.csproj create mode 100644 test/Generators/Shared/RoslynTestUtils.cs create mode 100644 test/Libraries/Directory.Build.props create mode 100644 test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/AsyncContextHttpContextOfTTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/AsyncStateHttpContextExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/Microsoft.AspNetCore.AsyncState.Tests.csproj create mode 100644 test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/Mock/IThing.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/Mock/Thing.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/AcceptanceTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/ConnectionTimeoutDelegateTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/ConnectionTimeoutExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/ConnectionTimeoutValidatorTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/Microsoft.AspNetCore.ConnectionTimeout.Tests.csproj create mode 100644 test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/Startup.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/StaticFakeClockExecution.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/CommonHeadersTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderKeyTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingFeatureTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingOptionsTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderRegistryTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderSetupTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HostHeaderValueTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/Microsoft.AspNetCore.HeaderParsing.Tests.csproj create mode 100644 test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/ParserTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/AcceptanceTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Checkpoint/AcceptanceTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Checkpoint/AddServerTimingHeaderMiddlewareTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Checkpoint/LatencyContextControlExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Checkpoint/RequestCheckpointExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Internal/RequestLatencyTelemetryMiddlewareTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Internal/RequestLatencyTelemetryOptionsValidatorTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/RequestLatencyTelemetryExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/AcceptanceTest.Mvc.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/AcceptanceTest.Routing.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/AcceptanceTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Controllers/ApiRoutingController.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Controllers/AttributeRoutingController.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Controllers/ConventionalRoutingController.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Controllers/MixedRoutingController.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/HeaderReaderTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/HttpLoggingServiceExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/HttpRequestBodyReaderTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/IncomingHttpDimensionsTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/IncomingRequestLogRecordTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/IncomingRequestStructTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/InfiniteStream.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/RequestBodyErrorPipeFeature.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/RequestBodyMultiSegmentPipeFeature.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/TestBodyPipeFeatureMiddleware.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/TestHttpLogEnricher.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/LoggingMiddlewareTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/LoggingOptionsValidationTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/MediaTypeSetExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/PipeReaderExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/ResponseInterceptingStreamTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/HttpMeteringTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/Internals/NullRequestEnricher.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/Internals/PropertyBagEdgeCaseEnricher.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/Internals/SameDefaultDimEnricher.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/Internals/TestEnricher.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Microsoft.AspNetCore.Telemetry.Middleware.Tests.csproj create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/appsettings.json create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Enrichment.RequestHeaders.Tests/Internals/TestExtensions.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Enrichment.RequestHeaders.Tests/RequestHeadersEnricherExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Enrichment.RequestHeaders.Tests/RequestHeadersEnricherTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Microsoft.AspNetCore.Telemetry.Tests.csproj create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Telemetry.Internal.Http/HttpUtilityExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Telemetry.Internal.Http/IncomingHttpRouteUtilityTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Telemetry.Internal.Http/TestController.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/HttpTracingExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/HttpTracingOptionsValidationTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/HttpUrlRedactionProcessorTests.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/ConfigurationExtensions.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestEnricher.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestEnricher2.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestEventListener.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestExporter.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestHttpClientProvider.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestHttpTraceEnricher.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestTraceProcessor.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TracerProviderExtensions.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/WrappedActivityExportProcessor.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/appsettings.json create mode 100644 test/Libraries/Microsoft.AspNetCore.Testing.Tests/FakesExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Testing.Tests/Internal/FakeCertificateFactoryTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Testing.Tests/Internal/FakeCertificateHttpClientHandlerTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Testing.Tests/Internal/FakeStartupTest.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Testing.Tests/Microsoft.AspNetCore.Testing.Tests.csproj create mode 100644 test/Libraries/Microsoft.AspNetCore.Testing.Tests/TestResources/ReturningHttpClientHandler.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Testing.Tests/TestResources/Startup.cs create mode 100644 test/Libraries/Microsoft.AspNetCore.Testing.Tests/TestResources/TestHandler.cs create mode 100644 test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/AcceptanceTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataSourceTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataValidatorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/Microsoft.Extensions.AmbientMetadata.Application.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncContextServiceCollectionExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncContextTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTokenTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AsyncState.Tests/Microsoft.Extensions.AsyncState.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.AsyncState.Tests/Mock/AnotherThing.cs create mode 100644 test/Libraries/Microsoft.Extensions.AsyncState.Tests/Mock/IThing.cs create mode 100644 test/Libraries/Microsoft.Extensions.AsyncState.Tests/Mock/Thing.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Classification/DataClassificationAttributeTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Classification/DataClassificationTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Classification/NoDataClassificationAttributeTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Classification/UnknownDataClassificationAttributeTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Microsoft.Extensions.Compliance.Abstractions.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/FakeFormattable.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/FakeObject.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/FakeSpanFormattable.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/NullRedactor.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/RedactorAbstractionsExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/ErasingRedactorTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/FakePlaintextRedactor.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/FakeStartup.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/Microsoft.Extensions.Compliance.Redaction.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/NullRedactorProvider.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/NullRedactorTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/RedactionAcceptanceTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/RedactorProviderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/XXHash3RedactorExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/XXHash3RedactorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/AttributeTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/FakeRedactorOptionsValidatorTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/FakeRedactorProviderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/FakeRedactorTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/InstancesTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/Microsoft.Extensions.Compliance.Testing.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/RedactionFakesAcceptanceTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/RedactionFakesEventCollectorTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/Setup.cs create mode 100644 test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/TaxonomyExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Abstractions/ExceptionSummaryTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/ExceptionSummarizerTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/ExceptionSummaryExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/HttpExceptionSummaryProviderExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/HttpExceptionSummaryProviderTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/TestException.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ApplicationLifecycleHealthCheckTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ApplicationLifecycleHealthChecksExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/KubernetesHealthCheckPublisherExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/KubernetesHealthCheckPublisherTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ManualHealthCheckExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ManualHealthCheckTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/MockHostApplicationLifetime.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/TelemetryHealthChecksPublisherExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/TelemetryHealthChecksPublisherTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/Helpers/DummyProvider.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/Helpers/DummyTracker.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/IResourceUtilizationSnapshotProviderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/IResourceUtilizationTrackerTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/NullResourceUtilizationTest/NullResourceUtilizationTrackerServiceTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/NullResourceUtilizationTest/NullSnapshotProviderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/ResouceUtilizationAbstractionsExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/ResourceUtilizationSnapshotTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/SystemResourcesTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/UtilizationTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/CalculatorTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/CircularBufferTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Providers/ConditionallyFaultProvider.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Providers/FakeProvider.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Providers/FaultProvider.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Publishers/AnotherPublisher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Publishers/EmptyPublisher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Publishers/FaultPublisher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Publishers/GenericPublisher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationBuilderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerOptionsManualValidatorTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerOptionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerOptionsValidatorTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerServiceTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTestResourceUtilizationLinux.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FakeOperatingSystem.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FakeUserHz.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/GenericPublisher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/PathReturningFileSystem.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/TestResources.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/MemoryInfoTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/SystemInfoTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/TcpTableInfoTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsCountersOptionsCustomValidatorTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsCountersTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsSnapshotProviderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsUtilizationExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/FileWithRChars create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpu.cfs_period_us create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpu.cfs_quota_us create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpuacct.stat create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpuset.cpus create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/meminfo create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/memory.limit_in_bytes create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/memory.usage_in_bytes create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/status create mode 100644 test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/test.cpuacct.stat create mode 100644 test/Libraries/Microsoft.Extensions.EnumStrings.Tests/EnumStringsAttributeTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.EnumStrings.Tests/Microsoft.Extensions.EnumStrings.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/FakeConfigurationSourceTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/FakeHostBuilderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/FakeHostTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/HostTerminatorServiceTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/HostingFakesExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/Microsoft.Extensions.Hosting.Testing.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/TestResources/DependentClass.cs create mode 100644 test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/TestResources/InnerClass.cs create mode 100644 test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/TestResources/OuterClass.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/InterfaceAttributesTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/MethodAttributesTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/Microsoft.Extensions.Http.AutoClient.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/ParameterAttributesTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/RestApiExceptionTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/FallbackClientHandlerOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Helpers/ConfigurationStubFactory.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Helpers/OptionsUtilities.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.BySelector.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.Fallback.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.Resilience.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.Standard.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpStandardResilienceOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/ContextExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/DefaultRequestClonerTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/FallbackClientHandlerOptionsValidatorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/FallbackTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/HttpRequestMessageExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/HttpResiliencePipelineBuilderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/PipelineNameHelperTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/ResilienceHandlerTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/Validators/HttpStandardResilienceOptionsCustomValidatorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/Validators/ValidationHelperTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/TestHandlerStub.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/HttpClientBuilderExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/HttpClientFaultInjectionExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/HttpFaultInjectionOptionsBuilderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/FaultInjectionTelemetryHandlerTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/FaultInjectionWeightAssignmentContextMessageHandlerTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/HttpClientChaosPolicyFactoryTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/HttpContentOptionsRegistryTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/PolicyContextExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HedgingTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/ConfigurationStubFactory.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/OptionsUtilities.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/TestHandlerStub.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpClientHedgingResiliencePredicatesTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpHedgingPolicyOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/DefaultRoutingStrategyFactoryTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/HedgingContextExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/IStubRoutingService.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/MockRoutingStrategy.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/RandomizerTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/RequestMessageSnapshotPolicyTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/RoutingHelperTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/Validators/HttpStandardHedgingResilienceOptionsCustomValidatorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Routing/OrderedRoutingStrategyTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Routing/RoutingStrategyTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Routing/WeightedRoutingStrategyTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/StandardHedgingTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Microsoft.Extensions.Http.Resilience.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpCircuitBreakerPolicyOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpClientResiliencePredicatesTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpFallbackPolicyOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpResponseMessageExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpRetryPolicyOptionTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/configs/appsettings.json create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/HttpClientLatencyTelemetryExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpCheckpointsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpMockProvider.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpRequestLatencyListenerTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpClientLoggingAcceptanceTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpClientLoggingDimensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpClientLoggingExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpHeadersReaderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpLoggingHandlerTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpRequestBodyReaderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpRequestReaderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpResponseBodyReaderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/EmptyEnricher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/EnricherWithCounter.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/HelperExtensions.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/ITestHttpClient1.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/ITestHttpClient2.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/LogRecordExtensions.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/MockedLogger.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/MockedRequestReader.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/NoRemoteCallHandler.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/NotSeekableStream.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/RandomStringGenerator.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestConfiguration.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestEnricher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestHttpClient1.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestHttpClient2.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestHttpMessageHandlerBuilder.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestingHandlerStub.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/LogRecordPooledObjectPolicyTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/LoggingOptionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/LoggingOptionsValidatorTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/MediaTypeCollectionExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/HttpMeteringHandlerTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/HelperExtensions.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/NoRemoteCallHandler.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/NullOutgoingRequestMetricEnricher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/PropertyBagEdgeCaseEnricher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/SameDefaultDimEnricher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/TestDownstreamDependencyMetadata.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/TestEnricher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/TestHandlerStub.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Microsoft.Extensions.Http.Telemetry.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Text.txt create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/HttpClientRedactionProcessorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/HttpClientTraceEnrichmentProcessorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/HttpClientTracingExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/HttpClientTracingOptionsValidationTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/FakeHttpWebResponse.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestEventListener.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestExtensions.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestHttpClientTraceEnricher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestHttpPathRedactor.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestHttpServer.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestTraceProcessor.cs create mode 100644 test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/appsettings.json create mode 100644 test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/AcceptanceTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/ContextualOptionsFactoryTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/ContextualOptionsServiceCollectionExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/Microsoft.Extensions.Options.Contextual.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.Options.Validation.Tests/Microsoft.Extensions.Options.Validation.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.Options.Validation.Tests/ValidateEnumeratedItemsAttributeTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Options.Validation.Tests/ValidateObjectMembersAttributeTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/FaultInjectionExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/FaultInjectionOptionsBuilderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/InjectedFaultExceptionTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/ChaosPolicyFactoryTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/ExceptionRegistryTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/FaultInjectionOptionsProviderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/FaultInjectionTelemetryHandlerTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/WeightAssignmentHelperTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/ChaosPolicyOptionsGroupTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/ExceptionPolicyOptionTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/FaultInjectionOptionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/HttpResponseInjectionPolicyOptionTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/LatencyPolicyOptionTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/OptionsValidationTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Microsoft.Extensions.Resilience.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/GlobalSuppressions.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/AsyncHedgingPolicyTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/AsyncHedgingPolicyTestsNonGeneric.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/AsyncHedgingSyntaxTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/FakeTimeProviderExtensions.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/HedgingEngineTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/HedgingTestUtilities.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/TaskHelper.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/HedgingTaskProviderArgumentsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Helpers/AssertionFailure.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Helpers/CustomObject.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Helpers/DisposableResult.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Helpers/FailureResultContextHelper.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/ContextExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/FailureReasonResolverTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/PipelineIdTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/PolicyFactoryTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/PolicyMeteringTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/RetryPolicyOptionsCustomValidatorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/BreakActionArgumentsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/BreakActionArgumentsTestsNonGeneric.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/BulkheadPolicyOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/BulkheadTaskArgumentsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/CircuitBreakerPolicyOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/CircuitBreakerPolicyOptionsTestsNonGeneric.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/Constants.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackPolicyOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackPolicyOptionsTestsNonGeneric.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackScenarioTaskArgumentsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackTaskArgumentsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackTaskArgumentsTestsNonGeneric.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingDelayArgumentsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingPolicyOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingPolicyOptionsTestsNonGeneric.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingTaskArgumentsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingTaskArgumentsTestsNonGeneric.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/OptionsUtilities.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/ResetActionArgumentsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryActionArgumentsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryActionArgumentsTestsNonGeneric.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryDelayArgumentsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryPolicyOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryPolicyOptionsTestsNonGeneric.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/TimeoutPolicyOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/TimeoutTaskArgumentsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/PollyServiceCollectionExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/ResilienceDimensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/ResiliencePollyFakeClockTestsCollection.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/RetryOptionsExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Helpers/ResilienceTestHelper.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/AsyncPolicyPipelineTResultTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/AsyncPolicyPipelineTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/NoopChangeTokenTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/OnChangeListenersHandlerTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/OptionsNameHelperTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/PipelineMeteringTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/PipelineTelemetryTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/PolicyPipelineBuilderTResultTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/PolicyPipelineBuilderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineBuilderExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineFactoryOptionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineFactoryOptionsValidatorTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineFactoryTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineProviderTest.DynamicChanges.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineProviderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResilienceFakeClockTestsCollection.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.BulkheadT.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.CircuitBreakerT.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.FallbackT.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.HedgingT.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.RetryT.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.TimeoutT.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ServiceCollectionExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/configs/appsettings.json create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/configs/optionsOnChangeTestNew.json create mode 100644 test/Libraries/Microsoft.Extensions.Resilience.Tests/configs/optionsOnChangeTestOriginal.json create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Enrichment/EnricherExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Enrichment/TestLogEnrichmentPropertyBag.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Enrichment/TestMetricEnrichmentPropertyBag.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Http/AbstractionTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/CheckpointTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/LatencyDataTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/LatencyRegistryExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/MeasureTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/NoopLatencyContextTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/TagTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Logging/LogMethodAttributeTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Logging/LogMethodHelperTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Logging/LogPropertiesAttributeTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Metering/MetricAttributeTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Microsoft.Extensions.Telemetry.Abstractions.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Latency/LatencyConsoleExporterTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Latency/LatencyConsoleExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Latency/LatencyConsoleOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/AcceptanceTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Helpers/TestException.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/ColorSetTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/LogFormatterOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/LogFormatterTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/LogLevelExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/TestLogEnricher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/TextWriterExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/LoggingConsoleExporterTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/LoggingConsoleExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/LoggingConsoleOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Microsoft.Extensions.Telemetry.Console.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/appsettings.json create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLogCollectorOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLogCollectorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLoggerExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLoggerProviderTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLoggerTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/TestLog.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.Counter.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.Histogram.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.ObservableCounter.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.ObservableGauge.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.ObservableUpdownCounter.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.UpDownCounter.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricValueTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricValuesHolderTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Microsoft.Extensions.Telemetry.Testing.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/Internals/TestExtensions.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/ProcessEnricherDimensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/ProcessEnricherExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/ProcessLogEnricherTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/TestLogEnrichmentPropertyBag.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/Internals/TestExtensions.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/Internals/TestLogEnrichmentPropertyBag.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/Internals/TestMetricEnrichmentPropertyBag.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceEnricherDimensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceEnricherExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceEnricherOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceLogEnricherTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceMetricEnricherTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceTraceEnricherTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/CheckpointTrackerTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextExtensions.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextPoolTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextProviderTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextRegistrySetTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextTokenIssuerTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/MeasureTrackerTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/MockLatencyContextRegistrationOptions.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/RegistryTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/TagCollectionTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/LatencyContextExtensionTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/AnotherEnricher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/EmptyEnricher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/EmptyStringEnricher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/FlexibleEnricher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/Helpers.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/PrimitiveValuesEnricher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/SimpleEnricher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/TestExceptionThrower.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/TestExporter.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/TestProcessor.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Log/LoggingOptionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/LogEnrichmentTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/LoggerTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/Auxiliary/TestEventSource.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/Auxiliary/TestUtils.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/EventCountersExtensionsTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/EventCountersListenerTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/EventCountersValidatorTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering/Internal/TestEnricher.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering/Internal/TestExporter.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering/Internal/TestExtensions.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering/OTelMeteringExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/HttpHeadersRedactorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/HttpParserTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/HttpRouteFormatterTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/MetricEnrichmentPropertyBagTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsConfigParserTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsConfigRefresherTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsEventListenerTest.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsEventSourceTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/Telemetry.Internal.Test.xunit.runner.json create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/TelemetryCommonExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/TestEventListener.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/TestEventSource.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Tracing.Sampling/SamplingExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Tracing.Sampling/SamplingOptionsCustomValidatorTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/Tracing/EnricherExtensionsTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json create mode 100644 test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/FakeTimeProviderTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/FakeTimeProviderTimerTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/Microsoft.Extensions.TimeProvider.Testing.Tests.csproj create mode 100644 test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/BatchItemTests.cs create mode 100644 test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/DatabaseOptionsTests.cs create mode 100644 test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/ExceptionsTests.cs create mode 100644 test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/PatchOperationTest.cs create mode 100644 test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/QueryTest.cs create mode 100644 test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/RegionalDatabaseOptionsTests.cs create mode 100644 test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/RequestOptionsTests.cs create mode 100644 test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/System.Cloud.DatabaseDb.Abstractions.Tests.csproj create mode 100644 test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/TableOptionsTests.cs create mode 100644 test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Consumers/DerivedConsumer.cs create mode 100644 test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Consumers/OverridenConsumer.cs create mode 100644 test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Consumers/SampleConsumer.cs create mode 100644 test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Consumers/SingleMessageConsumer.cs create mode 100644 test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Delegates/SampleWriterDelegate.cs create mode 100644 test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Middlewares/SampleMiddleware.cs create mode 100644 test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Sources/AnotherSource.cs create mode 100644 test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Sources/SampleSource.cs create mode 100644 test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Extensions/MessageContext/SerializedMessagePayloadExtensionsTests.cs create mode 100644 test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Extensions/Startup/ServiceCollectionExtensionsTests.cs create mode 100644 test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Internal/Implementations/BaseMessageConsumerTests.cs create mode 100644 test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Internal/Startup/ConsumerBackgroundServiceTests.cs create mode 100644 test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Internal/Startup/PipelineDelegateFactoryTests.cs create mode 100644 test/Libraries/System.Cloud.Messaging.Abstractions.Tests/System.Cloud.Messaging.Abstractions.Tests.csproj create mode 100644 test/Shared/Data.Validation/ExclusiveRangeAttributeTests.cs create mode 100644 test/Shared/Data.Validation/LengthAttributeTests.cs create mode 100644 test/Shared/Data.Validation/TimeSpanAttributeTests.cs create mode 100644 test/Shared/Debugger/DebuggerTest.cs create mode 100644 test/Shared/EmptyCollections/EmptyCollectionExtensionsTests.cs create mode 100644 test/Shared/EmptyCollections/EmptyReadOnlyListTests.cs create mode 100644 test/Shared/EmptyCollections/EmptyReadonlyDictionaryTests.cs create mode 100644 test/Shared/EmptyCollections/EmptyTests.cs create mode 100644 test/Shared/Memoization/MemoizeTests.cs create mode 100644 test/Shared/NumericExtensions/NumericExtensionsTests.cs create mode 100644 test/Shared/Pools/PoolTests.cs create mode 100644 test/Shared/Pools/TestResources/ITestClass.cs create mode 100644 test/Shared/Pools/TestResources/TestClass.cs create mode 100644 test/Shared/Pools/TestResources/TestDependency.cs create mode 100644 test/Shared/RentedSpan/RentedSpanTest.cs create mode 100644 test/Shared/Shared.Tests.csproj create mode 100644 test/Shared/Text.Formatting/CompositeFormatTests.cs create mode 100644 test/Shared/Text.Formatting/MakerTests.cs create mode 100644 test/Shared/Throw/DoubleTests.cs create mode 100644 test/Shared/Throw/IntegerTests.cs create mode 100644 test/Shared/Throw/LongTests.cs create mode 100644 test/Shared/Throw/ThrowTest.cs create mode 100644 test/TestUtilities/TestUtilities.csproj create mode 100644 test/TestUtilities/XUnit/ConditionalFactAttribute.cs create mode 100644 test/TestUtilities/XUnit/ConditionalFactDiscoverer.cs create mode 100644 test/TestUtilities/XUnit/ConditionalTheoryAttribute.cs create mode 100644 test/TestUtilities/XUnit/ConditionalTheoryDiscoverer.cs create mode 100644 test/TestUtilities/XUnit/ITestCondition.cs create mode 100644 test/TestUtilities/XUnit/OSSkipConditionAttribute.cs create mode 100644 test/TestUtilities/XUnit/OperatingSystems.cs create mode 100644 test/TestUtilities/XUnit/SkippedTestCase.cs create mode 100644 test/TestUtilities/XUnit/TestMethodExtensions.cs create mode 100644 test/TestUtilities/XUnit/WORKAROUND_SkippedDataRowTestCase.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/AcceptanceTest.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/AutoActivationExtensionsTests.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/AutoActivationHostedServiceTests.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/DependencyInjection.AutoActivation.Tests.csproj create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/AnotherFakeService.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/DifferentPocoClass.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/FactoryService.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/FakeOneMultipleService.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/FakeOpenGenericService.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/FakeService.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/IFactoryService.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/IFakeMultipleService.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/IFakeOpenGenericService.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/IFakeService.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/PocoClass.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/AnotherFakeServiceCounter.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IAnotherFakeServiceCounter.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IFactoryServiceCounter.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IFakeMultipleCounter.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IFakeOpenGenericCounter.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IFakeServiceCounter.cs create mode 100644 test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/InstanceCreatingCounter.cs create mode 100644 test/ToBeMoved/DependencyInjection.NamedService.Tests/DependencyInjection.NamedService.Tests.csproj create mode 100644 test/ToBeMoved/DependencyInjection.NamedService.Tests/ResolutionTests.cs create mode 100644 test/ToBeMoved/DependencyInjection.Pools.Tests/DependencyInjection.Pools.Tests.csproj create mode 100644 test/ToBeMoved/DependencyInjection.Pools.Tests/DependencyInjectionExtensionsTest.cs create mode 100644 test/ToBeMoved/DependencyInjection.Pools.Tests/TestResources/ITestClass.cs create mode 100644 test/ToBeMoved/DependencyInjection.Pools.Tests/TestResources/TestClass.cs create mode 100644 test/ToBeMoved/DependencyInjection.Pools.Tests/TestResources/TestDependency.cs create mode 100644 test/ToBeMoved/Directory.Build.props create mode 100644 test/ToBeMoved/Hosting.StartupInitialization.Tests/Hosting.StartupInitialization.Tests.csproj create mode 100644 test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/Database.cs create mode 100644 test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/DatabaseInitializer.cs create mode 100644 test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/DummyHostedService.cs create mode 100644 test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/TestResources.cs create mode 100644 test/ToBeMoved/Hosting.StartupInitialization.Tests/StartupInitializationAcceptanceTest.cs create mode 100644 test/ToBeMoved/Hosting.StartupInitialization.Tests/StartupInitializationExtensionsTest.cs create mode 100644 test/ToBeMoved/HttpClient.SocketHandling.Tests/HttpClient.SocketHandling.Tests.csproj create mode 100644 test/ToBeMoved/HttpClient.SocketHandling.Tests/HttpClientSocketHandlingExtensionsTest.cs create mode 100644 test/ToBeMoved/HttpClient.SocketHandling.Tests/Utils/HttpMessageHandlerBuilderHelpers.cs create mode 100644 test/ToBeRemoved/Directory.Build.props create mode 100644 test/ToBeRemoved/Options.ValidateOnStart.Tests/AcceptanceTest.cs create mode 100644 test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/AnnotatedOptions.cs create mode 100644 test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/AnotherNestedOptionsValidator.cs create mode 100644 test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/ComplexOptions.cs create mode 100644 test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/DepValidatorAttribute.cs create mode 100644 test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/FailingNestedOptionsValidator.cs create mode 100644 test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/FromAttribute.cs create mode 100644 test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/HostStartStopExtension.cs create mode 100644 test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/NestedOptions.cs create mode 100644 test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/NestedOptionsValidator.cs create mode 100644 test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/OptionsValidationModels.cs create mode 100644 test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/ThreeFailuresMultiErrorValidator.cs create mode 100644 test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/ZeroFailuresMultiErrorValidator.cs create mode 100644 test/ToBeRemoved/Options.ValidateOnStart.Tests/MultipleMessageValidatorTest.cs create mode 100644 test/ToBeRemoved/Options.ValidateOnStart.Tests/Options.ValidateOnStart.Tests.csproj create mode 100644 test/ToBeRemoved/Options.ValidateOnStart.Tests/OptionsBuilderExtensionsTests.cs create mode 100644 test/ToBeRemoved/Options.ValidateOnStart.Tests/OptionsValidatorExtensionsTest.cs create mode 100644 test/ToBeRemoved/Options.ValidateOnStart.Tests/ValidationHostedServiceTests.cs create mode 100644 testEnvironments.json diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000000..35b64e032d --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,30 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-coverage": { + "version": "17.6.9", + "commands": [ + "dotnet-coverage" + ] + }, + "dotnet-reportgenerator-globaltool": { + "version": "5.1.19", + "commands": [ + "reportgenerator" + ] + }, + "microsoft.visualstudio.slngen.tool": { + "version": "9.5.3", + "commands": [ + "slngen" + ] + }, + "PowerShell": { + "version": "7.3.3", + "commands": [ + "pwsh" + ] + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..5091a2e2c5 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,47 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet +{ + "name": "C# (.NET)", + "image": "mcr.microsoft.com/devcontainers/dotnet:0-6.0-focal", + "hostRequirements": { + "cpus": 4, + "memory": "8gb" + }, + "features": { + "ghcr.io/devcontainers/features/github-cli:1": {} + }, + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-dotnettools.csharp" + ], + "settings": { + // Loading projects on demand is better for larger codebases + "omnisharp.enableMsBuildLoadProjectsOnDemand": true, + "omnisharp.enableRoslynAnalyzers": true, + "omnisharp.enableEditorConfigSupport": true, + "omnisharp.enableAsyncCompletion": true, + "omnisharp.testRunSettings": "${containerWorkspaceFolder}/artifacts/obj/vscode/.runsettings" + } + } + }, + + // Use 'onCreateCommand' to run pre-build commands inside the codespace + "onCreateCommand": "${containerWorkspaceFolder}/.devcontainer/scripts/onCreateCommand.sh", + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "${containerWorkspaceFolder}/.devcontainer/scripts/postCreateCommand.sh", + + // Add the locally installed dotnet to the path to ensure that it is activated + // This allows developers to just use 'dotnet build' on the command-line, and the local dotnet version will be used. + "remoteEnv": { + "PATH": "${containerWorkspaceFolder}/.dotnet:${containerEnv:PATH}", + "DOTNET_MULTILEVEL_LOOKUP": "0" + }, + + // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/.devcontainer/scripts/onCreateCommand.sh b/.devcontainer/scripts/onCreateCommand.sh new file mode 100755 index 0000000000..e3bc732697 --- /dev/null +++ b/.devcontainer/scripts/onCreateCommand.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e + +# Dev Container can run out of disk space if we try to build all TFMs, so only build net8.0 +echo "net8.0" > .targetframeworks + +# Build the repo +./build.sh + +# save the commit hash of the currently built assemblies, so developers know which version was built +git rev-parse HEAD > ./artifacts/prebuild.sha \ No newline at end of file diff --git a/.devcontainer/scripts/postCreateCommand.sh b/.devcontainer/scripts/postCreateCommand.sh new file mode 100755 index 0000000000..3f6a7d0658 --- /dev/null +++ b/.devcontainer/scripts/postCreateCommand.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -e + +# reset the repo to the commit hash that was used to build the prebuilt Codespace +git reset --hard $(cat ./artifacts/prebuild.sha) \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..0944c8107f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +[*] + +indent_size = 4 +indent_style = space +tab_width = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +spelling_languages = en-us +spelling_checkable_types = strings,identifiers,comments +spelling_error_severity = information +spelling_exclusion_path = .\eng\spellchecking_exclusions.dic + +file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license. + +[*.{appxmanifest,axml,build,config,csproj,dbml,discomap,dtd,json,jsproj,lsproj,njsproj,nuspec,proj,props,resjson,resw,resx,StyleCop,targets,tasks,vbproj,yml,xml,xsd}] +indent_style = space +indent_size = 2 +tab_width = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..026fcf99b2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +# https://help.github.com/articles/dealing-with-line-endings/ +# Set default behavior to automatically normalize line endings. +* text=auto + +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain + +# Force bash scripts to always use lf line endings so that if a repo is accessed +# in Unix via a file share from Windows, the scripts will work. +*.in text eol=lf +*.sh text eol=lf + +# Likewise, force cmd and batch scripts to always use crlf +*.cmd text eol=crlf +*.bat text eol=crlf + +*.cs text=auto diff=csharp +*.vb text=auto +*.resx text=auto +*.c text=auto +*.cpp text=auto +*.cxx text=auto +*.h text=auto +*.hxx text=auto +*.py text=auto +*.rb text=auto +*.java text=auto +*.html text=auto +*.htm text=auto +*.css text=auto +*.scss text=auto +*.sass text=auto +*.less text=auto +*.js text=auto +*.lisp text=auto +*.clj text=auto +*.sql text=auto +*.php text=auto +*.lua text=auto +*.m text=auto +*.asm text=auto +*.erl text=auto +*.fs text=auto +*.fsx text=auto +*.hs text=auto + +*.csproj text=auto +*.vbproj text=auto +*.fsproj text=auto +*.dbproj text=auto +*.sln text=auto + +*.png filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.dll filter=lfs diff=lfs merge=lfs -text diff --git a/.github/ISSUE_TEMPLATE/01_bug_report.yml b/.github/ISSUE_TEMPLATE/01_bug_report.yml new file mode 100644 index 0000000000..81355693bd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_bug_report.yml @@ -0,0 +1,83 @@ +name: Bug Report +description: Create a report to help us improve +labels: ["untriaged", "bug"] +body: + - type: markdown + attributes: + value: | + We welcome bug reports! Please see our [contribution guidelines](https://github.com/dotnet/r9/blob/main/CONTRIBUTING.md#writing-a-good-bug-report) for more information on writing a good bug report. This template will help us gather the information we need to start the triage process. + - type: textarea + id: background + attributes: + label: Description + description: Please share a clear and concise description of the problem. + placeholder: Description + validations: + required: true + - type: textarea + id: repro-steps + attributes: + label: Reproduction Steps + description: | + Please include minimal steps to reproduce the problem, if possible. E.g.: the smallest possible code snippet; or a small project, with steps to run it. If possible include text as text rather than screenshots (so it shows up in searches). + placeholder: Minimal Reproduction + validations: + required: true + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + description: | + Provide a description of the expected behavior. + placeholder: Expected behavior + validations: + required: true + - type: textarea + id: actual-behavior + attributes: + label: Actual behavior + description: | + Provide a description of the actual behavior observed. If applicable please include any error messages, exception stacktraces or memory dumps. + placeholder: Actual behavior + validations: + required: true + - type: textarea + id: regression + attributes: + label: Regression? + description: | + Did this work in a previous build or release of .NET R9? If you can try a previous release or build to find out, that can help us narrow down the problem. If you don't know, that's OK. + placeholder: Regression? + validations: + required: false + - type: textarea + id: known-workarounds + attributes: + label: Known Workarounds + description: | + Please provide a description of any known workarounds. + placeholder: Known Workarounds + validations: + required: false + - type: textarea + id: configuration + attributes: + label: Configuration + description: | + Please provide more information on your .NET configuration: + * Which version of .NET is the code running on? E.g., '7.0 Preview1', or daily build number, use `dotnet --info`. + * What OS and version, and what distro, if applicable? + * Do you know whether it is specific to that configuration? + * If you're using Blazor, which web browser(s) do you see this issue in? + placeholder: Configuration + validations: + required: false + - type: textarea + id: other-info + attributes: + label: Other information + description: | + If you have an idea where the problem might lie, let us know that here. Please include any pointers to code, relevant changes, or related issues you know of. + placeholder: Other information + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/02_api_proposal.yml b/.github/ISSUE_TEMPLATE/02_api_proposal.yml new file mode 100644 index 0000000000..fdb27a4ece --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_api_proposal.yml @@ -0,0 +1,74 @@ +name: API Suggestion +description: Propose a change to the public API surface +title: "[API Proposal]: " +labels: ["untriaged", "api-suggestion"] +body: + - type: markdown + attributes: + value: | + We welcome API proposals! We have a process to evaluate the value and shape of new API. There is an overview of our process [here](https://github.com/dotnet/runtime/blob/main/docs/project/api-review-process.md). This template will help us gather the information we need to start the review process. + - type: textarea + id: background + attributes: + label: Background and motivation + description: Please describe the purpose and value of the new API here. + placeholder: Purpose + validations: + required: true + - type: textarea + id: api-proposal + attributes: + label: API Proposal + description: | + Please provide the specific public API signature diff that you are proposing. + + You may find the [Framework Design Guidelines](https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/framework-design-guidelines-digest.md) helpful. + placeholder: API declaration (no method bodies) + value: | + ```csharp + namespace System.Collections.Generic; + + public class MyFancyCollection : IEnumerable + { + public void Fancy(T item); + } + ``` + validations: + required: true + - type: textarea + id: api-usage + attributes: + label: API Usage + description: | + Please provide code examples that highlight how the proposed API additions are meant to be consumed. This will help suggest whether the API has the right shape to be functional, performant and usable. + placeholder: API usage + value: | + ```csharp + // Fancy the value + var c = new MyFancyCollection(); + c.Fancy(42); + + // Getting the values out + foreach (var v in c) + Console.WriteLine(v); + ``` + validations: + required: true + - type: textarea + id: alternative-designs + attributes: + label: Alternative Designs + description: | + Please provide alternative designs. This might not be APIs; for example instead of providing new APIs an option might be to change the behavior of an existing API. + placeholder: Alternative designs + validations: + required: false + - type: textarea + id: risks + attributes: + label: Risks + description: | + Please mention any risks that to your knowledge the API proposal might entail, such as breaking changes, performance regressions, etc. + placeholder: Risks + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/03_blank_issue.md b/.github/ISSUE_TEMPLATE/03_blank_issue.md new file mode 100644 index 0000000000..c19f5839eb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03_blank_issue.md @@ -0,0 +1,8 @@ +--- +name: Blank issue +about: Something that doesn't fit the other categories +title: '' +labels: 'untriaged' +assignees: '' + +--- diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..df00cb8690 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: true +contact_links: + - name: Issue with R9 extensions for Azure + url: https://github.com/azure/r9/issues/new/choose + about: Please open issues relating to R9 extensions for Azure in azure/r9. + - name: Issue with ASP.NET Core + url: https://github.com/dotnet/aspnetcore/issues/new/choose + about: Please open issues relating to ASP.NET Core in dotnet/aspnetcore. + - name: Issue with .NET runtime or core .NET libraries + url: https://github.com/dotnet/runtime/issues/new/choose + about: Please open issues with the .NET runtime or core in their repo + - name: Issue with .NET SDK + url: https://github.com/dotnet/sdk/issues/new/choose + about: Please open issues relating to the .NET SDK in dotnet/sdk. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..f9660a1e30 --- /dev/null +++ b/.gitignore @@ -0,0 +1,313 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# Tool Runtime Dir +# note: there is no trailing slash so if these are symlinks (which are seen as files, +# instead of directories), git will still ignore them. +.dotnet +.dotnet-mono +.dotnet-tools-global +.packages +.tools + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates +*.sln + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +!/eng/Release/ +[Rr]eleases/ +!/docs/releases/ +[Xx]64/ +[Xx]86/ +[Bb]uild/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ +.build/ +.vscode + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml + +# TODO: Un-comment the next line if you do not want to checkin +# your web deploy settings because they may include unencrypted +# passwords +#*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +packages/* +packages +# except build/, which is used as an MSBuild target. +!packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets +!src/Packages +!eng/Packages + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Microsoft Azure ApplicationInsights config file +ApplicationInsights.config + +# Windows Store app package directory +AppPackages/ +BundleArtifacts/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# LightSwitch generated files +GeneratedArtifacts/ +ModelManifest.xml + +# Paket dependency manager +.paket/paket.exe + +# FAKE - F# Make +.fake/ + +PubsubService/ServiceConfiguration.cscfg + +# Microsoft.Cloud.InstrumentationFramework.Metrics +*Ifx*.lib +*Ifx*.dll +*Tfx*.lib +*Tfx*.dll +*120.dll +TfxPerfCounter.man + +# Meta directories +.packages/*/* + +# Rider +.idea/ + +!src/MetaPackages/Sdk/build +!eng/Build +packages.lock.json +PackageInfo.json +ProjectInfo.json + +docs/_site +docs/obj +docs/refdocs/Microsoft.*.yml +docs/refdocs/Project.*.yml +docs/refdocs/toc.yml +docs/refdocs/.manifest +docs/packages/index.md +docs/binaries + +BenchmarkDotNet.Artifacts/* +!test/Extensions/Cryptography.Test/TestVectors/Files/*.rsp + +# local mutation testing +git-diff.txt +mutation-report.json +mutation-report.html + +*.sln +*.metaproj +**/launchSettings.json + +BenchmarkDotNet.artifacts/ + +/out.txt +/.targetframeworks + +/_TEST + +*.binlog + +/docs/releases/r9-packages.md \ No newline at end of file diff --git a/.spelling b/.spelling new file mode 100644 index 0000000000..14da61d41f --- /dev/null +++ b/.spelling @@ -0,0 +1,392 @@ +.NET +2.x +3.x +4.6x +4.7x +4.8x +5xx +6.a +6.c +6.f +AAD +AAD +abstractions +ad +Agentless +Aggregator +aggregators +AKS +AKV +AKVs +Alexey +analyzer +Analyzers +Andrey +API +APIs +AppInsights +approvers +ASP.NET +ASP.NET. +async +ATM +Authenticode +Autofac +awaitable +AzSecPack +Azure +backend +Backoff +base64 +Base64 +Behavior +Belenko +Bezdolny +Binskim +Blackforest +Blazek +bloomfilter +bloomfilters +boolean +bootstrapper +buildout +Brownbag +Brownbags +byte +C# +Caching +CAE +callee +callouts +Callouts +CDPx +Chainable +checkboxes +checkin +cmdlets +cmdlet +CNAME +CNAME +CNAMEs +CNAMEs +codebase +codebases +COGs +combinators +composability +composable +composable +config +Config +CoreAuth +CoreAuth +correlators +cpu +CPU +cross-cutting +cross-cutting +Crypto +cscfg +customizable +Cv2 +DCs +decorrelated +Deepak +Deliverables +deserialization +deserialize +deserialized +dev +DevOps +DGrep +discoverability +dockerize +DoD +dogfood +Dogfood +dont's +dotnet +dSMS +e.g. +ECS +ECS +ejuvenate +encodings +eng.ms +enricher +Enricher +enrichers +Enrichers +enum +enums +Ev2 +executables +external_bp +failover +filename +FlatBuffer +FlatBuffers +FlatSharp +flighting +FluentD +formatters +fulfill +FxCop +Gallatin +Gameday +Gamedays +Gantt +Generators +geneva +geo +geos +getters +github +Golang-ci +gRPC +GuestUser +hoc +hotfix +hotfixes +how-tos +http +HTTP +HttpClient +https +i.e. +IC3 +IDWeb +ifx +Ihar +In-proc +indempotent +InfoSec +InfoSec +initializer +initializers +Initializers +injectable +injectable +inlining +IntelliSense +interoperate +intrinsic +intrinsics +IoC +iOS +JSON +JSON-encoded +Juraj +Klauco +Kubernetes +Kusto +learnings +lifecycle +lightbulb +LinkedIn +Liron +livesite +Logging +lookups +Malkevich +Marcano +Matej +McAllister +MdmMetricReporter +memoization +memoize +memoized +metadata +microservice +microservices +Microsoft +microsoft.com +middleware +middlewares +Miri +miscomputed +Mise +Mise +MISE +MISE +mitigation +mitigations +Mitrache +Modi +Mooncake +MSBuild +MSGraph +MSGraph +MSI +MSIs +mutator +MyAccess +naïve +namespace +namespaces +natively +natively +Newtonsoft +NLog +Noskov +ns +NuGet +NuGets +nullability +nullable +onboard +onboarded +onboarding +onboards +OneBranch +OneCert +OneDrive +OneObservability +OpenTelemetry +OSS +OTel +OTEPs +PaaSv1 +parallelization +parameterless +pdf +Perfmon +PerfPanel +PerfView +pluggability +POCO +POCOs +Polly +Polly.NET +Postconfiguration +PowerShell +ppe +Pranav +pre +pre-aggregated +pre-aggregation +pre-built +pre-release +pre-requisite +preaggregate +preaggregates +preallocated +Preconfiguration +preconfigured +preformatted +preimage +prepending +prober +programmatically +Protobuf +Protobuf-net +PubSub +QoS +quantiles +R9 +R9_RELEASE +R9A000 +R9A001 +R9A002 +R9G000 +R9G001 +R9G002 +R9G003 +R9G004 +R9G005 +R9G006 +R9G007 +R9G008 +R9G009 +R9G010 +r9support +Ramati +ramping +RDFE +reconfigure +Redis +reimplementation +remediations +repo +repos +Ring0 +Ring1 +rollout +rollouts +Rousos +routable +runtime +runtimes +Saares +SAL +schemas +Scortzario +sdf +SDK +SDKs +seatbelt +serializer +serializers +Serilog +ServiceTree +Sev3 +SharePoint +Shearar +Shiproom +Shullo +Simmy +SimpleInjector +Skowronski +Skype +Skypetoken +SLA +SLAs +snappable +standalone +stateful +struct +structs +Stryker +Stryker.NET +substring +SumObserver +suppressions +Tal +teams-service-mwt-fae-data-service +timestamp +Tomka +toolchain +toolchains +toolset +Trouter +TRv2 +uncheck +unmanaged +unredacted +uri +URI +v1.3 +v1.4 +v1.6 +validator +validators +versionable +versioned +versioning +Versioning +Virtualization +virtualization +VM +WebAuth +WebHost +WebRTC +whitespace +Wiki +wildcard +Win32 +Workstream +Workstreams +xmls +xUnit +xxHash +Zipkin diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..f9ba8cf65f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,9 @@ +# Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..617ec70245 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,191 @@ +# Contributing Guide + +> :warning: Please note, this document is a subset of [Contributing to .NET Runtime][net-contributing], make sure to read it first. + +* [Reporting Issues](#reporting-issues) + + [Identify Where to Report](#identify-where-to-report) + + [Finding Existing Issues](#finding-existing-issues) + + [Writing a Good API Proposal](#writing-a-good-api-proposal) + + [Writing a Good Bug Report](#writing-a-good-bug-report) + - [Why are Minimal Reproductions Important?](#why-are-minimal-reproductions-important) + - [Are Minimal Reproductions Required?](#are-minimal-reproductions-required) + - [How to Create a Minimal Reproduction](#how-to-create-a-minimal-reproduction) +* [Contributing Changes](#contributing-changes) + + [DOs and DON'Ts](#dos-and-donts) + + [Breaking Changes](#breaking-changes) + + [Suggested Workflow](#suggested-workflow) + + [Commit Messages](#commit-messages) + + [PR Feedback](#pr-feedback) + + [Help Wanted (Up for Grabs)](#help-wanted-up-for-grabs) + + [Contributor License Agreement](#contributor-license-agreement) + +You can contribute to R9 with issues, pull-requests, and general reviews of both issues and pull-requests. Simply filing issues for problems you encounter is a great way to contribute. Contributing implementations is greatly appreciated. + +## Reporting Issues + +We always welcome bug reports, API proposals and overall feedback. Here are a few tips on how you can make reporting your issue as effective as possible. + +### Identify Where to Report + +The .NET codebase is distributed across multiple repositories in the [.NET organization](https://github.com/dotnet). Depending on the feedback you might want to file the issue on a different repo. Here are a few common repos: + +* [dotnet/runtime](https://github.com/dotnet/runtime) .NET runtime, libraries and shared host installers. +* [dotnet/aspnetcore](https://github.com/dotnet/aspnetcore) ASP.NET Core. +* [azure/r9](https://github.com/azure/r9) R9 extensions for Azure. + +### Finding Existing Issues + +Before filing a new issue, please search our [open issues](https://github.com/dotnet/runtime/issues) to check if it already exists. + +If you do find an existing issue, please include your own feedback in the discussion. Do consider upvoting (👍 reaction) the original post, as this helps us prioritize popular issues in our backlog. + +### Writing a Good API Proposal + +Please review our [API review process](https://github.com/dotnet/runtime/blob/main/docs/project/api-review-process.md) documents for guidelines on how to submit an API review. When ready to submit a proposal, please use the [API Suggestion issue template](https://github.com/dotnet/runtime/issues/new?assignees=&labels=api-suggestion&template=02_api_proposal.yml&title=%5BAPI+Proposal%5D%3A+). + +### Writing a Good Bug Report + +Good bug reports make it easier for maintainers to verify and root cause the underlying problem. The better a bug report, the faster the problem will be resolved. Ideally, a bug report should contain the following information: + +* A high-level description of the problem. +* A _minimal reproduction_, i.e. the smallest size of code/configuration required to reproduce the wrong behavior. +* A description of the _expected behavior_, contrasted with the _actual behavior_ observed. +* Information on the environment: OS/distro, CPU arch, SDK version, etc. +* Additional information, e.g. is it a regression from previous versions? are there any known workarounds? + +When ready to submit a bug report, please use the [Bug Report issue template](https://github.com/dotnet/runtime/issues/new?assignees=&labels=&template=01_bug_report.yml). + +#### Why are Minimal Reproductions Important? + +A reproduction lets maintainers verify the presence of a bug, and diagnose the issue using a debugger. A _minimal_ reproduction is the smallest possible console application demonstrating that bug. Minimal reproductions are generally preferable since they: + +1. Focus debugging efforts on a simple code snippet, +2. Ensure that the problem is not caused by unrelated dependencies/configuration, +3. Avoid the need to share production codebases. + +#### Are Minimal Reproductions Required? + +In certain cases, creating a minimal reproduction might not be practical (e.g. due to nondeterministic factors, external dependencies). In such cases you would be asked to provide as much information as possible, for example by sharing a memory dump of the failing application. If maintainers are unable to root cause the problem, they might still close the issue as not actionable. While not required, minimal reproductions are strongly encouraged and will significantly improve the chances of your issue being prioritized and fixed by the maintainers. + +#### How to Create a Minimal Reproduction + +The best way to create a minimal reproduction is gradually removing code and dependencies from a reproducing app, until the problem no longer occurs. A good minimal reproduction: + +* Excludes all unnecessary types, methods, code blocks, source files, nuget dependencies and project configurations. +* Contains documentation or code comments illustrating expected vs actual behavior. +* If possible, avoids performing any unneeded IO or system calls. For example, can the ASP.NET based reproduction be converted to a plain old console app? + +## Contributing Changes + +Project maintainers will merge changes that improve the product significantly. + +The [Pull Request Guide][pr-guide] and [Copyright][copyright-guide] docs define additional guidance. + +### DOs and DON'Ts + +Please do: + +* **DO** follow our [coding style][coding-style] (C# code-specific)
+ We strive to wrap the lines around 120 mark, and it's acceptable to stretch to no more than 150 chars (with some exceptions being URLs). [EditorGuidelines VS extension](https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelines) makes it easier to visualise (see https://github.com/dotnet/winforms/pull/4836). +* **DO** give priority to the current style of the project or file you're changing even if it diverges from the general guidelines. +* **DO** include tests when adding new features. When fixing bugs, start with + adding a test that highlights how the current behavior is broken. +* **DO** keep the discussions focused. When a new or related topic comes up + it's often better to create new issue than to side track the discussion. +* **DO** blog and tweet (or whatever) about your contributions, frequently! + +Please do not: + +* **DON'T** make PRs for style changes. +* **DON'T** surprise us with big pull requests. Instead, file an issue and start + a discussion so we can agree on a direction before you invest a large amount + of time. +* **DON'T** commit code that you didn't write. If you find code that you think is a good fit to add to .NET Core, file an issue and start a discussion before proceeding. +* **DON'T** submit PRs that alter licensing related files or headers. If you believe there's a problem with them, file an issue and we'll be happy to discuss it. +* **DON'T** add API additions without filing an issue and discussing with us first. See [API Review Process][api-review-process]. + +### Breaking Changes + +Contributions must maintain [API signature][breaking-changes-public-contract] and behavioral compatibility. Contributions that include [breaking changes][breaking-changes] will be rejected. Please file an issue to discuss your idea or change if you believe that it may affect managed code compatibility. + +### Suggested Workflow + +We use and recommend the following workflow: + +1. Create an issue for your work. + - You can skip this step for trivial changes. + - Reuse an existing issue on the topic, if there is one. + - Get agreement from the team and the community that your proposed change is a good one. + - If your change adds a new API, follow the [API Review Process][api-review-process]. + - Clearly state that you are going to take on implementing it, if that's the case. You can request that the issue be assigned to you. Note: The issue filer and the implementer don't have to be the same person. +2. Create a personal fork of the repository on GitHub (if you don't already have one). +3. In your fork, create a branch off of main (`git checkout -b mybranch`). + - Name the branch so that it clearly communicates your intentions, such as issue-123 or githubhandle-issue. + - Branches are useful since they isolate your changes from incoming changes from upstream. They also enable you to create multiple PRs from the same fork. +4. Make and commit your changes to your branch. + - [Workflow Instructions](docs/building.md) explains how to build and test. + - Please follow our [Commit Messages](#commit-messages) guidance. +5. Add new tests corresponding to your change, if applicable. +6. Build the repository with your changes. + - Make sure that the builds are clean. + - Make sure that the tests are all passing, including your new tests. +7. Create a pull request (PR) against the dotnet/runtime repository's **main** branch. + - State in the description what issue or improvement your change is addressing. + - Check if all the Continuous Integration checks are passing. +8. Wait for feedback or approval of your changes from the area owners. + - Details about the pull request [review procedure](docs/pr-guide.md). +9. When area owners have signed off, and all checks are green, your PR will be merged. + - The next official build will automatically include your change. + - You can delete the branch you used for making the change. + +### Commit Messages + +Please format commit messages as follows (based on [A Note About Git Commit Messages][note-about-git-commit-messages]). Also, use the [GitHub keywords][github-keywords]: + + ``` + Summarize change in 50 characters or less + + Fixes #42 + + Provide more detail after the first line. Leave one blank line below the + summary and wrap all lines at 72 characters or less. + + If the change fixes an issue, leave another blank line after the final + paragraph and indicate which issue is fixed in the specific format + below. + ``` + +Also do your best to factor commits appropriately, not too large with unrelated things in the same commit, and not too small with the same small change applied N times in N different commits. + +### PR Feedback + +Project maintainers and community members will provide feedback on your change. Community feedback is highly valued. You will often see the absence of team feedback if the community has already provided good review feedback. + +One or more project maintainers members will review every PR prior to merge. They will often reply with "LGTM, modulo comments". That means that the PR will be merged once the feedback is resolved. "LGTM" == "looks good to me". + +There are lots of thoughts and [approaches](https://github.com/antlr/antlr4-cpp/blob/master/CONTRIBUTING.md#emoji) for how to efficiently discuss changes. It is best to be clear and explicit with your feedback. Please be patient with people who might not understand the finer details about your approach to feedback. + +### Help Wanted (Up for Grabs) + +The team marks the most straightforward issues as [help wanted](https://github.com/dotnet/r9/labels/help%20wanted). This set of issues is the place to start if you are interested in contributing but new to the codebase. + +### Contributor License Agreement + +You must sign a [.NET Foundation Contribution License Agreement (CLA)](https://cla.dotnetfoundation.org) before your PR will be merged. This is a one-time requirement for projects in the .NET Foundation. You can read more about [Contribution License Agreements (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) on Wikipedia. + +The agreement: [net-foundation-contribution-license-agreement.pdf](https://github.com/dotnet/home/blob/master/guidance/net-foundation-contribution-license-agreement.pdf) + +You don't have to do this up-front. You can simply clone, fork, and submit your pull-request as usual. When your pull-request is created, it is classified by a CLA bot. If the change is trivial (for example, you just fixed a typo), then the PR is labelled with `cla-not-required`. Otherwise it's classified as `cla-required`. Once you signed a CLA, the current and all future pull-requests will be labelled as `cla-signed`. + + +[comment]: <> (URI Links) + +[api-review-process]: https://github.com/dotnet/runtime/blob/main/docs/project/api-review-process.md +[breaking-changes]: https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/breaking-changes.md +[breaking-changes-public-contract]: https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/breaking-changes.md#bucket-1-public-contract +[coding-style]: https://github.com/dotnet/runtime/blob/master/docs/coding-guidelines/coding-style.md +[copyright-guide]: https://github.com/dotnet/runtime/blob/main/docs/project/copyright.md +[github-keywords]: https://docs.github.com/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue +[net-contributing]: https://github.com/dotnet/runtime/blob/main/CONTRIBUTING.md +[note-about-git-commit-messages]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html +[pr-guide]: https://github.com/dotnet/runtime/blob/main/docs/pr-guide.md diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000000..0cadbbf21f --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,107 @@ + + + + + + + net + 8 + 0 + $(TargetFrameworkMajorVersion).$(TargetFrameworkMinorVersion) + + + $(TargetFrameworkName)$(TargetFrameworkVersion) + + $(LatestTargetFramework);net6.0 + $(SupportedNetCoreTargetFrameworks);netcoreapp3.1 + + + net6.0 + + + + $([System.IO.File]::ReadAllText('$(MSBuildThisFileDirectory)\.targetframeworks').Trim()) + $(LatestTargetFramework) + $(SupportedNetCoreTargetFrameworks);net6.0 + $(SupportedNetCoreTargetFrameworks) + $(NetCoreTargetFrameworks);netcoreapp3.1 + ;net462 + + + + + false + preview + enable + disable + true + portable + true + true + $(MSBuildThisFileDirectory)\eng\Common.runsettings + true + true + debug-determinism + + + true + + false + false + + + true + + + false + + + true + + + true + true + + + + + win + false + true + + + + + None + + + + + $(MicrosoftCodeAnalysisCSharpVersion) + + + + + + + + + + + + + + + diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000000..452a837e05 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,84 @@ + + + + + + + + + + + + + $(MSBuildWarningsAsMessages);NETSDK1138;MSB3270 + 5 + + + false + + + $(NoWarn);IL2026;IL2087;IL2067;IL2075;IL2091;IL2072;IL2090;CA1825;IL2070;IL2098;IL2057 + + + $(NoWarn);AD0001 + + + $(NoWarn);R9A029 + + + $(NoWarn);R9A049 + + + $(NoWarn);NU5104 + + $(NoWarn);SA1600;SA0001 + + + $(NoWarn);CA1062 + + + + + + $(NoWarn);ASP0019 + + + + + $(NoWarn);RS1024 + + + + + true + + + + + + + + + + <_Parameter1 Condition="'$(SignArtifacts)' == 'true' ">DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7 + <_Parameter1 Condition="'$(SignArtifacts)' != 'true' ">DynamicProxyGenAssembly2 + + + + + + + + + + + + + + <_BlameArgs>--blame --blame-crash --blame-crash-dump-type full --blame-hang --blame-hang-dump-type full --blame-hang-timeout 6m + + + $(TestRunnerAdditionalArguments) $(_BlameArgs) + + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000000..c951662e54 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..793ac5e0f2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) .NET Foundation. All rights reserved. + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 0000000000..2f02faaae9 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000000..7c90b948ea --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# Enriched Capabilities + +This repository contains a suite of libraries that provide facilities commonly needed when creating production-ready applications. Initially developed to support high-scale and high-availability services within Microsoft, such as Microsoft Teams, these libraries deliver functionality that can help make applications more efficient, more robust, and more manageable. + +The code in this repo is preliminary and will be released in stable form with .NET 8 + +The major functional areas this repo addresses are: +- Compliance: Mechanisms to help manage application data according to privacy regulations and policies, which includes a data annotation framework, supporting analyzers, audit report generation, and telemetry redaction. +- Diagnostics: Provides a set of APIs that can be used to gather and report diagnostic information about the health of a service. +- Contextual Options: Extends the .NET Options model to enable experimentations in production. +- Resilience: Builds on top of the popular Polly library to provide sophisticated resilience pipelines along with support for chaos engineering to make applications robust to transient errors. +- Telemetry: Sophisticated telemetry facilities provide enhanced logging, metering, tracing, and latency measuring functionality. +- AspNetCore extensions: Provides different middlewares and extensions that can be used to build high-performance and high-availability ASP.NET Core services. +- Cloud Abstractions: A growing set of abstractions representing common cloud-native service types, making it possible to write applications that can work across multiple cloud providers with relative ease. +- Static Analysis: Provides a set of Roslyn analyzers that can be used to enforce best practices and coding standards. +- Testing: Dramatically simplifies testing around common .NET abstractions such as ILogger and the TimeProvider. + +[![Build Status](https://dev.azure.com/dnceng/internal/_apis/build/status/r9/dotnet-r9?branchName=main)](https://dev.azure.com/dnceng/internal/_build/latest?definitionId=1223&branchName=main) +[![Help Wanted](https://img.shields.io/github/issues/dotnet/r9/help%20wanted?style=flat-square&color=%232EA043&label=help%20wanted)](https://github.com/dotnet/r9/labels/help%20wanted) +[![Discord](https://img.shields.io/discord/732297728826277939?style=flat-square&label=Discord&logo=discord&logoColor=white&color=7289DA)](https://aka.ms/dotnet-discord) + +## How can I contribute? + +We welcome contributions! Many people all over the world have helped make this project better. + +* [Contributing](CONTRIBUTING.md) explains what kinds of contributions we welcome +* [Build instructions](docs/building.md) explains how to build and test + +## Reporting security issues and security bugs + +Security issues and bugs should be reported privately, via email, to the Microsoft Security Response Center (MSRC) . You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Further information, including the MSRC PGP key, can be found in the [Security TechCenter](https://www.microsoft.com/msrc/faqs-report-an-issue). You can also find these instructions in this repo's [Security doc](SECURITY.md). + +Also see info about related [Microsoft .NET Core and ASP.NET Core Bug Bounty Program](https://www.microsoft.com/msrc/bounty-dot-net-core). + +## Useful Links + +* [.NET Core source index](https://source.dot.net) / [.NET Framework source index](https://referencesource.microsoft.com) +* [API Reference docs](https://docs.microsoft.com/dotnet/api) +* [.NET API Catalog](https://apisof.net) (incl. APIs from daily builds and API usage info) +* [API docs writing guidelines](https://github.com/dotnet/dotnet-api-docs/wiki) - useful when writing /// comments +* [.NET Discord Server](https://aka.ms/dotnet-discord) - a place to discuss the development of .NET and its ecosystem + +## .NET Foundation + +This project is a [.NET Foundation](https://www.dotnetfoundation.org/projects) project. + +There are many .NET related projects on GitHub. + +* [.NET home repo](https://github.com/Microsoft/dotnet) - links to 100s of .NET projects, from Microsoft and the community. +* [ASP.NET Core home](https://docs.microsoft.com/aspnet/core) - the best place to start learning about ASP.NET Core. + +This project has adopted the code of conduct defined by the [Contributor Covenant](https://contributor-covenant.org) to clarify expected behavior in our community. For more information, see the [.NET Foundation Code of Conduct](https://www.dotnetfoundation.org/code-of-conduct). + +General .NET OSS discussions: [.NET Foundation Discussions](https://github.com/dotnet-foundation/Home/discussions) + +## License + +.NET (including the runtime repo) is licensed under the [MIT](LICENSE.TXT) license. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..e2788710dd --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). + + diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000000..c5f5cb8eee --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,286 @@ +# Setting batch to true, triggers one build at a time. +# if there is a push while a build in progress, it will wait, +# until the running build finishes, and produce a build with all the changes +# that happened during the last build. +trigger: + batch: true + branches: + include: + - main + - release/* + paths: + include: + - '*' + exclude: + - eng/Version.Details.xml + - .github/* + - docs/* + - CODE_OF_CONDUCT.md + - CONTRIBUTING.md + - README.md + - SECURITY.md + - LICENSE.TXT + - PATENTS.TXT + - THIRD-PARTY-NOTICES.TXT + +pr: + branches: + include: + - main + - release/* + paths: + include: + - '*' + exclude: + - eng/Version.Details.xml + - .github/* + - docs/* + - CODE_OF_CONDUCT.md + - CONTRIBUTING.md + - README.md + - SECURITY.md + - LICENSE.TXT + - PATENTS.TXT + - THIRD-PARTY-NOTICES.TXT + +variables: + - name: _TeamName + value: dotnet-r9 + - name: NativeToolsOnMachine + value: true + - name: DOTNET_SKIP_FIRST_TIME_EXPERIENCE + value: true + + # TEMP until all the x-cutting refactoring is complete + - name: SkipCodeCoverage + value: true + + - name: runAsPublic + value: ${{ eq(variables['System.TeamProject'], 'public') }} + - name: _BuildConfig + value: Release + - name: isOfficialBuild + value: ${{ and(ne(variables['runAsPublic'], 'true'), notin(variables['Build.Reason'], 'PullRequest')) }} + - name: IsDeltaBuild + value: ${{ eq(variables['Build.Reason'], 'PullRequest') }} + - name: Build.Arcade.ArtifactsPath + value: $(Build.SourcesDirectory)/artifacts/ + - name: Build.Arcade.LogsPath + value: $(Build.Arcade.ArtifactsPath)log/$(_BuildConfig)/ + - name: Build.Arcade.TestResultsPath + value: $(Build.Arcade.ArtifactsPath)TestResults/$(_BuildConfig)/ + + # For full build we can do a shallow build, for a delta build we need the full history. + - ${{ if eq(variables['IsDeltaBuild'], 'true') }}: + - name: _FetchDepth + value: 0 + - ${{ else }}: + - name: _FetchDepth + value: 1 + + - ${{ if or(startswith(variables['Build.SourceBranch'], 'refs/heads/release/'), startswith(variables['Build.SourceBranch'], 'refs/heads/internal/release/'), eq(variables['Build.Reason'], 'Manual')) }}: + - name: PostBuildSign + value: false + - ${{ else }}: + - name: PostBuildSign + value: true + + # Produce test-signed build for PR and Public builds + - ${{ if or(eq(variables['runAsPublic'], 'true'), eq(variables['Build.Reason'], 'PullRequest')) }}: + # needed for darc (dependency flow) publishing + - name: _PublishArgs + value: '' + - name: _OfficialBuildIdArgs + value: '' + # needed for signing + - name: _SignType + value: test + - name: _SignArgs + value: '' + - name: _Sign + value: false + + # Set up non-PR build from internal project + - ${{ if and(ne(variables['runAsPublic'], 'true'), ne(variables['Build.Reason'], 'PullRequest')) }}: + # needed for darc (dependency flow) publishing + - name: _PublishArgs + value: >- + /p:DotNetPublishUsingPipelines=true + - name: _OfficialBuildIdArgs + value: /p:OfficialBuildId=$(BUILD.BUILDNUMBER) + # needed for signing + - name: _SignType + value: real + - name: _SignArgs + value: /p:DotNetSignType=$(_SignType) /p:TeamName=$(_TeamName) /p:Sign=$(_Sign) /p:DotNetPublishUsingPipelines=true + - name: _Sign + value: true + +stages: +- stage: build + displayName: Build + variables: + - template: /eng/common/templates/variables/pool-providers.yml + jobs: + - template: /eng/common/templates/jobs/jobs.yml + parameters: + enableMicrobuild: true + enableTelemetry: true + enableSourceIndex: false + runAsPublic: ${{ variables['runAsPublic'] }} + # Publish build logs + enablePublishBuildArtifacts: true + # Publish test logs + enablePublishTestResults: true + # Publish NuGet packages using v3 + # https://github.com/dotnet/arcade/blob/main/Documentation/CorePackages/Publishing.md#basic-onboarding-scenario-for-new-repositories-to-the-current-publishing-version-v3 + enablePublishUsingPipelines: true + enablePublishBuildAssets: true + workspace: + clean: all + + jobs: + - job: Windows + timeoutInMinutes: 180 + testResultsFormat: VSTest + + pool: + ${{ if eq(variables['runAsPublic'], 'true') }}: + name: $(DncEngPublicBuildPool) + demands: ImageOverride -equals windows.vs2022preview.amd64.open + # Non-public (i.e., official builds) + ${{ else }}: + name: $(DncEngInternalBuildPool) + demands: ImageOverride -equals windows.vs2022preview.amd64 + + variables: + - _buildScript: $(Build.SourcesDirectory)/build.cmd -ci -NativeToolsOnMachine + + preSteps: + - checkout: self + clean: true + persistCredentials: true + fetchDepth: $(_FetchDepth) + + steps: + - template: \eng\pipelines\templates\BuildAndTest.yml + parameters: + buildScript: $(_buildScript) + buildConfig: $(_BuildConfig) + repoLogPath: $(Build.Arcade.LogsPath) + repoTestResultsPath: $(Build.Arcade.TestResultsPath) + skipCodeCoverage: ${{ eq(variables['SkipCodeCoverage'], 'true') }} + isDeltaBuild: $(IsDeltaBuild) + isWindows: true + warnAsError: 0 + + - job: Ubuntu + timeoutInMinutes: 180 + testResultsFormat: VSTest + + pool: + ${{ if eq(variables['runAsPublic'], 'true') }}: + name: $(DncEngPublicBuildPool) + demands: ImageOverride -equals build.ubuntu.2004.amd64.open + # Non-public (i.e., official builds) + ${{ else }}: + name: $(DncEngInternalBuildPool) + demands: ImageOverride -equals build.ubuntu.2004.amd64 + + variables: + - _buildScript: $(Build.SourcesDirectory)/build.sh --ci + + preSteps: + - checkout: self + clean: true + persistCredentials: true + fetchDepth: $(_FetchDepth) + + steps: + - template: \eng\pipelines\templates\BuildAndTest.yml + parameters: + buildScript: $(_buildScript) + buildConfig: $(_BuildConfig) + repoLogPath: $(Build.Arcade.LogsPath) + repoTestResultsPath: $(Build.Arcade.TestResultsPath) + skipCodeCoverage: true + isDeltaBuild: $(IsDeltaBuild) + isWindows: false + warnAsError: 0 + + +- stage: correctness + displayName: Correctness + dependsOn: [] + variables: + - template: /eng/common/templates/variables/pool-providers.yml + jobs: + - template: /eng/common/templates/jobs/jobs.yml + parameters: + enableMicrobuild: true + enableTelemetry: true + runAsPublic: ${{ variables['runAsPublic'] }} + workspace: + clean: all + + jobs: + - job: WarningsCheck + timeoutInMinutes: 180 + + pool: + ${{ if eq(variables['runAsPublic'], 'true') }}: + name: $(DncEngPublicBuildPool) + demands: ImageOverride -equals build.ubuntu.2004.amd64.open + # Non-public (i.e., official builds) + ${{ else }}: + name: $(DncEngInternalBuildPool) + demands: ImageOverride -equals build.ubuntu.2004.amd64 + + variables: + - _buildScript: $(Build.SourcesDirectory)/build.sh --ci + + preSteps: + - checkout: self + clean: true + persistCredentials: true + fetchDepth: $(_FetchDepth) + + steps: + - template: \eng\pipelines\templates\BuildAndTest.yml + parameters: + buildScript: $(_buildScript) + buildConfig: $(_BuildConfig) + repoLogPath: $(Build.Arcade.LogsPath) + repoTestResultsPath: $(Build.Arcade.TestResultsPath) + skipCodeCoverage: true + skipTests: true + isDeltaBuild: $(IsDeltaBuild) + isWindows: false + + +# Publish and validation steps. Only run in official builds +- ${{ if and(ne(variables['runAsPublic'], 'true'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - template: /eng/common/templates/post-build/post-build.yml + parameters: + validateDependsOn: + - build + - correctness + publishingInfraVersion: 3 + enableSymbolValidation: false + enableSigningValidation: false + enableNugetValidation: false + enableSourceLinkValidation: false + # these param values come from the DotNet-Winforms-SDLValidation-Params azdo variable group + SDLValidationParameters: + enable: false + params: ' -SourceToolsList $(_TsaSourceToolsList) + -TsaInstanceURL $(_TsaInstanceURL) + -TsaProjectName $(_TsaProjectName) + -TsaNotificationEmail $(_TsaNotificationEmail) + -TsaCodebaseAdmin $(_TsaCodebaseAdmin) + -TsaBugAreaPath $(_TsaBugAreaPath) + -TsaIterationPath $(_TsaIterationPath) + -TsaRepositoryName $(_TsaRepositoryName) + -TsaCodebaseName $(_TsaCodebaseName) + -TsaOnboard $(_TsaOnboard) + -TsaPublish $(_TsaPublish)' diff --git a/bench/.editorconfig b/bench/.editorconfig new file mode 100644 index 0000000000..41aa151e4b --- /dev/null +++ b/bench/.editorconfig @@ -0,0 +1,7198 @@ +# Created by the R9 diagnostic config generator +# Generated : 2023-02-20 04:52:13Z +# Attributes: general, performance +# Analyzers : ILLink.RoslynAnalyzer, Internal.Analyzers, Microsoft.AspNetCore.App.Analyzers, Microsoft.AspNetCore.Components.Analyzers, Microsoft.CodeAnalysis.CodeStyle, Microsoft.CodeAnalysis.CSharp.CodeStyle, Microsoft.CodeAnalysis.CSharp.NetAnalyzers, Microsoft.CodeAnalysis.NetAnalyzers, Microsoft.CPR.Standard.Analyzers.Basic.R3, Microsoft.R9.Analyzers.Roslyn4.0, Microsoft.VisualStudio.Threading.Analyzers, Microsoft.VisualStudio.Threading.Analyzers.CSharp, SonarAnalyzer.CSharp, StyleCop.Analyzers + +[*.cs] + +# Title : Do not use model binding attributes with route handlers +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0003.severity = warning + +# Title : Do not use action results with route handlers +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0004.severity = warning + +# Title : Do not place attribute on method called by route handler lambda +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0005.severity = warning + +# Title : Do not use non-literal sequence numbers +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0006.severity = warning + +# Title : Route parameter and argument optionality is mismatched +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0007.severity = warning + +# Title : Do not use ConfigureWebHost with WebApplicationBuilder.Host +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0008.severity = error + +# Title : Do not use Configure with WebApplicationBuilder.WebHost +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0009.severity = error + +# Title : Do not use UseStartup with WebApplicationBuilder.WebHost +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0010.severity = error + +# Title : Suggest using builder.Logging over Host.ConfigureLogging or WebHost.ConfigureLogging +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0011.severity = warning + +# Title : Suggest using builder.Services over Host.ConfigureServices or WebHost.ConfigureServices +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0012.severity = warning + +# Title : Suggest switching from using Configure methods to WebApplicationBuilder.Configuration +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0013.severity = warning + +# Title : Suggest using top level route registrations +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0014.severity = warning + +# Title : Component parameter should have public setters. +# Category : Encapsulation +dotnet_diagnostic.BL0001.severity = error + +# Title : Component has multiple CaptureUnmatchedValues parameters +# Category : Usage +dotnet_diagnostic.BL0002.severity = warning + +# Title : Component parameter with CaptureUnmatchedValues has the wrong type +# Category : Usage +dotnet_diagnostic.BL0003.severity = warning + +# Title : Component parameter should be public. +# Category : Encapsulation +dotnet_diagnostic.BL0004.severity = error + +# Title : Component parameter should not be set outside of its component. +# Category : Usage +dotnet_diagnostic.BL0005.severity = warning + +# Title : Do not use RenderTree types +# Category : Usage +dotnet_diagnostic.BL0006.severity = warning + +# Title : Component parameters should be auto properties +# Category : Usage +dotnet_diagnostic.BL0007.severity = warning + +# Title : Do not declare static members on generic types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1000 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1000.severity = none + +# Title : Types that own disposable fields should be disposable +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1001 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1001.severity = warning + +# Title : Do not expose generic lists +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1002 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1002.severity = none + +# Title : Use generic event handler instances +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1003 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1003.severity = none + +# Title : Avoid excessive parameters on generic types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1005 +# Tags : PortedFromFxCop, Telemetry +dotnet_diagnostic.CA1005.severity = none + +# Title : Enums should have zero value +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1008 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode, RuleNoZero +dotnet_diagnostic.CA1008.severity = none + +# Title : Generic interface should also be implemented +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1010 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1010.severity = warning +dotnet_code_quality.CA1010.api_surface = all +dotnet_code_quality.CA1010.additional_required_generic_interfaces = T:System.Collections.IDictionary->T:System.Collections.Generic.IDictionary`2 + +# Title : Abstract types should not have public constructors +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1012 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1012.severity = warning +dotnet_code_quality.CA1012.api_surface = all + +# Title : Mark assemblies with CLSCompliant +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1014 +# Tags : PortedFromFxCop, Telemetry, CompilationEnd +dotnet_diagnostic.CA1014.severity = none + +# Title : Mark assemblies with assembly version +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1016 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA1016.severity = warning + +# Title : Mark assemblies with ComVisible +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1017 +# Tags : PortedFromFxCop, Telemetry, CompilationEnd +dotnet_diagnostic.CA1017.severity = none + +# Title : Mark attributes with AttributeUsageAttribute +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1018 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1018.severity = warning + +# Title : Define accessors for attribute arguments +# Category : Design +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1019 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1019.severity = suggestion + +# Title : Define accessors for attribute arguments +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1019 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1019.severity = warning + +# Title : Avoid out parameters +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1021 +# Tags : PortedFromFxCop, Telemetry +dotnet_diagnostic.CA1021.severity = none + +# Title : Use properties where appropriate +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1024 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1024.severity = none + +# Title : Mark enums with FlagsAttribute +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1027 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1027.severity = warning +dotnet_code_quality.CA1027.api_surface = all + +# Title : Enum Storage should be Int32 +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1028 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1028.severity = none + +# Title : Use events where appropriate +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1030 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1030.severity = warning + +# Title : Do not catch general exception types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1031 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1031.severity = warning + +# Title : Implement standard exception constructors +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1032 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1032.severity = none + +# Title : Interface methods should be callable by child types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1033.severity = warning + +# Title : Nested types should not be visible +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1034 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1034.severity = none + +# Title : Override methods on comparable types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1036 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1036.severity = none + +# Title : Avoid empty interfaces +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1040 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +# Comment : Reasonably frequent in modern .NET programming +dotnet_diagnostic.CA1040.severity = none + +# Title : Provide ObsoleteAttribute message +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1041 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1041.severity = warning +dotnet_code_quality.CA1041.api_surface = all + +# Title : Use Integral Or String Argument For Indexers +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1043 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1043.severity = none + +# Title : Properties should not be write only +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1044 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1044.severity = warning + +# Title : Do not pass types by reference +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1045 +# Tags : PortedFromFxCop, Telemetry +dotnet_diagnostic.CA1045.severity = none + +# Title : Do not overload equality operator on reference types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1046 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1046.severity = warning +dotnet_code_quality.CA1046.api_surface = all + +# Title : Do not declare protected member in sealed type +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1047 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1047.severity = warning +dotnet_code_quality.CA1047.api_surface = all + +# Title : Declare types in namespaces +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1050 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1050.severity = none + +# Title : Do not declare visible instance fields +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1051.severity = none + +# Title : Static holder types should be Static or NotInheritable +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1052 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1052.severity = warning + +# Title : URI-like parameters should not be strings +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1054 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1054.severity = none + +# Title : URI-like return values should not be strings +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1055 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1055.severity = none + +# Title : URI-like properties should not be strings +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1056 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1056.severity = none + +# Title : Types should not extend certain base types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1058 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1058.severity = warning +dotnet_code_quality.CA1058.api_surface = public + +# Title : Move pinvokes to native methods class +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1060 +# Tags : PortedFromFxCop, Telemetry +dotnet_diagnostic.CA1060.severity = warning + +# Title : Do not hide base class methods +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1061 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1061.severity = warning +dotnet_code_quality.CA1061.api_surface = all + +# Title : Validate arguments of public methods +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1062 +# Tags : PortedFromFxCop, Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1062.severity = none + +# Title : Implement IDisposable Correctly +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1063 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1063.severity = warning + +# Title : Exceptions should be public +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1064 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1064.severity = none + +# Title : Do not raise exceptions in unexpected locations +# Category : Design +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1065 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1065.severity = warning + +# Title : Do not raise exceptions in unexpected locations +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1065 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1065.severity = warning + +# Title : Implement IEquatable when overriding Object.Equals +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1066 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1066.severity = warning + +# Title : Override Object.Equals(object) when implementing IEquatable +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1067 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1067.severity = warning + +# Title : CancellationToken parameters must come last +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1068 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1068.severity = none + +# Title : Enums values should not be duplicated +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1069 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1069.severity = warning + +# Title : Do not declare event fields as virtual +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1070 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1070.severity = warning +dotnet_code_quality.CA1070.api_surface = all + +# Title : Avoid using cref tags with a prefix +# Category : Documentation +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1200 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1200.severity = warning + +# Title : Do not pass literals as localized parameters +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1303 +# Tags : PortedFromFxCop, Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1303.severity = none + +# Title : Specify CultureInfo +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1304 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1304.severity = none + +# Title : Specify IFormatProvider +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1305.severity = none + +# Title : Specify StringComparison for clarity +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1307 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1307.severity = none + +# Title : Normalize strings to uppercase +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1308 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1308.severity = none + +# Title : Use ordinal string comparison +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1309 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1309.severity = suggestion + +# Title : Specify StringComparison for correctness +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1310 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1310.severity = none + +# Title : Specify a culture or use an invariant version +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1311 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1311.severity = warning + +# Title : P/Invokes should not be visible +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1401 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1401.severity = none + +# Title : Validate platform compatibility +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1416.severity = warning + +# Title : Do not use 'OutAttribute' on string parameters for P/Invokes +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1417 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1417.severity = warning + +# Title : Use valid platform string +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1418 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1418.severity = warning + +# Title : Provide a parameterless constructor that is as visible as the containing type for concrete types derived from 'System.Runtime.InteropServices.SafeHandle' +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1419 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1419.severity = warning + +# Title : Property, type, or attribute requires runtime marshalling +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1420 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1420.severity = warning + +# Title : This method uses runtime marshalling even when the 'DisableRuntimeMarshallingAttribute' is applied +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1421 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1421.severity = suggestion + +# Title : Validate platform compatibility +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1422 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1422.severity = warning + +# Title : Avoid excessive inheritance +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1501 +# Tags : PortedFromFxCop, Telemetry, CompilationEnd +dotnet_diagnostic.CA1501.severity = warning +dotnet_code_quality.CA1501.api_surface = public + +# Title : Avoid excessive complexity +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1502 +# Tags : PortedFromFxCop, Telemetry, CompilationEnd +# Comment : Code gets complicated +dotnet_diagnostic.CA1502.severity = none + +# Title : Avoid unmaintainable code +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1505 +# Tags : PortedFromFxCop, Telemetry, CompilationEnd +dotnet_diagnostic.CA1505.severity = warning + +# Title : Avoid excessive class coupling +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1506 +# Tags : PortedFromFxCop, Telemetry, CompilationEnd +# Comment : Code gets complicated +dotnet_diagnostic.CA1506.severity = none + +# Title : Use nameof to express symbol names +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1507 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1507.severity = warning + +# Title : Avoid dead conditional code +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1508 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1508.severity = warning + +# Title : Invalid entry in code metrics rule specification file +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1509 +# Tags : Telemetry, CompilationEnd +dotnet_diagnostic.CA1509.severity = warning + +# Title : Do not name enum values 'Reserved' +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1700 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1700.severity = none + +# Title : Identifiers should not contain underscores +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1707 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +# Comment : StyleCop handles this +dotnet_diagnostic.CA1707.severity = none + +# Title : Identifiers should differ by more than case +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1708 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1708.severity = silent + +# Title : Identifiers should have correct suffix +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1710 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1710.severity = silent + +# Title : Identifiers should not have incorrect suffix +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1711 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1711.severity = silent + +# Title : Do not prefix enum values with type name +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1712 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1712.severity = warning + +# Title : Events should not have 'Before' or 'After' prefix +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1713 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1713.severity = warning + +# Title : Identifiers should have correct prefix +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1715 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1715.severity = none + +# Title : Identifiers should not match keywords +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1716 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1716.severity = warning +dotnet_code_quality.CA1716.api_surface = all +dotnet_code_quality.CA1716.analyzed_symbol_kinds = all + +# Title : Identifier contains type name +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1720 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1720.severity = silent + +# Title : Property names should not match get methods +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1721 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1721.severity = none + +# Title : Type names should not match namespaces +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1724 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA1724.severity = none + +# Title : Parameter names should match base declaration +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1725 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1725.severity = warning + +# Title : Use PascalCase for named placeholders +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1727 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1727.severity = silent + +# Title : Review unused parameters +# Category : Usage +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1801 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1801.severity = none + +# Title : Review unused parameters +# Category : Usage +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1801 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1801.severity = none + +# Title : Use literals where appropriate +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1802 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1802.severity = warning + +# Title : Use literals where appropriate +# Category : Performance +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1802 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1802.severity = warning + +# Title : Do not initialize unnecessarily +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1805 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1805.severity = warning + +# Title : Do not initialize unnecessarily +# Category : Performance +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1805 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1805.severity = warning + +# Title : Do not ignore method results +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1806 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1806.severity = warning + +# Title : Initialize reference type static fields inline +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1810 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1810.severity = warning + +# Title : Avoid uninstantiated internal classes +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1812 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +# Comment : S1144 finds more cases and has no false positives +dotnet_diagnostic.CA1812.severity = none + +# Title : Avoid unsealed attributes +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1813 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1813.severity = warning + +# Title : Prefer jagged arrays over multidimensional +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1814 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1814.severity = warning + +# Title : Override equals and operator equals on value types +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1815 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1815.severity = warning + +# Title : Dispose methods should call SuppressFinalize +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1816 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1816.severity = warning + +# Title : Properties should not return arrays +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1819 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1819.severity = warning + +# Title : Test for empty strings using string length +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1820 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1820.severity = warning + +# Title : Remove empty Finalizers +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1821 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1821.severity = warning + +# Title : Mark members as static +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1822.severity = warning + +# Title : Avoid unused private fields +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1823 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1823.severity = none + +# Title : Mark assemblies with NeutralResourcesLanguageAttribute +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1824 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA1824.severity = warning + +# Title : Avoid zero-length array allocations +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1825 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1825.severity = warning + +# Title : Do not use Enumerable methods on indexable collections +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1826 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1826.severity = warning + +# Title : Do not use Count() or LongCount() when Any() can be used +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1827 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1827.severity = warning + +# Title : Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1828 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1828.severity = warning + +# Title : Use Length/Count property instead of Count() when available +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1829 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1829.severity = warning + +# Title : Prefer strongly-typed Append and Insert method overloads on StringBuilder +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1830 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1830.severity = warning + +# Title : Use AsSpan or AsMemory instead of Range-based indexers when appropriate +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1831 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1831.severity = warning + +# Title : Use AsSpan or AsMemory instead of Range-based indexers when appropriate +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1832 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1832.severity = warning + +# Title : Use AsSpan or AsMemory instead of Range-based indexers when appropriate +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1833 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1833.severity = warning + +# Title : Consider using 'StringBuilder.Append(char)' when applicable +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1834 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1834.severity = warning + +# Title : Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1835 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1835.severity = warning + +# Title : Prefer IsEmpty over Count +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1836 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1836.severity = warning + +# Title : Use 'Environment.ProcessId' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1837 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1837.severity = warning + +# Title : Avoid 'StringBuilder' parameters for P/Invokes +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1838 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1838.severity = warning + +# Title : Use 'Environment.ProcessPath' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1839 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1839.severity = warning + +# Title : Use 'Environment.CurrentManagedThreadId' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1840 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1840.severity = warning + +# Title : Prefer Dictionary.Contains methods +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1841 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1841.severity = warning + +# Title : Do not use 'WhenAll' with a single task +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1842 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1842.severity = suggestion + +# Title : Do not use 'WaitAll' with a single task +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1843 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1843.severity = warning + +# Title : Provide memory-based overrides of async methods when subclassing 'Stream' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1844 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1844.severity = warning + +# Title : Use span-based 'string.Concat' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1845 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1845.severity = warning + +# Title : Prefer 'AsSpan' over 'Substring' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1846 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1846.severity = warning + +# Title : Use char literal for a single character lookup +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1847 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1847.severity = warning + +# Title : Use the LoggerMessage delegates +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848 +# Tags : Telemetry, EnabledRuleInAggressiveMode +# Comment : Use R9 logging model instead +dotnet_diagnostic.CA1848.severity = none + +# Title : Call async methods when in an async method +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1849 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1849.severity = warning + +# Title : Prefer static 'HashData' method over 'ComputeHash' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1850 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1850.severity = suggestion + +# Title : Possible multiple enumerations of 'IEnumerable' collection +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1851.severity = suggestion + +# Title : Seal internal types +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852 +# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA1852.severity = none + +# Title : Unnecessary call to 'Dictionary.ContainsKey(key)' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1853 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1853.severity = suggestion + +# Title : Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1854 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1854.severity = warning + +# Title : Prefer 'Clear' over 'Fill' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1855 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1855.severity = warning + +# Title : Dispose objects before losing scope +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000 +# Tags : PortedFromFxCop, Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2000.severity = warning + +# Title : Do not lock on objects with weak identity +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2002 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2002.severity = warning + +# Title : Consider calling ConfigureAwait on the awaited task +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2007 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2007.severity = warning + +# Title : Do not create tasks without passing a TaskScheduler +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2008 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2008.severity = warning + +# Title : Do not call ToImmutableCollection on an ImmutableCollection value +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2009 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2009.severity = warning + +# Title : Avoid infinite recursion +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2011 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2011.severity = error + +# Title : Use ValueTasks correctly +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2012 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2012.severity = warning + +# Title : Do not use ReferenceEquals with value types +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2013 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2013.severity = warning + +# Title : Do not use stackalloc in loops +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2014 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2014.severity = warning + +# Title : Do not use stackalloc in loops +# Category : Reliability +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2014 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2014.severity = warning + +# Title : Do not define finalizers for types derived from MemoryManager +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2015 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2015.severity = warning + +# Title : Forward the 'CancellationToken' parameter to methods +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2016 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2016.severity = warning + +# Title : Parameter count mismatch +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2017 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2017.severity = warning + +# Title : 'Buffer.BlockCopy' expects the number of bytes to be copied for the 'count' argument +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2018 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2018.severity = warning + +# Title : Improper 'ThreadStatic' field initialization +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2019 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2019.severity = warning + +# Title : Prevent from behavioral change +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2020 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2020.severity = suggestion + +# Title : Review SQL queries for security vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2100 +# Tags : PortedFromFxCop, Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2100.severity = none + +# Title : Specify marshaling for P/Invoke string arguments +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2101 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2101.severity = warning + +# Title : Review visible event handlers +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2109 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2109.severity = none + +# Title : Seal methods that satisfy private interfaces +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2119 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2119.severity = warning + +# Title : Do Not Catch Corrupted State Exceptions +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2153 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2153.severity = warning + +# Title : Rethrow to preserve stack details +# Category : Usage +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2200 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2200.severity = warning + +# Title : Rethrow to preserve stack details +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2200 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2200.severity = warning + +# Title : Do not raise reserved exception types +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2201 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2201.severity = warning + +# Title : Initialize value type static fields inline +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2207 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2207.severity = warning + +# Title : Instantiate argument exceptions correctly +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2208 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2208.severity = warning + +# Title : Non-constant fields should not be visible +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2211 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2211.severity = none + +# Title : Disposable fields should be disposed +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2213 +# Tags : PortedFromFxCop, Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2213.severity = warning + +# Title : Do not call overridable methods in constructors +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2214 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2214.severity = warning + +# Title : Dispose methods should call base class dispose +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2215 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2215.severity = warning + +# Title : Disposable types should declare finalizer +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2216 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2216.severity = warning + +# Title : Do not mark enums with FlagsAttribute +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2217 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2217.severity = warning +dotnet_code_quality.CA2217.api_surface = all + +# Title : Do not raise exceptions in finally clauses +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2219 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2219.severity = warning + +# Title : Operator overloads have named alternates +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2225 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2225.severity = none + +# Title : Operators should have symmetrical overloads +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2226 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2226.severity = none + +# Title : Collection properties should be read only +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2227 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2227.severity = none + +# Title : Implement serialization constructors +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2229 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +# Comment : Obsolete +dotnet_diagnostic.CA2229.severity = none + +# Title : Overload operator equals on overriding value type Equals +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2231 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2231.severity = error + +# Title : Pass system uri objects instead of strings +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2234 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2234.severity = none + +# Title : Mark all non-serializable fields +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2235 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +# Comment : Obsolete +dotnet_diagnostic.CA2235.severity = none + +# Title : Mark ISerializable types with serializable +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2237 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +# Comment : Obsolete +dotnet_diagnostic.CA2237.severity = none + +# Title : Provide correct arguments to formatting methods +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2241 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2241.severity = warning + +# Title : Test for NaN correctly +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2242 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2242.severity = warning + +# Title : Attribute string literals should parse correctly +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2243 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2243.severity = warning + +# Title : Do not duplicate indexed element initializations +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2244 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2244.severity = warning + +# Title : Do not assign a property to itself +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2245 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2245.severity = warning + +# Title : Assigning symbol and its member in the same statement +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2246 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2246.severity = warning + +# Title : Argument passed to TaskCompletionSource constructor should be TaskCreationOptions enum instead of TaskContinuationOptions enum +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2247 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2247.severity = warning + +# Title : Provide correct 'enum' argument to 'Enum.HasFlag' +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2248 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2248.severity = warning + +# Title : Consider using 'string.Contains' instead of 'string.IndexOf' +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2249 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2249.severity = warning + +# Title : Use 'ThrowIfCancellationRequested' +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2250 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2250.severity = suggestion + +# Title : Use 'string.Equals' +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2251 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2251.severity = warning + +# Title : This API requires opting into preview features +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2252 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2252.severity = error + +# Title : Named placeholders should not be numeric values +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2253 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2253.severity = warning + +# Title : Template should be a static expression +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2254 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2254.severity = none + +# Title : The 'ModuleInitializer' attribute should not be used in libraries +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2255 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2255.severity = warning + +# Title : All members declared in parent interfaces must have an implementation in a DynamicInterfaceCastableImplementation-attributed interface +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2256 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2256.severity = error + +# Title : Members defined on an interface with the 'DynamicInterfaceCastableImplementationAttribute' should be 'static' +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2257 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2257.severity = warning + +# Title : Providing a 'DynamicInterfaceCastableImplementation' interface in Visual Basic is unsupported +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2258 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2258.severity = warning + +# Title : 'ThreadStatic' only affects static fields +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2259 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2259.severity = warning + +# Title : Use correct type parameter +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2260 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2260.severity = warning + +# Title : Do not use insecure deserializer BinaryFormatter +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2300 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2300.severity = none + +# Title : Do not call BinaryFormatter.Deserialize without first setting BinaryFormatter.Binder +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2301 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2301.severity = none + +# Title : Ensure BinaryFormatter.Binder is set before calling BinaryFormatter.Deserialize +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2302 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2302.severity = none + +# Title : Do not use insecure deserializer LosFormatter +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2305 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2305.severity = none + +# Title : Do not use insecure deserializer NetDataContractSerializer +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2310 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2310.severity = none + +# Title : Do not deserialize without first setting NetDataContractSerializer.Binder +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2311 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2311.severity = none + +# Title : Ensure NetDataContractSerializer.Binder is set before deserializing +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2312 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2312.severity = none + +# Title : Do not use insecure deserializer ObjectStateFormatter +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2315 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2315.severity = none + +# Title : Do not deserialize with JavaScriptSerializer using a SimpleTypeResolver +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2321 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2321.severity = none + +# Title : Ensure JavaScriptSerializer is not initialized with SimpleTypeResolver before deserializing +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2322 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2322.severity = none + +# Title : Do not use TypeNameHandling values other than None +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2326 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2326.severity = none + +# Title : Do not use insecure JsonSerializerSettings +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2327 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2327.severity = none + +# Title : Ensure that JsonSerializerSettings are secure +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2328 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2328.severity = none + +# Title : Do not deserialize with JsonSerializer using an insecure configuration +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2329 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2329.severity = none + +# Title : Ensure that JsonSerializer has a secure configuration when deserializing +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2330 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2330.severity = none + +# Title : Do not use DataTable.ReadXml() with untrusted data +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2350 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2350.severity = none + +# Title : Do not use DataSet.ReadXml() with untrusted data +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2351 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2351.severity = none + +# Title : Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2352 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2352.severity = none + +# Title : Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2352 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2352.severity = none + +# Title : Unsafe DataSet or DataTable in serializable type +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2353 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2353.severity = none + +# Title : Unsafe DataSet or DataTable in serializable type +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2353 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2353.severity = none + +# Title : Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attacks +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2354 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2354.severity = none + +# Title : Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attacks +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2354 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2354.severity = none + +# Title : Unsafe DataSet or DataTable type found in deserializable object graph +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2355 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2355.severity = none + +# Title : Unsafe DataSet or DataTable type found in deserializable object graph +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2355 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2355.severity = none + +# Title : Unsafe DataSet or DataTable type in web deserializable object graph +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2356 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2356.severity = none + +# Title : Unsafe DataSet or DataTable type in web deserializable object graph +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2356 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2356.severity = none + +# Title : Ensure auto-generated class containing DataSet.ReadXml() is not used with untrusted data +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2361 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2361.severity = none + +# Title : Unsafe DataSet or DataTable in auto-generated serializable type can be vulnerable to remote code execution attacks +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2362 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2362.severity = none + +# Title : Unsafe DataSet or DataTable in autogenerated serializable type can be vulnerable to remote code execution attacks +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2362 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2362.severity = none + +# Title : Review code for SQL injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3001 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3001.severity = none + +# Title : Review code for XSS vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3002 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3002.severity = none + +# Title : Review code for file path injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3003 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3003.severity = none + +# Title : Review code for information disclosure vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3004 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3004.severity = none + +# Title : Review code for LDAP injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3005 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3005.severity = none + +# Title : Review code for process command injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3006 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3006.severity = none + +# Title : Review code for open redirect vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3007 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3007.severity = none + +# Title : Review code for XPath injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3008 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3008.severity = none + +# Title : Review code for XML injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3009 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3009.severity = none + +# Title : Review code for XAML injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3010 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3010.severity = none + +# Title : Review code for DLL injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3011 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3011.severity = none + +# Title : Review code for regex injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3012 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3012.severity = none + +# Title : Do Not Add Schema By URL +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3061 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3061.severity = none + +# Title : Insecure DTD processing in XML +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3075 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3075.severity = none + +# Title : Insecure XSLT script processing. +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3076 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3076.severity = none + +# Title : Insecure XSLT script processing +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3076 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3076.severity = none + +# Title : Insecure Processing in API Design, XmlDocument and XmlTextReader +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3077 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3077.severity = none + +# Title : Insecure Processing in API Design, XmlDocument and XmlTextReader +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3077 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3077.severity = none + +# Title : Mark Verb Handlers With Validate Antiforgery Token +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3147 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3147.severity = none + +# Title : Do Not Use Weak Cryptographic Algorithms +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5350 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5350.severity = none + +# Title : Do Not Use Broken Cryptographic Algorithms +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5351 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5351.severity = none + +# Title : Review cipher mode usage with cryptography experts +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5358 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5358.severity = none + +# Title : Do Not Disable Certificate Validation +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5359 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5359.severity = none + +# Title : Do Not Call Dangerous Methods In Deserialization +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5360 +# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5360.severity = none + +# Title : Do Not Disable SChannel Use of Strong Crypto +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5361 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5361.severity = none + +# Title : Potential reference cycle in deserialized object graph +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5362 +# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5362.severity = none + +# Title : Do Not Disable Request Validation +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5363 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5363.severity = none + +# Title : Do Not Use Deprecated Security Protocols +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5364 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5364.severity = none + +# Title : Do Not Disable HTTP Header Checking +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5365 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5365.severity = none + +# Title : Use XmlReader for 'DataSet.ReadXml()' +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5366 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5366.severity = none + +# Title : Do Not Serialize Types With Pointer Fields +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5367 +# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5367.severity = none + +# Title : Set ViewStateUserKey For Classes Derived From Page +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5368 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5368.severity = none + +# Title : Use XmlReader for 'XmlSerializer.Deserialize()' +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5369 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5369.severity = none + +# Title : Use XmlReader for XmlValidatingReader constructor +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5370 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5370.severity = none + +# Title : Use XmlReader for 'XmlSchema.Read()' +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5371 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5371.severity = none + +# Title : Use XmlReader for XPathDocument constructor +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5372 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5372.severity = none + +# Title : Do not use obsolete key derivation function +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5373 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5373.severity = none + +# Title : Do Not Use XslTransform +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5374 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5374.severity = none + +# Title : Do Not Use Account Shared Access Signature +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5375 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5375.severity = none + +# Title : Use SharedAccessProtocol HttpsOnly +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5376 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5376.severity = none + +# Title : Use Container Level Access Policy +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5377 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5377.severity = none + +# Title : Do not disable ServicePointManagerSecurityProtocols +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5378 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5378.severity = none + +# Title : Ensure Key Derivation Function algorithm is sufficiently strong +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5379 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5379.severity = none + +# Title : Do Not Add Certificates To Root Store +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5380 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5380.severity = none + +# Title : Ensure Certificates Are Not Added To Root Store +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5381 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5381.severity = none + +# Title : Use Secure Cookies In ASP.NET Core +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5382 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5382.severity = none + +# Title : Ensure Use Secure Cookies In ASP.NET Core +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5383 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5383.severity = none + +# Title : Do Not Use Digital Signature Algorithm (DSA) +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5384 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5384.severity = none + +# Title : Use Rivest-Shamir-Adleman (RSA) Algorithm With Sufficient Key Size +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5385 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5385.severity = none + +# Title : Avoid hardcoding SecurityProtocolType value +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5386 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5386.severity = none + +# Title : Do Not Use Weak Key Derivation Function With Insufficient Iteration Count +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5387 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5387.severity = none + +# Title : Ensure Sufficient Iteration Count When Using Weak Key Derivation Function +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5388 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5388.severity = none + +# Title : Do Not Add Archive Item's Path To The Target File System Path +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5389 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5389.severity = none + +# Title : Do not hard-code encryption key +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5390 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5390.severity = none + +# Title : Use antiforgery tokens in ASP.NET Core MVC controllers +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5391 +# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5391.severity = none + +# Title : Use DefaultDllImportSearchPaths attribute for P/Invokes +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5392 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5392.severity = none + +# Title : Do not use unsafe DllImportSearchPath value +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5393 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5393.severity = none + +# Title : Do not use insecure randomness +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5394 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5394.severity = none + +# Title : Miss HttpVerb attribute for action methods +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5395 +# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5395.severity = none + +# Title : Set HttpOnly to true for HttpCookie +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5396 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5396.severity = none + +# Title : Do not use deprecated SslProtocols values +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5397 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5397.severity = none + +# Title : Avoid hardcoded SslProtocols values +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5398 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5398.severity = none + +# Title : HttpClients should enable certificate revocation list checks +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5399 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5399.severity = none + +# Title : Ensure HttpClient certificate revocation list check is not disabled +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5400 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5400.severity = none + +# Title : Do not use CreateEncryptor with non-default IV +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5401 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5401.severity = none + +# Title : Use CreateEncryptor with the default IV +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5402 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5402.severity = none + +# Title : Do not hard-code certificate +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5403 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5403.severity = none + +# Title : Do not disable token validation checks +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5404 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5404.severity = none + +# Title : Do not always skip token validation in delegates +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5405 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5405.severity = none + +# Title : Avoid single use string builders in frequently called class members. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR1001.severity = warning + +# Title : Use bitwise operations instead of 'Enum.HasFlag' +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : Obsolete +dotnet_diagnostic.CPR101.severity = none + +# Title : Use HashSet.Contains instead of List.Contains +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR102.severity = warning + +# Title : Use Ordinal and OrdinalIgnoreCase instead of InvariantCulture when localization is not required. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR103.severity = warning + +# Title : Use DateTime.UtcNow instead of DateTime.Now when time zone conversion is not applicable. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR105.severity = warning + +# Title : MemoryStream.ToArray() is memory inefficient and can often be avoided. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR107.severity = warning + +# Title : List.AddRange() is memory inefficient. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : Obsolete +dotnet_diagnostic.CPR108.severity = none + +# Title : List.Reverse can cause boxing. Implement your own reverse for better performance. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : Obsolete +dotnet_diagnostic.CPR109.severity = none + +# Title : Specify an initial list size when initializing a list to reduce reallocations. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : Too noisy +dotnet_diagnostic.CPR110.severity = none + +# Title : ImmutableDictionary is memory inefficient. Use IReadOnlyDictionary if readonly collection is needed. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR111.severity = warning + +# Title : ImmutableList is memory inefficient. Use IReadOnlyList if a readonly collection is needed. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR112.severity = warning + +# Title : Avoid Linq as much as possible since much of the functionality is inefficient. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : Too noisy +dotnet_diagnostic.CPR113.severity = none + +# Title : string.StartWith and string.EndsWith can be implemented more efficiently checking characters with the indexer for short strings. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR114.severity = warning + +# Title : string.Contains with string.ToLower() or string.ToUpper() causes allocations which can be avoided with a case insensitive comparison. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR115.severity = warning + +# Title : string.Equals with string.ToLower() or string.ToUpper() causes allocations which can be avoided with a case insensitive comparison. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR116.severity = warning + +# Title : operator== with string.ToLower() or string.ToUpper() causes allocations which can be avoided with a case insensitive comparison. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR117.severity = warning + +# Title : Linq.SequenceEqual is inefficient. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR118.severity = warning + +# Title : Use Debug.WriteLine for debugging instead of Console.WriteLine. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : Duplicate +dotnet_diagnostic.CPR119.severity = none + +# Title : File.ReadAllXXX should be replaced by using a StreamReader to avoid adding objects to the large object heap (LOH). +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR120.severity = warning + +# Title : Specify 'concurrencyLevel' and 'capacity' in the ConcurrentDictionary ctor. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR121.severity = warning + +# Title : ConcurrentDictionary.Keys and ConcurrentDictionary.Values takes a lock defeats the benefits of concurrency. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR122.severity = warning + +# Title : ConcurrentDictionary Count, ToArray(), CopyTo() and Clear() take locks and defeats the benefits of the concurrency. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR123.severity = warning + +# Title : TraceSource.TraceEvent does a lock on each listener and can cause lock contention. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR124.severity = warning + +# Title : String interning can cause lock contention. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR125.severity = warning + +# Title : string.Format and StringBuilder.AppendFormat are not efficient for concatenation. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR126.severity = warning + +# Title : Use a custom implementation of IComparer rather than Nullable.Compare. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR127.severity = warning + +# Title : Process.GetProcessName/Process.GetMachineName does a lot of processing. Use once per process and save the result. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR128.severity = warning + +# Title : string.IndexOf is inefficient when used to check the beginning of string. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR129.severity = warning + +# Title : ArrayList is non-generic. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR130.severity = none + +# Title : Hashtable is non-generic. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR131.severity = none + +# Title : Avoid char.ToString. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR134.severity = warning + +# Title : Stream.CopyTo should be used with buffer size. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR135.severity = warning + +# Title : StreamReader.ReadLine can allocate StringBuilder instances each call if the lines are longer than the buffer size. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR136.severity = warning + +# Title : Random class instances should be shared as statics. Random is not thread safe so locks, ThreadLocal class or [ThreadStatic] attribute should be used for synchronization. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR138.severity = warning + +# Title : Regular expressions should be reused from static fields or properties. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR139.severity = warning + +# Title : TextWriter.WriteLine(string) allocates a char array. Use different overload or make two calls. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR140.severity = warning + +# Title : Reduce delegate allocations by storing them in static fields and properties. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : This rule flags most lambda uses, which makes it extremely verbose and not particularly useful +dotnet_diagnostic.CPR145.severity = none + +# Title : Extra dictionary access +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR500.severity = warning + +# Title : Avoid repeated type checking. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR501.severity = warning + +# Title : Do not cast multiple times. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR502.severity = warning + +# Title : Extra HashSet access +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR503.severity = warning + +# Title : Do not use banned insecure deserialization APIs +# Category : Security +# Help Link: https://aka.ms/ia2989 +dotnet_diagnostic.IA2989.severity = none + +# Title : Do Not Use Banned APIs For Insecure Deserializers +# Category : Security +# Help Link: https://aka.ms/ia2992 +dotnet_diagnostic.IA2992.severity = none + +# Title : Do Not Use Banned Constructors For Insecure Deserializers +# Category : Security +# Help Link: https://aka.ms/ia2993 +dotnet_diagnostic.IA2993.severity = none + +# Title : Do Not Use ResourceSet Without ResourceReader +# Category : Security +# Help Link: https://aka.ms/ia2994 +dotnet_diagnostic.IA2994.severity = none + +# Title : Do Not Use ResourceReader +# Category : Security +# Help Link: https://aka.ms/ia2995 +dotnet_diagnostic.IA2995.severity = none + +# Title : Do Not Use ResXResourceReader Without ITypeResolutionService +# Category : Security +# Help Link: https://aka.ms/ia2996 +dotnet_diagnostic.IA2996.severity = none + +# Title : Do Not Use TypeNameHandling Other Than None +# Category : Security +# Help Link: https://aka.ms/ia2997 +dotnet_diagnostic.IA2997.severity = none + +# Title : Do Not Deserialize With BinaryFormatter Without Binder +# Category : Security +# Help Link: https://aka.ms/ia2998 +dotnet_diagnostic.IA2998.severity = none + +# Title : Do Not Set BinaryFormatter.Binder to null +# Category : Security +# Help Link: https://aka.ms/ia2999 +dotnet_diagnostic.IA2999.severity = none + +# Title : Do Not Use Weak Cryptographic Algorithms +# Category : Security +# Help Link: http://aka.ms/IA5350 +# Tags : Telemetry +dotnet_diagnostic.IA5350.severity = none + +# Title : Do Not Use Broken Cryptographic Algorithms +# Category : Security +# Help Link: http://aka.ms/IA5351 +# Tags : Telemetry +dotnet_diagnostic.IA5351.severity = none + +# Title : Do Not Misuse Cryptographic APIs +# Category : Security +# Help Link: http://aka.ms/IA5352 +# Tags : Telemetry +dotnet_diagnostic.IA5352.severity = none + +# Title : Use approved crypto libraries for the supported platform +# Category : Security +# Help Link: https://aka.ms/ia5359 +# Tags : Telemetry +dotnet_diagnostic.IA5359.severity = none + +# Title : Custom web token handler was found +# Category : Security +# Help Link: https://aka.ms/ia6450 +# Tags : Telemetry +dotnet_diagnostic.IA6450.severity = none + +# Title : Implement required validations for app asserted actor token +# Category : Security +# Help Link: https://aka.ms/ia6451 +# Tags : Telemetry +dotnet_diagnostic.IA6451.severity = none + +# Title : Do not disable {0} +# Category : Security +# Help Link: https://aka.ms/ia6452 +# Tags : Telemetry +dotnet_diagnostic.IA6452.severity = none + +# Title : Do not disable {0} +# Category : Security +# Help Link: https://aka.ms/ia6453 +# Tags : Telemetry +dotnet_diagnostic.IA6453.severity = none + +# Title : Do not disable {0} +# Category : Security +# Help Link: https://aka.ms/ia6454 +# Tags : Telemetry +dotnet_diagnostic.IA6454.severity = none + +# Title : Do Not Use Insecure PowerShell LanguageModes +# Category : Security +# Help Link: https://aka.ms/ia6456 +# Tags : Telemetry +dotnet_diagnostic.IA6456.severity = none + +# Title : Review PowerShell Execution for PowerShell Injection +# Category : Security +# Help Link: https://aka.ms/IA6457 +dotnet_diagnostic.IA6457.severity = none + +# Title : Simplify name +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0001 +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line. This metadata was entered manually, since it is not exposed in the normal way from the analyzer assembly. +dotnet_diagnostic.IDE0001.severity = silent + +# Title : Simplify name +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0002 +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line. This metadata was entered manually, since it is not exposed in the normal way from the analyzer assembly. +dotnet_diagnostic.IDE0002.severity = silent + +# Title : Remove this or Me qualification +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0003-ide0009 +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line. This metadata was entered manually, since it is not exposed in the normal way from the analyzer assembly. +dotnet_diagnostic.IDE0003.severity = silent +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Title : Remove Unnecessary Cast +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0004 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled, Unnecessary +dotnet_diagnostic.IDE0004.severity = warning + +# Title : Using directive is unnecessary. +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended, Unnecessary +dotnet_diagnostic.IDE0005.severity = none + +# Title : Using directive is unnecessary. +# Category : Style +# Tags : Telemetry, EnforceOnBuild_Never, NotConfigurable, Unnecessary +dotnet_diagnostic.IDE0005_gen.severity = none + +# Title : Use implicit type +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0007 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0007.severity = silent +csharp_style_var_elsewhere = true +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = true + +# Title : Use explicit type +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0008 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0008.severity = silent + +# Title : Member access should be qualified. +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0009 +# Tags : Telemetry, EnforceOnBuild_Never +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line +dotnet_diagnostic.IDE0009.severity = none + +# Title : Add missing cases +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0010 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0010.severity = silent + +# Title : Add braces +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0011 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0011.severity = warning +csharp_prefer_braces = true + +# Title : Use 'throw' expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0016 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0016.severity = warning +csharp_style_throw_expression = true + +# Title : Simplify object initialization +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0017 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0017.severity = warning +dotnet_style_object_initializer = true + +# Title : Inline variable declaration +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0018 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0018.severity = warning +csharp_style_inlined_variable_declaration = true + +# Title : Use pattern matching +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0019 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0019.severity = silent +csharp_style_pattern_matching_over_is_with_cast_check = true + +# Title : Use pattern matching +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0020 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0020.severity = silent +csharp_style_pattern_matching_over_is_with_cast_check + +# Title : Use expression body for constructors +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0021 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0021.severity = warning +csharp_style_expression_bodied_constructors = false + +# Title : Use expression body for methods +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0022 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0022.severity = silent +csharp_style_expression_bodied_methods = when_on_single_line + +# Title : Use expression body for conversion operators +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0023 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0023.severity = warning +csharp_style_expression_bodied_operators = false + +# Title : Use expression body for operators +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0024 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0024.severity = warning +csharp_style_expression_bodied_operators = false + +# Title : Use expression body for properties +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0025 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0025.severity = warning +csharp_style_expression_bodied_properties = true + +# Title : Use expression body for indexers +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0026 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0026.severity = warning +csharp_style_expression_bodied_indexers = true + +# Title : Use expression body for accessors +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0027 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0027.severity = warning +csharp_style_expression_bodied_accessors = true + +# Title : Simplify collection initialization +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0028.severity = warning +dotnet_style_collection_initializer = true + +# Title : Use coalesce expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0029 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0029.severity = warning +dotnet_style_coalesce_expression = true + +# Title : Use coalesce expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0030 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0030.severity = warning +dotnet_style_coalesce_expression = true + +# Title : Use null propagation +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0031 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0031.severity = warning +dotnet_style_null_propagation = true + +# Title : Use auto property +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0032 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0032.severity = warning +dotnet_style_prefer_auto_properties = true + +# Title : Use explicitly provided tuple name +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0033 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0033.severity = warning +dotnet_style_explicit_tuple_names = true + +# Title : Simplify 'default' expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0034 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0034.severity = warning +csharp_prefer_simple_default_expression = true + +# Title : Unreachable code detected +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0035 +# Tags : Telemetry, EnforceOnBuild_Never, NotConfigurable, Unnecessary +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line +dotnet_diagnostic.IDE0035.severity = warning + +# Title : Order modifiers +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0036 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0036.severity = silent +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async + +# Title : Use inferred member name +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0037 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled, Unnecessary +dotnet_diagnostic.IDE0037.severity = silent +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true + +# Title : Use local function +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0039 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0039.severity = silent +csharp_style_prefer_local_over_anonymous_function = false + +# Title : Add accessibility modifiers +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0040 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0040.severity = warning +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Title : Use 'is null' check +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0041 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0041.severity = warning +dotnet_style_prefer_is_null_check_over_reference_equality_method = true + +# Title : Deconstruct variable declaration +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0042 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0042.severity = silent +csharp_style_deconstructed_variable_declaration = true + +# Title : Invalid format string +# Category : Compiler +# Tags : EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0043.severity = warning + +# Title : Add readonly modifier +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0044 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0044.severity = warning +dotnet_style_readonly_field = true:warning + +# Title : Convert to conditional expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0045 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0045.severity = silent +dotnet_style_prefer_conditional_expression_over_assignment = true + +# Title : Convert to conditional expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0046 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0046.severity = silent +dotnet_style_prefer_conditional_expression_over_return = true + +# Title : Remove unnecessary parentheses +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0047 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0047.severity = silent +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Title : Add parentheses for clarity +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0048 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0048.severity = silent +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Title : Use language keywords instead of framework type names for type references +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0049 +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line. This metadata was entered manually, since it is not exposed in the normal way from the analyzer assembly. +dotnet_diagnostic.IDE0049.severity = none + +# Title : Convert to tuple +# Category : Style +# Tags : Telemetry +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line +dotnet_diagnostic.IDE0050.severity = silent + +# Title : Remove unused private members +# Category : CodeQuality +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended, Unnecessary +dotnet_diagnostic.IDE0051.severity = none + +# Title : Remove unread private members +# Category : CodeQuality +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0052 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended, Unnecessary +dotnet_diagnostic.IDE0052.severity = warning + +# Title : Use expression body for lambda expressions +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0053 +# Tags : Telemetry, EnforceOnBuild_Recommended +# Comment : This metadata was entered manually, since it is not exposed in the normal way from the analyzer assembly. +dotnet_diagnostic.IDE0053.severity = suggestion +csharp_style_expression_bodied_lambdas = when_on_single_line + +# Title : Use compound assignment +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0054 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0054.severity = warning +dotnet_style_prefer_compound_assignment = true + +# Title : Fix formatting +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0055.severity = warning +dotnet_style_namespace_match_folder = true +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_labels = flush_left +csharp_indent_switch_labels = true +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = false + +# Title : Use index operator +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0056 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0056.severity = silent +csharp_style_prefer_index_operator = true + +# Title : Use range operator +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0057 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0057.severity = silent +csharp_style_prefer_range_operator = true:warning + +# Title : Expression value is never used +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0058 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0058.severity = none + +# Title : Unnecessary assignment of a value +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0059 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended, Unnecessary +dotnet_diagnostic.IDE0059.severity = none + +# Title : Remove unused parameter +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0060 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended, Unnecessary +dotnet_diagnostic.IDE0060.severity = warning +dotnet_code_quality_unused_parameters = all + +# Title : Use expression body for local functions +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0061 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0061.severity = warning +csharp_style_expression_bodied_local_functions = true + +# Title : Make local function 'static' +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0062 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0062.severity = warning +csharp_prefer_static_local_function = true + +# Title : Use simple 'using' statement +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0063 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0063.severity = warning +csharp_prefer_simple_using_statement = true + +# Title : Make readonly fields writable +# Category : CodeQuality +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0064 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0064.severity = warning + +# Title : Misplaced using directive +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0065 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0065.severity = warning +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true +csharp_using_directive_placement = outside_namespace:suggestion + +# Title : Convert switch statement to expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0066 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0066.severity = warning +csharp_style_prefer_switch_expression = true:warning + +# Title : Use 'System.HashCode' +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0070 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0070.severity = warning + +# Title : Simplify interpolation +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0071 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0071.severity = warning +dotnet_style_prefer_simplified_interpolation = true + +# Title : Add missing cases +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0072 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0072.severity = silent + +# Title : The file header is missing or not located at the top of the file +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0073 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0073.severity = warning + +# Title : Use compound assignment +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0074 +# Tags : Telemetry, EnforceOnBuild_Recommended +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line +dotnet_diagnostic.IDE0074.severity = suggestion +dotnet_style_prefer_compound_assignment = true + +# Title : Simplify conditional expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0075 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0075.severity = warning +dotnet_style_prefer_simplified_boolean_expressions = true + +# Title : Invalid global 'SuppressMessageAttribute' +# Category : CodeQuality +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0076 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended, Unnecessary +dotnet_diagnostic.IDE0076.severity = warning + +# Title : Avoid legacy format target in 'SuppressMessageAttribute' +# Category : CodeQuality +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0077 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0077.severity = warning + +# Title : Use pattern matching +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0078 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0078.severity = silent +csharp_style_prefer_pattern_matching = true + +# Title : Remove unnecessary suppression +# Category : Style +# Tags : Telemetry +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line +dotnet_diagnostic.IDE0079.severity = suggestion +dotnet_remove_unnecessary_suppression_exclusions = CS0618 + +# Title : Remove unnecessary suppression operator +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0080 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0080.severity = warning + +# Title : 'typeof' can be converted to 'nameof' +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0082 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0082.severity = warning + +# Title : Use pattern matching +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0083 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0083.severity = silent +csharp_style_prefer_not_pattern = true + +# Title : Use 'new(...)' +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0090 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0090.severity = warning +csharp_style_implicit_object_creation_when_type_is_apparent = true + +# Title : Remove redundant equality +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0100 +# Tags : Telemetry, EnforceOnBuild_Recommended +# Comment : S1125 triggers in more cases +dotnet_diagnostic.IDE0100.severity = none + +# Title : Remove unnecessary discard +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0110 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0110.severity = warning + +# Title : Simplify LINQ expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0120 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0120.severity = silent + +# Title : Namespace does not match folder structure +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0130 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0130.severity = silent + +# Title : Prefer 'null' check over type check +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0150 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0150.severity = warning +csharp_style_prefer_null_check_over_type_check = true + +# Title : Convert to block scoped namespace +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0160 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0160.severity = silent +csharp_style_namespace_declarations = file_scoped + +# Title : Convert to file-scoped namespace +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0161 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0161.severity = silent +csharp_style_namespace_declarations = file_scoped + +# Title : Property pattern can be simplified +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0170 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0170.severity = silent +csharp_style_prefer_extended_property_pattern = true + +# Title : Use tuple to swap values +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0180 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0180.severity = silent +csharp_style_prefer_tuple_swap = true + +# Title : Null check can be simplified +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0190 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0190.severity = silent + +# Title : Remove unnecessary lambda expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0200 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0200.severity = silent + +# Title : Convert to top-level statements +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0210 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0210.severity = silent + +# Title : Convert to 'Program.Main' style program +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0211 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0211.severity = silent + +# Title : Add explicit cast +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0220 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0220.severity = silent + +# Title : Use UTF-8 string literal +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0230 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0230.severity = silent + +# Title : Remove redundant nullable directive +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0240 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0240.severity = warning + +# Title : Remove unnecessary nullable directive +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0241 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0241.severity = warning + +# Title : Make struct 'readonly' +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0250 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0250.severity = warning + +# Title : Delegate invocation can be simplified. +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide1005 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE1005.severity = warning +csharp_style_conditional_delegate_call = true + +# Title : Naming Styles +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide1006 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE1006.severity = warning +dotnet_naming_rule.interface_should_be_ipascalcase.severity = warning +dotnet_naming_rule.interface_should_be_ipascalcase.symbols = interface +dotnet_naming_rule.interface_should_be_ipascalcase.style = ipascalcase +dotnet_naming_rule.types_should_be_pascalcase.severity = warning +dotnet_naming_rule.types_should_be_pascalcase.symbols = types +dotnet_naming_rule.types_should_be_pascalcase.style = pascalcase +dotnet_naming_rule.constant_should_be_pascalcase.severity = warning +dotnet_naming_rule.constant_should_be_pascalcase.symbols = constant +dotnet_naming_rule.constant_should_be_pascalcase.style = pascalcase +dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = warning +dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase +dotnet_naming_rule.method_parameter_should_be_camelcase.severity = warning +dotnet_naming_rule.method_parameter_should_be_camelcase.symbols = method_parameter +dotnet_naming_rule.method_parameter_should_be_camelcase.style = camelcase +dotnet_naming_rule.local_variable_should_be_camelcase.severity = warning +dotnet_naming_rule.local_variable_should_be_camelcase.symbols = local_variable +dotnet_naming_rule.local_variable_should_be_camelcase.style = camelcase +dotnet_naming_rule.type_parameter_should_be_tpascalcase.severity = warning +dotnet_naming_rule.type_parameter_should_be_tpascalcase.symbols = type_parameter +dotnet_naming_rule.type_parameter_should_be_tpascalcase.style = tpascalcase +dotnet_naming_rule.private_field_should_be__camelcase.severity = warning +dotnet_naming_rule.private_field_should_be__camelcase.symbols = private_field +dotnet_naming_rule.private_field_should_be__camelcase.style = _camelcase +dotnet_naming_rule.field_should_be_pascalcase.severity = warning +dotnet_naming_rule.field_should_be_pascalcase.symbols = field +dotnet_naming_rule.field_should_be_pascalcase.style = pascalcase +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = +dotnet_naming_symbols.method_parameter.applicable_kinds = parameter +dotnet_naming_symbols.method_parameter.applicable_accessibilities = * +dotnet_naming_symbols.method_parameter.required_modifiers = +dotnet_naming_symbols.local_variable.applicable_kinds = local +dotnet_naming_symbols.local_variable.applicable_accessibilities = local +dotnet_naming_symbols.local_variable.required_modifiers = +dotnet_naming_symbols.type_parameter.applicable_kinds = type_parameter +dotnet_naming_symbols.type_parameter.applicable_accessibilities = * +dotnet_naming_symbols.type_parameter.required_modifiers = +dotnet_naming_symbols.constant.applicable_kinds = field, local +dotnet_naming_symbols.constant.applicable_accessibilities = * +dotnet_naming_symbols.constant.required_modifiers = const +dotnet_naming_symbols.private_field.applicable_kinds = field +dotnet_naming_symbols.private_field.applicable_accessibilities = private +dotnet_naming_symbols.private_field.required_modifiers = +dotnet_naming_symbols.field.applicable_kinds = field +dotnet_naming_symbols.field.applicable_accessibilities = public, internal, protected, protected_internal, private_protected, local +dotnet_naming_symbols.field.required_modifiers = +dotnet_naming_style.pascalcase.required_prefix = +dotnet_naming_style.pascalcase.required_suffix = +dotnet_naming_style.pascalcase.word_separator = +dotnet_naming_style.pascalcase.capitalization = pascal_case +dotnet_naming_style.ipascalcase.required_prefix = I +dotnet_naming_style.ipascalcase.required_suffix = +dotnet_naming_style.ipascalcase.word_separator = +dotnet_naming_style.ipascalcase.capitalization = pascal_case +dotnet_naming_style.camelcase.required_prefix = +dotnet_naming_style.camelcase.required_suffix = +dotnet_naming_style.camelcase.word_separator = +dotnet_naming_style.camelcase.capitalization = camel_case +dotnet_naming_style._camelcase.required_prefix = _ +dotnet_naming_style._camelcase.required_suffix = +dotnet_naming_style._camelcase.word_separator = +dotnet_naming_style._camelcase.capitalization = camel_case +dotnet_naming_style.tpascalcase.required_prefix = T +dotnet_naming_style.tpascalcase.required_suffix = +dotnet_naming_style.tpascalcase.word_separator = +dotnet_naming_style.tpascalcase.capitalization = pascal_case + +# Title : Avoid multiple blank lines +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2000 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE2000.severity = warning +dotnet_style_allow_multiple_blank_lines_experimental = false + +# Title : Embedded statements must be on their own line +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2001 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE2001.severity = warning + +# Title : Consecutive braces must not have blank line between them +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2002 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE2002.severity = warning + +# Title : Blank line required between block and subsequent statement +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2003 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE2003.severity = silent + +# Title : Blank line not allowed after constructor initializer colon +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2004 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE2004.severity = warning + +# Title : Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +# Category : Trimming +dotnet_diagnostic.IL2026.severity = warning + +# Title : The value passed as the assembly name or type name to the CreateInstance method can't be statically analyzed. +# Category : Trimming +dotnet_diagnostic.IL2032.severity = warning + +# Title : The 'DynamicallyAccessedMembersAttribute' is not allowed on methods. It is allowed on method return value or method parameters. +# Category : Trimming +dotnet_diagnostic.IL2041.severity = warning + +# Title : 'DynamicallyAccessedMembersAttribute' on property conflicts with the same attribute on its accessor. +# Category : Trimming +dotnet_diagnostic.IL2043.severity = warning + +# Title : 'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides. +# Category : Trimming +dotnet_diagnostic.IL2046.severity = warning + +# Title : Correctness of COM interop cannot be guaranteed after trimming. Interfaces and interface members might be removed. +# Category : Trimming +dotnet_diagnostic.IL2050.severity = warning + +# Title : Either the type on which the MakeGenericType is called can't be statically determined, or the type parameters to be used for generic arguments can't be statically determined. +# Category : Trimming +dotnet_diagnostic.IL2055.severity = warning + +# Title : Unrecognized value passed to the parameter of method. It's not possible to guarantee the availability of the target type. +# Category : Trimming +dotnet_diagnostic.IL2057.severity = warning + +# Title : Parameters passed to method cannot be analyzed. Consider using methods 'System.Type.GetType' and `System.Activator.CreateInstance` instead. +# Category : Trimming +dotnet_diagnostic.IL2058.severity = warning + +# Title : The type passed to the RunClassConstructor is not statically known, Trimmer can't make sure that its static constructor is available. +# Category : Trimming +dotnet_diagnostic.IL2059.severity = warning + +# Title : Call to 'System.Reflection.MethodInfo.MakeGenericMethod' can not be statically analyzed. It's not possible to guarantee the availability of requirements of the generic method. +# Category : Trimming +dotnet_diagnostic.IL2060.severity = warning + +# Title : The parameter of method has a DynamicallyAccessedMembersAttribute, but the value passed to it can not be statically analyzed. +# Category : Trimming +dotnet_diagnostic.IL2062.severity = warning + +# Title : The return value of method has a DynamicallyAccessedMembersAttribute, but the value returned from the method can not be statically analyzed. +# Category : Trimming +dotnet_diagnostic.IL2063.severity = warning + +# Title : The field has a DynamicallyAccessedMembersAttribute, but the value assigned to it can not be statically analyzed. +# Category : Trimming +dotnet_diagnostic.IL2064.severity = warning + +# Title : The method has a DynamicallyAccessedMembersAttribute (which applies to the implicit 'this' parameter), but the value used for the 'this' parameter can not be statically analyzed. +# Category : Trimming +dotnet_diagnostic.IL2065.severity = warning + +# Title : The generic parameter of type or method has a DynamicallyAccessedMembersAttribute, but the value used for it can not be statically analyzed. +# Category : Trimming +dotnet_diagnostic.IL2066.severity = warning + +# Title : Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The parameter of method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2067.severity = warning + +# Title : Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The parameter of method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2068.severity = warning + +# Title : Value stored in field does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The parameter of method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2069.severity = warning + +# Title : 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The parameter of method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2070.severity = warning + +# Title : Generic argument does not satisfy 'DynamicallyAccessedMembersAttribute' in target method or type. The parameter of method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2071.severity = warning + +# Title : Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2072.severity = warning + +# Title : Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The return value of the source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2073.severity = warning + +# Title : Value stored in field does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The return value of the source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2074.severity = warning + +# Title : 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2075.severity = warning + +# Title : Target generic argument does not satisfy 'DynamicallyAccessedMembersAttribute' in target method or type. The return value of the source method does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. +# Category : Trimming +dotnet_diagnostic.IL2076.severity = warning + +# Title : Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The source field does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2077.severity = warning + +# Title : Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The source field does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2078.severity = warning + +# Title : Value stored in target field does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The source field does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2079.severity = warning + +# Title : 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The source field does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2080.severity = warning + +# Title : Target generic argument does not satisfy 'DynamicallyAccessedMembersAttribute' in target method or type. The source field does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2081.severity = warning + +# Title : Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The implicit 'this' argument of source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2082.severity = warning + +# Title : Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The implicit 'this' argument of source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2083.severity = warning + +# Title : Value stored in target field does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The implicit 'this' argument of source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2084.severity = warning + +# Title : 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The implicit 'this' argument of source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2085.severity = warning + +# Title : Target generic argument does not satisfy 'DynamicallyAccessedMembersAttribute' in target method or type. The implicit 'this' argument of source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2086.severity = warning + +# Title : Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The generic parameter of the source method or type does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2087.severity = warning + +# Title : Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The generic parameter of the source method or type does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2088.severity = warning + +# Title : Value stored in target field does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The generic parameter of the source method or type does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2089.severity = warning + +# Title : 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The generic parameter of the source method or type does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2090.severity = warning + +# Title : Target generic argument does not satisfy 'DynamicallyAccessedMembersAttribute' in target method or type. The generic parameter of the source method or type does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2091.severity = warning + +# Title : 'DynamicallyAccessedMemberTypes' on the parameter of method don't match overridden parameter of method. All overridden members must have the same 'DynamicallyAccessedMembersAttribute' usage. +# Category : Trimming +dotnet_diagnostic.IL2092.severity = warning + +# Title : 'DynamicallyAccessedMemberTypes' on the return value of method don't match overridden return value of method. All overridden members must have the same 'DynamicallyAccessedMembersAttribute' usage. +# Category : Trimming +dotnet_diagnostic.IL2093.severity = warning + +# Title : 'DynamicallyAccessedMemberTypes' on the implicit 'this' parameter of method don't match overridden implicit 'this' parameter of method. All overridden members must have the same 'DynamicallyAccessedMembersAttribute' usage. +# Category : Trimming +dotnet_diagnostic.IL2094.severity = warning + +# Title : 'DynamicallyAccessedMemberTypes' on the generic parameter of method or type don't match overridden generic parameter method or type. All overridden members must have the same 'DynamicallyAccessedMembersAttribute' usage. +# Category : Trimming +dotnet_diagnostic.IL2095.severity = warning + +# Title : Call to 'Type.GetType' method can perform case insensitive lookup of the type, currently ILLink can not guarantee presence of all the matching types. +# Category : Trimming +dotnet_diagnostic.IL2096.severity = warning + +# Title : Field has 'DynamicallyAccessedMembersAttribute', but that attribute can only be applied to fields of type 'System.Type' or 'System.String'. +# Category : Trimming +dotnet_diagnostic.IL2097.severity = warning + +# Title : Parameter of method has 'DynamicallyAccessedMembersAttribute', but that attribute can only be applied to parameters of type 'System.Type' or 'System.String'. +# Category : Trimming +dotnet_diagnostic.IL2098.severity = warning + +# Title : Property has 'DynamicallyAccessedMembersAttribute', but that attribute can only be applied to properties of type 'System.Type' or 'System.String'. +# Category : Trimming +dotnet_diagnostic.IL2099.severity = warning + +# Title : Value passed to the parameter of method cannot be statically determined as a property accessor. +# Category : Trimming +dotnet_diagnostic.IL2103.severity = warning + +# Title : Return type of method has 'DynamicallyAccessedMembersAttribute', but that attribute can only be applied to properties of type 'System.Type' or 'System.String'. +# Category : Trimming +dotnet_diagnostic.IL2106.severity = warning + +# Title : Types that derive from a base class with 'RequiresUnreferencedCodeAttribute' need to explicitly use the 'RequiresUnreferencedCodeAttribute' or suppress this warning +# Category : Trimming +dotnet_diagnostic.IL2109.severity = warning + +# Title : Field with 'DynamicallyAccessedMembersAttribute' is accessed via reflection. Trimmer can't guarantee availability of the requirements of the field. +# Category : Trimming +dotnet_diagnostic.IL2110.severity = warning + +# Title : Method with parameters or return value with `DynamicallyAccessedMembersAttribute` is accessed via reflection. Trimmer can't guarantee availability of the requirements of the method. +# Category : Trimming +dotnet_diagnostic.IL2111.severity = warning + +# Title : The use of 'RequiresUnreferencedCodeAttribute' on static constructors is disallowed since is a method not callable by the user, is only called by the runtime. Placing the attribute directly on the static constructor will have no effect, instead use 'RequiresUnreferencedCodeAttribute' on the type which will handle warning and silencing from the static constructor. +# Category : Trimming +dotnet_diagnostic.IL2116.severity = warning + +# Title : Avoid accessing Assembly file path when publishing as a single file +# Category : SingleFile +dotnet_diagnostic.IL3000.severity = warning + +# Title : Avoid using accessing Assembly file path when publishing as a single-file +# Category : Publish +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/il3000 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.IL3000.severity = warning + +# Title : Avoid accessing Assembly file path when publishing as a single file +# Category : SingleFile +dotnet_diagnostic.IL3001.severity = warning + +# Title : Avoid using accessing Assembly file path when publishing as a single-file +# Category : Publish +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/il3001 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.IL3001.severity = warning + +# Title : Avoid calling members marked with 'RequiresAssemblyFilesAttribute' when publishing as a single-file +# Category : SingleFile +dotnet_diagnostic.IL3002.severity = warning + +# Title : 'RequiresAssemblyFilesAttribute' annotations must match across all interface implementations or overrides. +# Category : SingleFile +dotnet_diagnostic.IL3003.severity = warning + +# Title : The use of 'RequiresAssemblyFilesAttribute' on static constructors is disallowed since is a method not callable by the user, is only called by the runtime. Placing the attribute directly on the static constructor will have no effect, instead use 'RequiresUnreferencedCodeAttribute' on the type which will handle warning and silencing from the static constructor. +# Category : SingleFile +dotnet_diagnostic.IL3004.severity = warning + +# Title : Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. +# Category : AOT +dotnet_diagnostic.IL3050.severity = warning + +# Title : 'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides. +# Category : AOT +dotnet_diagnostic.IL3051.severity = warning + +# Title : The use of 'RequiresDynamicCodeAttribute' on static constructors is disallowed since is a method not callable by the user, is only called by the runtime. Placing the attribute directly on the static constructor will have no effect, instead use 'RequiresUnreferencedCodeAttribute' on the type which will handle warning and silencing from the static constructor. +# Category : AOT +dotnet_diagnostic.IL3056.severity = warning + +# Title : Use source generated logging methods for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a000 +dotnet_diagnostic.R9A000.severity = warning + +# Title : Use 'Microsoft.IO.RecyclableMemoryStream' instead of 'System.IO.MemoryStream' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a001 +dotnet_diagnostic.R9A001.severity = none + +# Title : Use higher performance methods from 'IExtendedDistributedCache' +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a003 +dotnet_diagnostic.R9A003.severity = warning + +# Title : Use the 'Microsoft.R9.Extensions.Caching.Redis' package instead of 'Microsoft.Extensions.Caching.StackExchangeRedis' +# Category : Resilience +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a005 +dotnet_diagnostic.R9A005.severity = warning + +# Title : Update return type to match metric type +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a006 +dotnet_diagnostic.R9A006.severity = error + +# Title : Remove method body +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a007 +dotnet_diagnostic.R9A007.severity = error + +# Title : Add a parameter of type 'IMeter' to the method declaration +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a008 +dotnet_diagnostic.R9A008.severity = error + +# Title : Update method parameters for dimensions to be string type +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a009 +dotnet_diagnostic.R9A009.severity = error + +# Title : Make method static +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a010 +dotnet_diagnostic.R9A010.severity = error + +# Title : Make method partial +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a011 +dotnet_diagnostic.R9A011.severity = error + +# Title : Make method public +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a012 +dotnet_diagnostic.R9A012.severity = warning + +# Title : Seal non-public classes for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a013 +dotnet_diagnostic.R9A013.severity = warning + +# Title : Use the 'Microsoft.R9.Extensions.Diagnostics.Throws' class instead of explicitly throwing exception for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a014 +dotnet_diagnostic.R9A014.severity = warning + +# Title : Use the 'Microsoft.R9.Extensions.Diagnostics.Throws' class instead of explicitly throwing exception for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a015 +dotnet_diagnostic.R9A015.severity = warning + +# Title : Use eager options validation +# Category : Reliability +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a016 +dotnet_diagnostic.R9A016.severity = none + +# Title : Use asynchronous operations instead of legacy thread blocking code +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a017 +dotnet_diagnostic.R9A017.severity = warning + +# Title : Use 'Microsoft.R9.Extensions.Text.CompositeFormat' instead of 'string.Format' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a018 +dotnet_diagnostic.R9A018.severity = warning + +# Title : Remove unnecessary dictionary lookups +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a019 +dotnet_diagnostic.R9A019.severity = warning + +# Title : Remove unnecessary set lookups +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a020 +dotnet_diagnostic.R9A020.severity = warning + +# Title : Perform message formatting in the body of the logging method +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a021 +dotnet_diagnostic.R9A021.severity = warning + +# Title : Use 'System.TimeProvider' to make the code easier to test +# Category : Reliability +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a022 +dotnet_diagnostic.R9A022.severity = none + +# Title : Use 'Microsoft.R9.Extensions.Time.PerfStopwatch' instead of 'System.Diagnostics.Stopwatch' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a023 +dotnet_diagnostic.R9A023.severity = warning + +# Title : Propagate data classification +# Category : Privacy +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a024 +dotnet_diagnostic.R9A024.severity = none + +# Title : Use fixed format for 'System.ObsoleteAttribute' message +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a025 +dotnet_diagnostic.R9A025.severity = none + +# Title : Minimum deprecation period for an obsolete public API +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a026 +dotnet_diagnostic.R9A026.severity = none + +# Title : Use fixed API surface format for soft deleted members +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a027 +dotnet_diagnostic.R9A027.severity = none + +# Title : Argument provided for user input parameter on user data vending API is not from any request context +# Category : Privacy +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a028 +dotnet_diagnostic.R9A028.severity = none + +# Title : Using experimental API +# Category : Reliability +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a029 +dotnet_diagnostic.R9A029.severity = none + +# Title : Use the character-based overloads of 'String.StartsWith' or 'String.EndsWith' +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a030 +dotnet_diagnostic.R9A030.severity = warning + +# Title : Make types declared in an executable internal +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a031 +dotnet_diagnostic.R9A031.severity = warning + +# Title : Consider using an array instead of a collection +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a032 +dotnet_diagnostic.R9A032.severity = none + +# Title : Replace uses of 'Enum.GetName' and 'Enum.ToString' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a033 +dotnet_diagnostic.R9A033.severity = warning + +# Title : Optimize method group use to avoid allocations +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a034 +dotnet_diagnostic.R9A034.severity = warning + +# Title : Make struct readonly +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a035 +dotnet_diagnostic.R9A035.severity = warning + +# Title : Use 'Microsoft.R9.Extensions.Text.NumericExtensions.ToInvariantString' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a036 +dotnet_diagnostic.R9A036.severity = warning + +# Title : Use 'System.ValueTuple' instead of 'System.Tuple' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a037 +dotnet_diagnostic.R9A037.severity = warning + +# Title : Use 'Microsoft.R9.Extensions.Pools.PoolFactory' instead for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a038 +dotnet_diagnostic.R9A038.severity = warning + +# Title : Remove superfluous null checks when compiling in a nullable context +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a039 +dotnet_diagnostic.R9A039.severity = none + +# Title : Use generic collections instead of legacy collections for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a040 +dotnet_diagnostic.R9A040.severity = warning + +# Title : Use concrete types when possible for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a041 +dotnet_diagnostic.R9A041.severity = warning + +# Title : Annotate all User Data APIs parameters +# Category : Privacy +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a042 +dotnet_diagnostic.R9A042.severity = warning + +# Title : Use 'Microsoft.R9.Extensions.Text.StringSplitExtensions.TrySplit' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a043 +dotnet_diagnostic.R9A043.severity = warning + +# Title : Assign array of literal values to a static field for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a044 +dotnet_diagnostic.R9A044.severity = warning + +# Title : Use 'Array.Empty' instead of allocating a 0-element array for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a045 +dotnet_diagnostic.R9A045.severity = warning + +# Title : Source generated metrics (fast metrics) should be located in 'Metric' class +# Category : Readability +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a046 +dotnet_diagnostic.R9A046.severity = warning + +# Title : Do not use manual metrics, use fast (source generated) metrics instead for better performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a047 +dotnet_diagnostic.R9A047.severity = warning + +# Title : Use the 'Count' or 'Length' properties instead of the 'Any' method for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a048 +dotnet_diagnostic.R9A048.severity = warning + +# Title : Newly added API must be annotated with experimental attribute +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a049 +dotnet_diagnostic.R9A049.severity = none + +# Title : An experimental API was marked as obsolete +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a050 +dotnet_diagnostic.R9A050.severity = none + +# Title : A stable API was marked as experimental +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a051 +dotnet_diagnostic.R9A051.severity = none + +# Title : A stable API was deleted outside the deprecation period +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a052 +dotnet_diagnostic.R9A052.severity = none + +# Title : A deprecated API is not annotated with the obsolete attribute +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a053 +dotnet_diagnostic.R9A053.severity = none + +# Title : A deprecated API is marked as experimental +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a054 +dotnet_diagnostic.R9A054.severity = none + +# Title : The signature of a stable API has changed +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a055 +dotnet_diagnostic.R9A055.severity = none + +# Title : Fire-and-forget async call inside a 'using' block +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a056 +dotnet_diagnostic.R9A056.severity = warning + +# Title : Use consistent versions of R9 assemblies +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a057 +dotnet_diagnostic.R9A057.severity = warning + +# Title : Consider removing unnecessary conditional access operator (?) +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a058 +dotnet_diagnostic.R9A058.severity = suggestion + +# Title : Consider removing unnecessary null coalescing assignment (??=) +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a059 +dotnet_diagnostic.R9A059.severity = suggestion + +# Title : Consider removing unnecessary null coalescing operator (??) +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a060 +dotnet_diagnostic.R9A060.severity = suggestion + +# Title : The async method doesn't support cancellation +# Category : Resilience +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a061 +dotnet_diagnostic.R9A061.severity = none + +# Title : +# Category : Style +# Tags : Telemetry, EnforceOnBuild_Never, NotConfigurable +dotnet_diagnostic.RemoveUnnecessaryImportsFixable.severity = silent + +# Title : Methods and properties should be named in PascalCase +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-100 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S100.severity = none + +# Title : Method overrides should not change parameter defaults +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1006 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1006.severity = warning + +# Title : Types should be named in PascalCase +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-101 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S101.severity = none + +# Title : Lines should not be too long +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-103 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S103.severity = warning + +# Title : Files should not have too many lines of code +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-104 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S104.severity = warning + +# Title : Destructors should not throw exceptions +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1048 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1048.severity = none + +# Title : Tabulation characters should not be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-105 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S105.severity = none + +# Title : Standard outputs should not be used directly to log anything +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-106 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S106.severity = none + +# Title : Collapsible "if" statements should be merged +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1066 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1066.severity = none + +# Title : Expressions should not be too complex +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1067 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1067.severity = warning + +# Title : Methods should not have too many parameters +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-107 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S107.severity = warning + +# Title : URIs should not be hardcoded +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1075 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1075.severity = warning + +# Title : Nested blocks of code should not be left empty +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-108 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S108.severity = warning + +# Title : Magic numbers should not be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-109 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S109.severity = warning + +# Title : Inheritance tree of classes should not be too deep +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-110 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S110.severity = warning + +# Title : Fields should not have public accessibility +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1104 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1104.severity = none + +# Title : A close curly brace should be located at the beginning of a line +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1109 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1109.severity = none + +# Title : Redundant pairs of parentheses should be removed +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1110 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1110.severity = none + +# Title : Empty statements should be removed +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1116 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1116.severity = none + +# Title : Local variables should not shadow class fields +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1117 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1117.severity = warning + +# Title : Utility classes should not have public constructors +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1118 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1118.severity = none + +# Title : General exceptions should never be thrown +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-112 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S112.severity = none + +# Title : Assignments should not be made from within sub-expressions +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1121 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1121.severity = warning + +# Title : "Obsolete" attributes should include explanations +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1123 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1123.severity = none + +# Title : Boolean literals should not be redundant +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1125 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1125.severity = warning + +# Title : Unused "using" should be removed +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1128 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1128.severity = warning + +# Title : Files should contain an empty newline at the end +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-113 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S113.severity = none + +# Title : Track uses of "FIXME" tags +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1134 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1134.severity = warning + +# Title : Track uses of "TODO" tags +# Category : Info Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1135 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1135.severity = warning + +# Title : Unused private types or members should be removed +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1144 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay, Unnecessary +dotnet_diagnostic.S1144.severity = warning + +# Title : Useless "if(true) {...}" and "if(false){...}" blocks should be removed +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1145 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1145.severity = warning + +# Title : Exit methods should not be called +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1147 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1147.severity = warning + +# Title : "switch case" clauses should not have too many lines of code +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1151 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1151.severity = none + +# Title : "Any()" should be used to test for emptiness +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1155 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1155.severity = warning + +# Title : Exceptions should not be thrown in finally blocks +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1163 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1163.severity = none + +# Title : Empty arrays and collections should be returned instead of null +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1168 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1168.severity = warning + +# Title : Unused method parameters should be removed +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1172 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1172.severity = none + +# Title : Overriding members should do more than simply call the same member in the base class +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1185 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1185.severity = warning + +# Title : Methods should not be empty +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1186 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1186.severity = warning + +# Title : String literals should not be duplicated +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1192 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1192.severity = suggestion + +# Title : Nested code blocks should not be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1199 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1199.severity = warning + +# Title : Classes should not be coupled to too many other classes (Single Responsibility Principle) +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1200 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1200.severity = none + +# Title : "Equals(Object)" and "GetHashCode()" should be overridden in pairs +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1206 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1206.severity = none + +# Title : Control structures should use curly braces +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-121 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S121.severity = none + +# Title : "Equals" and the comparison operators should be overridden when implementing "IComparable" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1210 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1210.severity = none + +# Title : "GC.Collect" should not be called +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1215 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1215.severity = warning + +# Title : Statements should be on separate lines +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-122 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S122.severity = none + +# Title : Method parameters, caught exceptions and foreach variables' initial values should not be ignored +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1226 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1226.severity = warning + +# Title : break statements should not be used except for switch cases +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1227 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1227.severity = none + +# Title : Floating point numbers should not be tested for equality +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1244 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1244.severity = error + +# Title : Sections of code should not be commented out +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-125 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S125.severity = warning + +# Title : "if ... else if" constructs should end with "else" clauses +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-126 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S126.severity = none + +# Title : A "while" loop should be used instead of a "for" loop +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1264 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1264.severity = warning + +# Title : "for" loop stop conditions should be invariant +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-127 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S127.severity = warning + +# Title : "switch" statements should have at least 3 "case" clauses +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1301 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1301.severity = none + +# Title : Track uses of in-source issue suppressions +# Category : Info Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1309 +# Tags : C#, MainSourceScope, TestSourceScope +# Comment : Suppressions are frequently necessary. +dotnet_diagnostic.S1309.severity = none + +# Title : "switch/Select" statements should contain a "default/Case Else" clauses +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-131 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S131.severity = none + +# Title : Using hardcoded IP addresses is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1313 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1313.severity = warning + +# Title : Control flow statements "if", "switch", "for", "foreach", "while", "do" and "try" should not be nested too deeply +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-134 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S134.severity = none + +# Title : Functions should not have too many lines of code +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-138 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S138.severity = none + +# Title : Culture should be specified for "string" operations +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1449 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1449.severity = warning + +# Title : Private fields only used as local variables in methods should become local variables +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1450 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1450.severity = warning + +# Title : Track lack of copyright and license headers +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1451 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1451.severity = none + +# Title : "switch" statements should not have too many "case" clauses +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1479 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1479.severity = warning + +# Title : Unused local variables should be removed +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1481 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1481.severity = none + +# Title : Methods and properties should not be too complex +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1541 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1541.severity = none + +# Title : Tests should not be ignored +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1607 +# Tags : C#, TestSourceScope, SonarWay +dotnet_diagnostic.S1607.severity = warning + +# Title : Strings should not be concatenated using '+' in a loop +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1643 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1643.severity = warning + +# Title : Variables should not be self-assigned +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1656 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1656.severity = warning + +# Title : Multiple variables should not be declared on the same line +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1659 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1659.severity = warning + +# Title : An abstract class should have both abstract and concrete methods +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1694 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1694.severity = warning + +# Title : NullReferenceException should not be caught +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1696 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1696.severity = warning + +# Title : Short-circuit logic should be used to prevent null pointer dereferences in conditionals +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1697 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1697.severity = warning + +# Title : "==" should not be used when "Equals" is overridden +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1698 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1698.severity = warning + +# Title : Constructors should only call non-overridable methods +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1699 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1699.severity = warning + +# Title : Loops with at most one iteration should be refactored +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1751 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1751.severity = warning + +# Title : Identical expressions should not be used on both sides of a binary operator +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1764 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1764.severity = warning + +# Title : "switch" statements should not be nested +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1821 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1821.severity = none + +# Title : Objects should not be created to be dropped immediately without being used +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1848 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1848.severity = warning + +# Title : Unused assignments should be removed +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1854 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1854.severity = none + +# Title : "ToString()" calls should not be redundant +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1858 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1858.severity = warning + +# Title : Related "if/else if" statements should not have the same condition +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1862 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1862.severity = warning + +# Title : Two branches in a conditional structure should not have exactly the same implementation +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1871 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1871.severity = warning + +# Title : Redundant casts should not be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1905 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1905.severity = none + +# Title : Inheritance list should not be redundant +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1939 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1939.severity = warning + +# Title : Boolean checks should not be inverted +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1940 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1940.severity = warning + +# Title : Inappropriate casts should not be made +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1944 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1944.severity = warning + +# Title : "for" loop increment clauses should modify the loops' counters +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1994 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1994.severity = warning + +# Title : Hashes should include an unpredictable salt +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2053 +# Tags : C#, MainSourceScope, SonarWay +# Comment : Analysis is too slow +dotnet_diagnostic.S2053.severity = none + +# Title : Hard-coded credentials are security-sensitive +# Category : Blocker Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2068 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2068.severity = warning + +# Title : SHA-1 and Message-Digest hash algorithms should not be used in secure contexts +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2070 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2070.severity = none + +# Title : Formatting SQL queries is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2077 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2077.severity = warning + +# Title : Creating cookies without the "secure" flag is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2092 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2092.severity = warning + +# Title : Collections should not be passed as arguments to their own methods +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2114 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2114.severity = warning + +# Title : A secure password should be used when connecting to a database +# Category : Blocker Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2115 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2115.severity = warning + +# Title : Values should not be uselessly incremented +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2123 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2123.severity = warning + +# Title : Underscores should be used to make large numbers readable +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2148 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2148.severity = warning + +# Title : "sealed" classes should not have "protected" members +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2156 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2156.severity = warning + +# Title : Short-circuit logic should be used in boolean contexts +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2178 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2178.severity = warning + +# Title : Integral numbers should not be shifted by zero or more than their number of bits-1 +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2183 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2183.severity = warning + +# Title : Results of integer division should not be assigned to floating point variables +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2184 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2184.severity = warning + +# Title : TestCases should contain tests +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2187 +# Tags : C#, TestSourceScope, SonarWay +dotnet_diagnostic.S2187.severity = warning + +# Title : Recursion should not be infinite +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2190 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2190.severity = warning + +# Title : Modulus results should not be checked for direct equality +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2197 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2197.severity = warning + +# Title : Return values from functions without side effects should not be ignored +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2201 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2201.severity = none + +# Title : Runtime type checking should be simplified +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2219 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2219.severity = warning + +# Title : "Exception" should not be caught +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2221 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2221.severity = none + +# Title : Locks should be released on all paths +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2222 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2222.severity = warning + +# Title : Non-constant static fields should not be visible +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2223 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2223.severity = warning + +# Title : "ToString()" method should not return null +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2225 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2225.severity = warning + +# Title : Console logging should not be used +# Category : Minor Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2228 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2228.severity = none + +# Title : Parameters should be passed in the correct order +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2234 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2234.severity = warning + +# Title : Using pseudorandom number generators (PRNGs) is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2245 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2245.severity = warning + +# Title : A "for" loop update clause should move the counter in the right direction +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2251 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2251.severity = warning + +# Title : For-loop conditions should be true at least once +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2252 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2252.severity = warning + +# Title : Writing cookies is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2255 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2255.severity = warning + +# Title : Using non-standard cryptographic algorithms is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2257 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2257.severity = warning + +# Title : Null pointers should not be dereferenced +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2259 +# Tags : C#, MainSourceScope, SonarWay +# Comment : Redundant, covered by modern C# compiler +dotnet_diagnostic.S2259.severity = none + +# Title : Composite format strings should not lead to unexpected behavior at runtime +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2275 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2275.severity = warning + +# Title : Neither DES (Data Encryption Standard) nor DESede (3DES) should be used +# Category : Blocker Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2278 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2278.severity = warning + +# Title : Field-like events should not be virtual +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2290 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2290.severity = warning + +# Title : Overflow checking should not be disabled for "Enumerable.Sum" +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2291 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2291.severity = warning + +# Title : Trivial properties should be auto-implemented +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2292 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2292.severity = none + +# Title : "nameof" should be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2302 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2302.severity = warning + +# Title : "async" and "await" should not be used as identifiers +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2306 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2306.severity = warning + +# Title : Methods and properties that don't access instance data should be static +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2325 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2325.severity = none + +# Title : Unused type parameters should be removed +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2326 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +# Comment : Valid pattern used in a number of places +dotnet_diagnostic.S2326.severity = none + +# Title : "try" statements with identical "catch" and/or "finally" blocks should be merged +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2327 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2327.severity = warning + +# Title : "GetHashCode" should not reference mutable fields +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2328 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2328.severity = warning + +# Title : Array covariance should not be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2330 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2330.severity = warning + +# Title : Redundant modifiers should not be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2333 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2333.severity = warning + +# Title : Public constant members should not be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2339 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2339.severity = none + +# Title : Enumeration types should comply with a naming convention +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2342 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2342.severity = none + +# Title : Enumeration type names should not have "Flags" or "Enum" suffixes +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2344 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2344.severity = warning + +# Title : Flags enumerations should explicitly initialize all their members +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2345 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2345.severity = warning + +# Title : Flags enumerations zero-value members should be named "None" +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2346 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2346.severity = none + +# Title : Fields should be private +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2357 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2357.severity = none + +# Title : Optional parameters should not be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2360 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2360.severity = none + +# Title : Properties should not make collection or array copies +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2365 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2365.severity = warning + +# Title : Public methods should not have multidimensional array parameters +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2368 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2368.severity = warning + +# Title : Exceptions should not be thrown from property getters +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2372 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2372.severity = warning + +# Title : Write-only properties should not be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2376 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2376.severity = warning + +# Title : Mutable fields should not be "public static" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2386 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2386.severity = warning + +# Title : Child class fields should not shadow parent class fields +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2387 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2387.severity = warning + +# Title : Types and methods should not have too many generic parameters +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2436 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2436.severity = warning + +# Title : Silly bit operations should not be performed +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2437 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay, Unnecessary +dotnet_diagnostic.S2437.severity = warning + +# Title : Whitespace and control characters in string literals should be explicit +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2479 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2479.severity = warning + +# Title : Generic exceptions should not be ignored +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2486 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2486.severity = warning + +# Title : Shared resources should not be used for locking +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2551 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2551.severity = warning + +# Title : Conditionally executed code should be reachable +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2583 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2583.severity = none + +# Title : Boolean expressions should not be gratuitous +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2589 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2589.severity = warning + +# Title : Setting loose file permissions is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2612 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2612.severity = warning + +# Title : The length returned from a stream read should be checked +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2674 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2674.severity = warning + +# Title : Multiline blocks should be enclosed in curly braces +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2681 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2681.severity = warning + +# Title : "NaN" should not be used in comparisons +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2688 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2688.severity = warning + +# Title : "IndexOf" checks should not be for positive numbers +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2692 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2692.severity = warning + +# Title : Instance members should not write to "static" fields +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2696 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2696.severity = warning + +# Title : Tests should include assertions +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2699 +# Tags : C#, TestSourceScope, SonarWay +dotnet_diagnostic.S2699.severity = warning + +# Title : Literal boolean values should not be used in assertions +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2701 +# Tags : C#, TestSourceScope +dotnet_diagnostic.S2701.severity = warning + +# Title : "catch" clauses should do more than rethrow +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2737 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2737.severity = warning + +# Title : Static fields should not be used in generic types +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2743 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2743.severity = none + +# Title : XML parsers should not be vulnerable to XXE attacks +# Category : Blocker Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2755 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2755.severity = warning + +# Title : "=+" should not be used instead of "+=" +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2757 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2757.severity = warning + +# Title : The ternary operator should not return the same value regardless of the condition +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2758 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2758.severity = warning + +# Title : Sequential tests should not check the same condition +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2760 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2760.severity = warning + +# Title : Doubled prefix operators "!!" and "~~" should not be used +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2761 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2761.severity = warning + +# Title : SQL keywords should be delimited by whitespace +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2857 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2857.severity = warning + +# Title : "IDisposables" should be disposed +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2930 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +# Comment : Duplicate, see CA2000 +dotnet_diagnostic.S2930.severity = none + +# Title : Classes with "IDisposable" members should implement "IDisposable" +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2931 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2931.severity = warning + +# Title : Fields that are only assigned in the constructor should be "readonly" +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2933 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2933.severity = none + +# Title : Property assignments should not be made for "readonly" fields not constrained to reference types +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2934 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2934.severity = warning + +# Title : Classes should "Dispose" of members from the classes' own "Dispose" methods +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2952 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2952.severity = warning + +# Title : Methods named "Dispose" should implement "IDisposable.Dispose" +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2953 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2953.severity = warning + +# Title : Generic parameters not constrained to reference types should not be compared to "null" +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2955 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2955.severity = warning + +# Title : "IEnumerable" LINQs should be simplified +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2971 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2971.severity = warning + +# Title : "Object.ReferenceEquals" should not be used for value types +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2995 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2995.severity = warning + +# Title : "ThreadStatic" fields should not be initialized +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2996 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2996.severity = warning + +# Title : "IDisposables" created in a "using" statement should not be returned +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2997 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2997.severity = warning + +# Title : "ThreadStatic" should not be used on non-static fields +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3005 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3005.severity = warning + +# Title : Static fields should not be updated in constructors +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3010 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3010.severity = warning + +# Title : Reflection should not be used to increase accessibility of classes, methods, or fields +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3011 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3011.severity = warning + +# Title : Members should not be initialized to default values +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3052 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3052.severity = none + +# Title : Types should not have members with visibility set higher than the type's visibility +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3059 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3059.severity = none + +# Title : "is" should not be used with "this" +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3060 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3060.severity = warning + +# Title : "async" methods should not return "void" +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3168 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3168.severity = none + +# Title : Multiple "OrderBy" calls should not be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3169 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3169.severity = warning + +# Title : Delegates should not be subtracted +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3172 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3172.severity = warning + +# Title : "interface" instances should not be cast to concrete types +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3215 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3215.severity = warning + +# Title : "ConfigureAwait(false)" should be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3216 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3216.severity = none + +# Title : "Explicit" conversions of "foreach" loops should not be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3217 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3217.severity = warning + +# Title : Inner class members should not shadow outer class "static" or type members +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3218 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3218.severity = warning + +# Title : Method calls should not resolve ambiguously to overloads with "params" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3220 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3220.severity = warning + +# Title : "GC.SuppressFinalize" should not be invoked for types without destructors +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3234 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3234.severity = warning + +# Title : Redundant parentheses should not be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3235 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3235.severity = warning + +# Title : Caller information arguments should not be provided explicitly +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3236 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3236.severity = warning + +# Title : "value" parameters should be used +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3237 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3237.severity = warning + +# Title : The simplest possible condition syntax should be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3240 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3240.severity = none + +# Title : Methods should not return values that are never used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3241 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3241.severity = warning + +# Title : Method parameters should be declared with base types +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3242 +# Tags : C#, MainSourceScope, TestSourceScope +# Comment : We want to encourage concrete types instead of interface types when possible as it's considerably faster. +dotnet_diagnostic.S3242.severity = none + +# Title : Anonymous delegates should not be used to unsubscribe from Events +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3244 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3244.severity = warning + +# Title : Generic type parameters should be co/contravariant when possible +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3246 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3246.severity = warning + +# Title : Duplicate casts should not be made +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3247 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3247.severity = warning + +# Title : Classes directly extending "object" should not call "base" in "GetHashCode" or "Equals" +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3249 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3249.severity = warning + +# Title : Implementations should be provided for "partial" methods +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3251 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3251.severity = warning + +# Title : Constructor and destructor declarations should not be redundant +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3253 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3253.severity = warning + +# Title : Default parameter values should not be passed as arguments +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3254 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3254.severity = warning + +# Title : "string.IsNullOrEmpty" should be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3256 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3256.severity = warning + +# Title : Declarations and initializations should be as concise as possible +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3257 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3257.severity = warning + +# Title : Non-derived "private" classes and records should be "sealed" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3260 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3260.severity = none + +# Title : Namespaces should not be empty +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3261 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3261.severity = warning + +# Title : "params" should be used on overrides +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3262 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3262.severity = warning + +# Title : Static fields should appear in the order they must be initialized +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3263 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3263.severity = warning + +# Title : Events should be invoked +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3264 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3264.severity = warning + +# Title : Non-flags enums should not be used in bitwise operations +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3265 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3265.severity = warning + +# Title : Loops should be simplified with "LINQ" expressions +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3267 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3267.severity = none + +# Title : Cipher Block Chaining IVs should be unpredictable +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3329 +# Tags : C#, MainSourceScope, SonarWay +# Comment : Analysis is too slow +dotnet_diagnostic.S3329.severity = none + +# Title : Creating cookies without the "HttpOnly" flag is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3330 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3330.severity = warning + +# Title : Caller information parameters should come at the end of the parameter list +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3343 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3343.severity = warning + +# Title : Expressions used in "Debug.Assert" should not produce side effects +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3346 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3346.severity = warning + +# Title : Unchanged local variables should be "const" +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3353 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3353.severity = warning + +# Title : Ternary operators should not be nested +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3358 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3358.severity = warning + +# Title : "this" should not be exposed from constructors +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3366 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3366.severity = warning + +# Title : Attribute, EventArgs, and Exception type names should end with the type being extended +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3376 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3376.severity = none + +# Title : "base.Equals" should not be used to check for reference equality in "Equals" if "base" is not "object" +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3397 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3397.severity = warning + +# Title : Methods should not return constants +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3400 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3400.severity = warning + +# Title : Assertion arguments should be passed in the correct order +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3415 +# Tags : C#, TestSourceScope, SonarWay +dotnet_diagnostic.S3415.severity = warning + +# Title : Method overloads with default parameter values should not overlap +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3427 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3427.severity = warning + +# Title : "[ExpectedException]" should not be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3431 +# Tags : C#, TestSourceScope +dotnet_diagnostic.S3431.severity = warning + +# Title : Test method signatures should be correct +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3433 +# Tags : C#, TestSourceScope, SonarWay +dotnet_diagnostic.S3433.severity = warning + +# Title : Variables should not be checked against the values they're about to be assigned +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3440 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3440.severity = warning + +# Title : Redundant property names should be omitted in anonymous classes +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3441 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3441.severity = warning + +# Title : "abstract" classes should not have "public" constructors +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3442 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3442.severity = warning + +# Title : Type should not be examined on "System.Type" instances +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3443 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3443.severity = warning + +# Title : Interfaces should not simply inherit from base interfaces with colliding members +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3444 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3444.severity = warning + +# Title : Exceptions should not be explicitly rethrown +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3445 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3445.severity = warning + +# Title : "[Optional]" should not be used on "ref" or "out" parameters +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3447 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3447.severity = warning + +# Title : Right operands of shift operators should be integers +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3449 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3449.severity = warning + +# Title : Parameters with "[DefaultParameterValue]" attributes should also be marked "[Optional]" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3450 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3450.severity = warning + +# Title : "[DefaultValue]" should not be used when "[DefaultParameterValue]" is meant +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3451 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3451.severity = warning + +# Title : Classes should not have only "private" constructors +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3453 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3453.severity = warning + +# Title : "string.ToCharArray()" and "ReadOnlySpan.ToArray()" should not be called redundantly +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3456 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3456.severity = warning + +# Title : Composite format strings should be used correctly +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3457 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3457.severity = warning + +# Title : Empty "case" clauses that fall through to the "default" should be omitted +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3458 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3458.severity = warning + +# Title : Unassigned members should be removed +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3459 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3459.severity = warning + +# Title : Type inheritance should not be recursive +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3464 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3464.severity = warning + +# Title : Optional parameters should be passed to "base" calls +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3466 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3466.severity = warning + +# Title : Empty "default" clauses should be removed +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3532 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3532.severity = warning + +# Title : "ServiceContract" and "OperationContract" attributes should be used together +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3597 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3597.severity = warning + +# Title : One-way "OperationContract" methods should have "void" return type +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3598 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3598.severity = warning + +# Title : "params" should not be introduced on overrides +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3600 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3600.severity = warning + +# Title : Methods with "Pure" attribute should return a value +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3603 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3603.severity = warning + +# Title : Member initializer values should not be redundant +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3604 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3604.severity = warning + +# Title : Nullable type comparison should not be redundant +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3610 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3610.severity = warning + +# Title : Jump statements should not be redundant +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3626 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3626.severity = warning + +# Title : Empty nullable value should not be accessed +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3655 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3655.severity = warning + +# Title : Exception constructors should not throw exceptions +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3693 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3693.severity = warning + +# Title : Track use of "NotImplementedException" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3717 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3717.severity = warning + +# Title : Cognitive Complexity of methods should not be too high +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3776 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +# Comment : Code gets complicated +dotnet_diagnostic.S3776.severity = none + +# Title : "SafeHandle.DangerousGetHandle" should not be called +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3869 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3869.severity = warning + +# Title : Exception types should be "public" +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3871 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3871.severity = none + +# Title : Parameter names should not duplicate the names of their methods +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3872 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3872.severity = warning + +# Title : "out" and "ref" parameters should not be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3874 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3874.severity = none + +# Title : "operator==" should not be overloaded on reference types +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3875 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3875.severity = warning + +# Title : Strings or integral types should be used for indexers +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3876 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3876.severity = warning + +# Title : Exceptions should not be thrown from unexpected methods +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3877 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3877.severity = none + +# Title : Finalizers should not be empty +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3880 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3880.severity = warning + +# Title : "IDisposable" should be implemented correctly +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3881 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3881.severity = none + +# Title : "CoSetProxyBlanket" and "CoInitializeSecurity" should not be used +# Category : Blocker Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3884 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3884.severity = warning + +# Title : "Assembly.Load" should be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3885 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3885.severity = warning + +# Title : Mutable, non-private fields should not be "readonly" +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3887 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3887.severity = warning + +# Title : Neither "Thread.Resume" nor "Thread.Suspend" should be used +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3889 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3889.severity = warning + +# Title : Classes that provide "Equals()" should implement "IEquatable" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3897 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3897.severity = warning + +# Title : Value types should implement "IEquatable" +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3898 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3898.severity = none + +# Title : Arguments of public methods should be validated against null +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3900 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3900.severity = none + +# Title : "Assembly.GetExecutingAssembly" should not be called +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3902 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3902.severity = warning + +# Title : Types should be defined in named namespaces +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3903 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +# Comment : Doesn't work with file-scoped namespaces, so disabling for now. +dotnet_diagnostic.S3903.severity = none + +# Title : Assemblies should have version information +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3904 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3904.severity = warning + +# Title : Event Handlers should have the correct signature +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3906 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3906.severity = warning + +# Title : Generic event handlers should be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3908 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3908.severity = warning + +# Title : Collections should implement the generic interface +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3909 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3909.severity = none + +# Title : All branches in a conditional structure should not have exactly the same implementation +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3923 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3923.severity = warning + +# Title : "ISerializable" should be implemented correctly +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3925 +# Tags : C#, MainSourceScope, SonarWay +# Comment : TODO - is ISerializable still relevant? +dotnet_diagnostic.S3925.severity = none + +# Title : Deserialization methods should be provided for "OptionalField" members +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3926 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3926.severity = warning + +# Title : Serialization event handlers should be implemented correctly +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3927 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3927.severity = warning + +# Title : Parameter names used into ArgumentException constructors should match an existing one +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3928 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3928.severity = none + +# Title : Number patterns should be regular +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3937 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3937.severity = warning + +# Title : Calculations should not overflow +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3949 +# Tags : C#, MainSourceScope, TestSourceScope, Unnecessary +dotnet_diagnostic.S3949.severity = suggestion + +# Title : "Generic.List" instances should not be part of public APIs +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3956 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3956.severity = warning + +# Title : "static readonly" constants should be "const" instead +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3962 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3962.severity = none + +# Title : "static" fields should be initialized inline +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3963 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3963.severity = none + +# Title : Objects should not be disposed more than once +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3966 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3966.severity = warning + +# Title : Multidimensional arrays should not be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3967 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3967.severity = warning + +# Title : "GC.SuppressFinalize" should not be called +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3971 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3971.severity = warning + +# Title : Conditionals should start on new lines +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3972 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3972.severity = warning + +# Title : A conditionally executed single line should be denoted by indentation +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3973 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3973.severity = warning + +# Title : Collection sizes and array length comparisons should make sense +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3981 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3981.severity = warning + +# Title : Exceptions should not be created without being thrown +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3984 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3984.severity = warning + +# Title : Assemblies should be marked as CLS compliant +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3990 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3990.severity = none + +# Title : Assemblies should explicitly specify COM visibility +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3992 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3992.severity = none + +# Title : Custom attributes should be marked with "System.AttributeUsageAttribute" +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3993 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3993.severity = none + +# Title : URI Parameters should not be strings +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3994 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3994.severity = none + +# Title : URI return values should not be strings +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3995 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3995.severity = warning + +# Title : URI properties should not be strings +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3996 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3996.severity = warning + +# Title : String URI overloads should call "System.Uri" overloads +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3997 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3997.severity = warning + +# Title : Threads should not lock on objects with weak identity +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3998 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3998.severity = warning + +# Title : Pointers to unmanaged memory should not be visible +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4000 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4000.severity = warning + +# Title : Disposable types should declare finalizers +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4002 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4002.severity = warning + +# Title : Collection properties should be readonly +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4004 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4004.severity = none + +# Title : "System.Uri" arguments should be used instead of strings +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4005 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4005.severity = none + +# Title : Inherited member visibility should not be decreased +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4015 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4015.severity = warning + +# Title : Enumeration members should not be named "Reserved" +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4016 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4016.severity = warning + +# Title : Method signatures should not contain nested generic types +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4017 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4017.severity = none + +# Title : All type parameters should be used in the parameter list to enable type inference +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4018 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4018.severity = none + +# Title : Base class methods should not be hidden +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4019 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4019.severity = warning + +# Title : Enumerations should have "Int32" storage +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4022 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4022.severity = warning + +# Title : Interfaces should not be empty +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4023 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4023.severity = warning + +# Title : Child class fields should not differ from parent class fields only by capitalization +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4025 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4025.severity = warning + +# Title : Assemblies should be marked with "NeutralResourcesLanguageAttribute" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4026 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4026.severity = warning + +# Title : Exceptions should provide standard constructors +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4027 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4027.severity = none + +# Title : Classes implementing "IEquatable" should be sealed +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4035 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4035.severity = warning + +# Title : Searching OS commands in PATH is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4036 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4036.severity = warning + +# Title : Interface methods should be callable by derived types +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4039 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4039.severity = warning + +# Title : Strings should be normalized to uppercase +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4040 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4040.severity = none + +# Title : Type names should not match namespaces +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4041 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4041.severity = warning + +# Title : Generics should be used when appropriate +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4047 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4047.severity = warning + +# Title : Properties should be preferred +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4049 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4049.severity = warning + +# Title : Operators should be overloaded consistently +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4050 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4050.severity = warning + +# Title : Types should not extend outdated base types +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4052 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4052.severity = warning + +# Title : Literals should not be passed as localized parameters +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4055 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4055.severity = none + +# Title : Overloads with a "CultureInfo" or an "IFormatProvider" parameter should be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4056 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4056.severity = warning + +# Title : Locales should be set for data types +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4057 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4057.severity = warning + +# Title : Overloads with a "StringComparison" parameter should be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4058 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4058.severity = none + +# Title : Property names should not match get methods +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4059 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4059.severity = warning + +# Title : Non-abstract attributes should be sealed +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4060 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4060.severity = none + +# Title : "params" should be used instead of "varargs" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4061 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4061.severity = warning + +# Title : Operator overloads should have named alternatives +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4069 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4069.severity = none + +# Title : Non-flags enums should not be marked with "FlagsAttribute" +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4070 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4070.severity = none + +# Title : Method overloads should be grouped together +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4136 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4136.severity = warning + +# Title : Duplicate values should not be passed as arguments +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4142 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4142.severity = warning + +# Title : Collection elements should not be replaced unconditionally +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4143 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4143.severity = warning + +# Title : Methods should not have identical implementations +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4144 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4144.severity = warning + +# Title : Empty collections should not be accessed or iterated +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4158 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4158.severity = warning + +# Title : Classes should implement their "ExportAttribute" interfaces +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4159 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4159.severity = warning + +# Title : Native methods should be wrapped +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4200 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4200.severity = warning + +# Title : Null checks should not be used with "is" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4201 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4201.severity = warning + +# Title : Windows Forms entry points should be marked with STAThread +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4210 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4210.severity = warning + +# Title : Members should not have conflicting transparency annotations +# Category : Major Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4211 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4211.severity = warning + +# Title : Serialization constructors should be secured +# Category : Major Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4212 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4212.severity = warning + +# Title : "P/Invoke" methods should not be visible +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4214 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4214.severity = warning + +# Title : Events should have proper arguments +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4220 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4220.severity = warning + +# Title : Extension methods should not extend "object" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4225 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4225.severity = warning + +# Title : Extensions should be in separate namespaces +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4226 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4226.severity = none + +# Title : "ConstructorArgument" parameters should exist in constructors +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4260 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4260.severity = warning + +# Title : Methods should be named according to their synchronicities +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4261 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4261.severity = none + +# Title : Getters and setters should access the expected fields +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4275 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4275.severity = warning + +# Title : "Shared" parts should not be created with "new" +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4277 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4277.severity = warning + +# Title : Weak SSL/TLS protocols should not be used +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4423 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4423.severity = warning + +# Title : Cryptographic keys should be robust +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4426 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4426.severity = warning + +# Title : "PartCreationPolicyAttribute" should be used with "ExportAttribute" +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4428 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4428.severity = warning + +# Title : AES encryption algorithm should be used with secured mode +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4432 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4432.severity = warning + +# Title : LDAP connections should be authenticated +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4433 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4433.severity = warning + +# Title : Parameter validation in yielding methods should be wrapped +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4456 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4456.severity = warning + +# Title : Parameter validation in "async"/"await" methods should be wrapped +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4457 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4457.severity = warning + +# Title : Calls to "async" methods should not be blocking +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4462 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4462.severity = none + +# Title : Unread "private" fields should be removed +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4487 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay, Unnecessary +dotnet_diagnostic.S4487.severity = none + +# Title : Disabling CSRF protections is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4502 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4502.severity = warning + +# Title : Delivering code in production with debug features activated is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4507 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4507.severity = warning + +# Title : "default" clauses should be first or last +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4524 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4524.severity = warning + +# Title : ASP.NET HTTP request validation feature should not be disabled +# Category : Major Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4564 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4564.severity = warning + +# Title : "new Guid()" should not be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4581 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4581.severity = warning + +# Title : Calls to delegate's method "BeginInvoke" should be paired with calls to "EndInvoke" +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4583 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4583.severity = warning + +# Title : Non-async "Task/Task" methods should not return null +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4586 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4586.severity = warning + +# Title : String offset-based methods should be preferred for finding substrings from offsets +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4635 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4635.severity = warning + +# Title : Using regular expressions is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4784 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4784.severity = warning + +# Title : Encrypting data is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4787 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4787.severity = warning + +# Title : Using weak hashing algorithms is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4790 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4790.severity = warning + +# Title : Configuring loggers is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4792 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4792.severity = warning + +# Title : Using Sockets is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4818 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4818.severity = warning + +# Title : Using command line arguments is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4823 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4823.severity = warning + +# Title : Reading the Standard Input is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4829 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4829.severity = warning + +# Title : Server certificates should be verified during SSL/TLS connections +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4830 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4830.severity = none + +# Title : Controlling permissions is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4834 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4834.severity = warning + +# Title : "ValueTask" should be consumed correctly +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5034 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S5034.severity = warning + +# Title : Expanding archive files without controlling resource consumption is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5042 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5042.severity = warning + +# Title : Having a permissive Cross-Origin Resource Sharing policy is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5122 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5122.severity = warning + +# Title : Using clear-text protocols is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5332 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5332.severity = warning + +# Title : Using publicly writable directories is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5443 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5443.severity = warning + +# Title : Insecure temporary file creation methods should not be used +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5445 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5445.severity = warning + +# Title : Encryption algorithms should be used with secure mode and padding scheme +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5542 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5542.severity = warning + +# Title : Cipher algorithms should be robust +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5547 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5547.severity = warning + +# Title : JWT should be signed and verified with strong cipher algorithms +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5659 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5659.severity = warning + +# Title : Allowing requests with excessive content length is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5693 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S5693.severity = warning + +# Title : Disabling ASP.NET "Request Validation" feature is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5753 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5753.severity = warning + +# Title : Deserializing objects without performing data validation is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5766 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S5766.severity = warning + +# Title : Types allowed to be deserialized should be restricted +# Category : Major Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5773 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5773.severity = warning + +# Title : Use a testable date/time provider +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6354 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S6354.severity = none + +# Title : Azure Functions should be stateless +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6419 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S6419.severity = warning + +# Title : Client instances should not be recreated on each Azure Function invocation +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6420 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S6420.severity = warning + +# Title : Azure Functions should use Structured Error Handling +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6421 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S6421.severity = warning + +# Title : Calls to "async" methods should not be blocking in Azure Functions +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6422 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S6422.severity = warning + +# Title : Azure Functions should log all failures +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6423 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S6423.severity = warning + +# Title : Interfaces for durable entities should satisfy the restrictions +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6424 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S6424.severity = warning + +# Title : Not specifying a timeout for regular expressions is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6444 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S6444.severity = warning + +# Title : Literal suffixes should be upper case +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-818 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S818.severity = warning + +# Title : Increment (++) and decrement (--) operators should not be used in a method call or mixed with other operators in an expression +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-881 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S881.severity = suggestion + +# Title : "goto" statement should not be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-907 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S907.severity = warning + +# Title : Parameter names should match base declaration and other partial definitions +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-927 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S927.severity = none + +# Title : Copy-paste token calculator +# Category : +# Tags : MainSourceScope, TestSourceScope, Utility, NotConfigurable +dotnet_diagnostic.S9999-cpd.severity = warning + +# Title : Log generator +# Category : +# Tags : MainSourceScope, TestSourceScope, Utility, NotConfigurable +dotnet_diagnostic.S9999-log.severity = none + +# Title : File metadata generator +# Category : +# Tags : MainSourceScope, TestSourceScope, Utility, NotConfigurable +dotnet_diagnostic.S9999-metadata.severity = warning + +# Title : Metrics calculator +# Category : +# Tags : MainSourceScope, TestSourceScope, Utility, NotConfigurable +dotnet_diagnostic.S9999-metrics.severity = warning + +# Title : Symbol reference calculator +# Category : +# Tags : MainSourceScope, TestSourceScope, Utility, NotConfigurable +dotnet_diagnostic.S9999-symbolRef.severity = warning + +# Title : Token type calculator +# Category : +# Tags : MainSourceScope, TestSourceScope, Utility, NotConfigurable +dotnet_diagnostic.S9999-token-type.severity = warning + +# Title : XML comment analysis disabled +# Category : StyleCop.CSharp.SpecialRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA0001.md +dotnet_diagnostic.SA0001.severity = none + +# Title : Invalid settings file +# Category : StyleCop.CSharp.SpecialRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA0002.md +dotnet_diagnostic.SA0002.severity = warning + +# Title : Keywords should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1000.md +dotnet_diagnostic.SA1000.severity = none + +# Title : Commas should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1001.md +dotnet_diagnostic.SA1001.severity = none + +# Title : Semicolons should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1002.md +dotnet_diagnostic.SA1002.severity = none + +# Title : Symbols should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1003.md +dotnet_diagnostic.SA1003.severity = none + +# Title : Documentation lines should begin with single space +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1004.md +dotnet_diagnostic.SA1004.severity = warning + +# Title : Single line comments should begin with single space +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1005.md +dotnet_diagnostic.SA1005.severity = warning + +# Title : Preprocessor keywords should not be preceded by space +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1006.md +dotnet_diagnostic.SA1006.severity = warning + +# Title : Operator keyword should be followed by space +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1007.md +dotnet_diagnostic.SA1007.severity = none + +# Title : Opening parenthesis should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1008.md +dotnet_diagnostic.SA1008.severity = none + +# Title : Closing parenthesis should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1009.md +dotnet_diagnostic.SA1009.severity = none + +# Title : Opening square brackets should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1010.md +dotnet_diagnostic.SA1010.severity = none + +# Title : Closing square brackets should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1011.md +dotnet_diagnostic.SA1011.severity = none + +# Title : Opening braces should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1012.md +dotnet_diagnostic.SA1012.severity = none + +# Title : Closing braces should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1013.md +dotnet_diagnostic.SA1013.severity = none + +# Title : Opening generic brackets should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1014.md +dotnet_diagnostic.SA1014.severity = none + +# Title : Closing generic brackets should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1015.md +dotnet_diagnostic.SA1015.severity = none + +# Title : Opening attribute brackets should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1016.md +dotnet_diagnostic.SA1016.severity = none + +# Title : Closing attribute brackets should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1017.md +dotnet_diagnostic.SA1017.severity = none + +# Title : Nullable type symbols should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1018.md +dotnet_diagnostic.SA1018.severity = none + +# Title : Member access symbols should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1019.md +dotnet_diagnostic.SA1019.severity = none + +# Title : Increment decrement symbols should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1020.md +dotnet_diagnostic.SA1020.severity = none + +# Title : Negative signs should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1021.md +dotnet_diagnostic.SA1021.severity = none + +# Title : Positive signs should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1022.md +dotnet_diagnostic.SA1022.severity = none + +# Title : Dereference and access of symbols should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1023.md +dotnet_diagnostic.SA1023.severity = none + +# Title : Colons Should Be Spaced Correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1024.md +dotnet_diagnostic.SA1024.severity = none + +# Title : Code should not contain multiple whitespace in a row +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1025.md +dotnet_diagnostic.SA1025.severity = none + +# Title : Code should not contain space after new or stackalloc keyword in implicitly typed array allocation +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1026.md +dotnet_diagnostic.SA1026.severity = none + +# Title : Use tabs correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1027.md +dotnet_diagnostic.SA1027.severity = warning + +# Title : Code should not contain trailing whitespace +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1028.md +# Tags : Unnecessary +dotnet_diagnostic.SA1028.severity = warning + +# Title : Do not prefix calls with base unless local implementation exists +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1100.md +dotnet_diagnostic.SA1100.severity = warning + +# Title : Prefix local calls with this +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1101.md +dotnet_diagnostic.SA1101.severity = none + +# Title : Query clause should follow previous clause +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1102.md +dotnet_diagnostic.SA1102.severity = warning + +# Title : Query clauses should be on separate lines or all on one line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1103.md +dotnet_diagnostic.SA1103.severity = warning + +# Title : Query clause should begin on new line when previous clause spans multiple lines +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1104.md +dotnet_diagnostic.SA1104.severity = warning + +# Title : Query clauses spanning multiple lines should begin on own line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1105.md +dotnet_diagnostic.SA1105.severity = warning + +# Title : Code should not contain empty statements +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1106.md +# Tags : Unnecessary +dotnet_diagnostic.SA1106.severity = warning + +# Title : Code should not contain multiple statements on one line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1107.md +dotnet_diagnostic.SA1107.severity = none + +# Title : Block statements should not contain embedded comments +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1108.md +dotnet_diagnostic.SA1108.severity = warning + +# Title : Block statements should not contain embedded regions +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1109.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1109.severity = warning + +# Title : Opening parenthesis or bracket should be on declaration line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1110.md +dotnet_diagnostic.SA1110.severity = warning + +# Title : Closing parenthesis should be on line of last parameter +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1111.md +dotnet_diagnostic.SA1111.severity = warning + +# Title : Closing parenthesis should be on line of opening parenthesis +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1112.md +dotnet_diagnostic.SA1112.severity = warning + +# Title : Comma should be on the same line as previous parameter +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1113.md +dotnet_diagnostic.SA1113.severity = warning + +# Title : Parameter list should follow declaration +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1114.md +dotnet_diagnostic.SA1114.severity = warning + +# Title : Parameter should follow comma +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1115.md +dotnet_diagnostic.SA1115.severity = none + +# Title : Split parameters should start on line after declaration +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1116.md +dotnet_diagnostic.SA1116.severity = none + +# Title : Parameters should be on same line or separate lines +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1117.md +dotnet_diagnostic.SA1117.severity = none + +# Title : Parameter should not span multiple lines +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1118.md +dotnet_diagnostic.SA1118.severity = warning + +# Title : Statement should not use unnecessary parenthesis +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1119.md +dotnet_diagnostic.SA1119.severity = warning + +# Title : Statement should not use unnecessary parenthesis +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1119.md +# Tags : Unnecessary, NotConfigurable +dotnet_diagnostic.SA1119_p.severity = none + +# Title : Comments should contain text +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1120.md +dotnet_diagnostic.SA1120.severity = warning + +# Title : Use built-in type alias +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1121.md +# Tags : Unnecessary +dotnet_diagnostic.SA1121.severity = warning + +# Title : Use string.Empty for empty strings +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1122.md +dotnet_diagnostic.SA1122.severity = none + +# Title : Do not place regions within elements +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1123.md +dotnet_diagnostic.SA1123.severity = warning + +# Title : Do not use regions +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1124.md +dotnet_diagnostic.SA1124.severity = none + +# Title : Use shorthand for nullable types +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1125.md +dotnet_diagnostic.SA1125.severity = warning + +# Title : Prefix calls correctly +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1126.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1126.severity = none + +# Title : Generic type constraints should be on their own line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1127.md +dotnet_diagnostic.SA1127.severity = warning + +# Title : Put constructor initializers on their own line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1128.md +dotnet_diagnostic.SA1128.severity = warning + +# Title : Do not use default value type constructor +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1129.md +dotnet_diagnostic.SA1129.severity = warning + +# Title : Use lambda syntax +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1130.md +dotnet_diagnostic.SA1130.severity = warning + +# Title : Use readable conditions +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1131.md +dotnet_diagnostic.SA1131.severity = warning + +# Title : Do not combine fields +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1132.md +# Comment : S1169 handles fields and variables +dotnet_diagnostic.SA1132.severity = none + +# Title : Do not combine attributes +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1133.md +dotnet_diagnostic.SA1133.severity = warning + +# Title : Attributes should not share line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1134.md +dotnet_diagnostic.SA1134.severity = none + +# Title : Using directives should be qualified +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1135.md +dotnet_diagnostic.SA1135.severity = warning + +# Title : Enum values should be on separate lines +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1136.md +dotnet_diagnostic.SA1136.severity = warning + +# Title : Elements should have the same indentation +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1137.md +# Comment : Doesn't work with file-scoped namespaces +dotnet_diagnostic.SA1137.severity = none + +# Title : Use literal suffix notation instead of casting +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1139.md +dotnet_diagnostic.SA1139.severity = warning + +# Title : Use tuple syntax +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1141.md +dotnet_diagnostic.SA1141.severity = warning + +# Title : Refer to tuple fields by name +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1142.md +dotnet_diagnostic.SA1142.severity = none + +# Title : Using directives should be placed correctly +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md +dotnet_diagnostic.SA1200.severity = none + +# Title : Elements should appear in the correct order +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1201.md +dotnet_diagnostic.SA1201.severity = none + +# Title : Elements should be ordered by access +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1202.md +dotnet_diagnostic.SA1202.severity = none + +# Title : Constants should appear before fields +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1203.md +dotnet_diagnostic.SA1203.severity = warning + +# Title : Static elements should appear before instance elements +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1204.md +dotnet_diagnostic.SA1204.severity = warning + +# Title : Partial elements should declare access +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1205.md +dotnet_diagnostic.SA1205.severity = warning + +# Title : Declaration keywords should follow order +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1206.md +dotnet_diagnostic.SA1206.severity = warning + +# Title : Protected should come before internal +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1207.md +dotnet_diagnostic.SA1207.severity = warning + +# Title : System using directives should be placed before other using directives +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1208.md +dotnet_diagnostic.SA1208.severity = warning + +# Title : Using alias directives should be placed after other using directives +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1209.md +dotnet_diagnostic.SA1209.severity = warning + +# Title : Using directives should be ordered alphabetically by namespace +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1210.md +dotnet_diagnostic.SA1210.severity = warning + +# Title : Using alias directives should be ordered alphabetically by alias name +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1211.md +dotnet_diagnostic.SA1211.severity = warning + +# Title : Property accessors should follow order +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1212.md +dotnet_diagnostic.SA1212.severity = warning + +# Title : Event accessors should follow order +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1213.md +dotnet_diagnostic.SA1213.severity = warning + +# Title : Readonly fields should appear before non-readonly fields +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1214.md +dotnet_diagnostic.SA1214.severity = warning + +# Title : Using static directives should be placed at the correct location +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1216.md +dotnet_diagnostic.SA1216.severity = warning + +# Title : Using static directives should be ordered alphabetically +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1217.md +dotnet_diagnostic.SA1217.severity = warning + +# Title : Element should begin with upper-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md +dotnet_diagnostic.SA1300.severity = none + +# Title : Element should begin with lower-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1301.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1301.severity = none + +# Title : Interface names should begin with I +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1302.md +dotnet_diagnostic.SA1302.severity = none + +# Title : Const field names should begin with upper-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md +dotnet_diagnostic.SA1303.severity = none + +# Title : Non-private readonly fields should begin with upper-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1304.md +dotnet_diagnostic.SA1304.severity = none + +# Title : Field names should not use Hungarian notation +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1305.md +dotnet_diagnostic.SA1305.severity = none + +# Title : Field names should begin with lower-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1306.md +dotnet_diagnostic.SA1306.severity = none + +# Title : Accessible fields should begin with upper-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1307.md +dotnet_diagnostic.SA1307.severity = none + +# Title : Variable names should not be prefixed +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1308.md +dotnet_diagnostic.SA1308.severity = none + +# Title : Field names should not begin with underscore +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1309.md +dotnet_diagnostic.SA1309.severity = none + +# Title : Field names should not contain underscore +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1310.md +dotnet_diagnostic.SA1310.severity = warning + +# Title : Static readonly fields should begin with upper-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md +dotnet_diagnostic.SA1311.severity = none + +# Title : Variable names should begin with lower-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md +dotnet_diagnostic.SA1312.severity = none + +# Title : Parameter names should begin with lower-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1313.md +dotnet_diagnostic.SA1313.severity = none + +# Title : Type parameter names should begin with T +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1314.md +dotnet_diagnostic.SA1314.severity = none + +# Title : Tuple element names should use correct casing +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1316.md +dotnet_diagnostic.SA1316.severity = warning + +# Title : Access modifier should be declared +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1400.md +dotnet_diagnostic.SA1400.severity = warning + +# Title : Fields should be private +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md +dotnet_diagnostic.SA1401.severity = none + +# Title : File may only contain a single type +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1402.md +dotnet_diagnostic.SA1402.severity = warning + +# Title : File may only contain a single namespace +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1403.md +dotnet_diagnostic.SA1403.severity = warning + +# Title : Code analysis suppression should have justification +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1404.md +dotnet_diagnostic.SA1404.severity = warning + +# Title : Debug.Assert should provide message text +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1405.md +dotnet_diagnostic.SA1405.severity = warning + +# Title : Debug.Fail should provide message text +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1406.md +dotnet_diagnostic.SA1406.severity = warning + +# Title : Arithmetic expressions should declare precedence +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1407.md +dotnet_diagnostic.SA1407.severity = warning + +# Title : Conditional expressions should declare precedence +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1408.md +dotnet_diagnostic.SA1408.severity = warning + +# Title : Remove unnecessary code +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1409.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1409.severity = warning + +# Title : Remove delegate parenthesis when possible +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1410.md +# Tags : Unnecessary +dotnet_diagnostic.SA1410.severity = warning + +# Title : Attribute constructor should not use unnecessary parenthesis +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1411.md +# Tags : Unnecessary +dotnet_diagnostic.SA1411.severity = warning + +# Title : Store files as UTF-8 with byte order mark +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1412.md +# Comment : Pedantic +dotnet_diagnostic.SA1412.severity = none + +# Title : Use trailing comma in multi-line initializers +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1413.md +dotnet_diagnostic.SA1413.severity = none + +# Title : Tuple types in signatures should have element names +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1414.md +dotnet_diagnostic.SA1414.severity = warning + +# Title : Braces for multi-line statements should not share line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1500.md +dotnet_diagnostic.SA1500.severity = warning + +# Title : Statement should not be on a single line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1501.md +dotnet_diagnostic.SA1501.severity = warning + +# Title : Element should not be on a single line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1502.md +dotnet_diagnostic.SA1502.severity = warning + +# Title : Braces should not be omitted +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1503.md +dotnet_diagnostic.SA1503.severity = none + +# Title : All accessors should be single-line or multi-line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1504.md +dotnet_diagnostic.SA1504.severity = warning + +# Title : Opening braces should not be followed by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1505.md +dotnet_diagnostic.SA1505.severity = warning + +# Title : Element documentation headers should not be followed by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1506.md +dotnet_diagnostic.SA1506.severity = warning + +# Title : Code should not contain multiple blank lines in a row +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1507.md +dotnet_diagnostic.SA1507.severity = none + +# Title : Closing braces should not be preceded by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1508.md +dotnet_diagnostic.SA1508.severity = none + +# Title : Opening braces should not be preceded by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1509.md +dotnet_diagnostic.SA1509.severity = warning + +# Title : Chained statement blocks should not be preceded by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1510.md +dotnet_diagnostic.SA1510.severity = warning + +# Title : While-do footer should not be preceded by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1511.md +dotnet_diagnostic.SA1511.severity = warning + +# Title : Single-line comments should not be followed by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1512.md +dotnet_diagnostic.SA1512.severity = none + +# Title : Closing brace should be followed by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1513.md +dotnet_diagnostic.SA1513.severity = warning + +# Title : Element documentation header should be preceded by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1514.md +dotnet_diagnostic.SA1514.severity = warning + +# Title : Single-line comment should be preceded by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1515.md +dotnet_diagnostic.SA1515.severity = warning + +# Title : Elements should be separated by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1516.md +dotnet_diagnostic.SA1516.severity = none + +# Title : Code should not contain blank lines at start of file +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1517.md +dotnet_diagnostic.SA1517.severity = warning + +# Title : Use line endings correctly at end of file +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1518.md +dotnet_diagnostic.SA1518.severity = none + +# Title : Braces should not be omitted from multi-line child statement +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1519.md +dotnet_diagnostic.SA1519.severity = warning + +# Title : Use braces consistently +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1520.md +dotnet_diagnostic.SA1520.severity = warning + +# Title : Elements should be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1600.md +dotnet_diagnostic.SA1600.severity = none + +# Title : Partial elements should be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1601.md +dotnet_diagnostic.SA1601.severity = none + +# Title : Enumeration items should be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1602.md +dotnet_diagnostic.SA1602.severity = none + +# Title : Documentation should contain valid XML +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1603.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1603.severity = warning + +# Title : Element documentation should have summary +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1604.md +dotnet_diagnostic.SA1604.severity = warning + +# Title : Partial element documentation should have summary +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1605.md +dotnet_diagnostic.SA1605.severity = warning + +# Title : Element documentation should have summary text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1606.md +dotnet_diagnostic.SA1606.severity = warning + +# Title : Partial element documentation should have summary text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1607.md +dotnet_diagnostic.SA1607.severity = warning + +# Title : Element documentation should not have default summary +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1608.md +dotnet_diagnostic.SA1608.severity = warning + +# Title : Property documentation should have value +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1609.md +dotnet_diagnostic.SA1609.severity = none + +# Title : Property documentation should have value text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1610.md +dotnet_diagnostic.SA1610.severity = none + +# Title : Element parameters should be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1611.md +dotnet_diagnostic.SA1611.severity = none + +# Title : Element parameter documentation should match element parameters +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1612.md +dotnet_diagnostic.SA1612.severity = warning + +# Title : Element parameter documentation should declare parameter name +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1613.md +dotnet_diagnostic.SA1613.severity = warning + +# Title : Element parameter documentation should have text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1614.md +dotnet_diagnostic.SA1614.severity = warning + +# Title : Element return value should be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1615.md +dotnet_diagnostic.SA1615.severity = none + +# Title : Element return value documentation should have text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1616.md +dotnet_diagnostic.SA1616.severity = warning + +# Title : Void return value should not be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1617.md +dotnet_diagnostic.SA1617.severity = warning + +# Title : Generic type parameters should be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1618.md +dotnet_diagnostic.SA1618.severity = warning + +# Title : Generic type parameters should be documented partial class +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1619.md +dotnet_diagnostic.SA1619.severity = warning + +# Title : Generic type parameter documentation should match type parameters +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1620.md +dotnet_diagnostic.SA1620.severity = warning + +# Title : Generic type parameter documentation should declare parameter name +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1621.md +dotnet_diagnostic.SA1621.severity = warning + +# Title : Generic type parameter documentation should have text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1622.md +dotnet_diagnostic.SA1622.severity = warning + +# Title : Property summary documentation should match accessors +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1623.md +dotnet_diagnostic.SA1623.severity = warning + +# Title : Property summary documentation should omit accessor with restricted access +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1624.md +dotnet_diagnostic.SA1624.severity = warning + +# Title : Element documentation should not be copied and pasted +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1625.md +dotnet_diagnostic.SA1625.severity = warning + +# Title : Single-line comments should not use documentation style slashes +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1626.md +dotnet_diagnostic.SA1626.severity = warning + +# Title : Documentation text should not be empty +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1627.md +dotnet_diagnostic.SA1627.severity = warning + +# Title : Documentation text should begin with a capital letter +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1628.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1628.severity = warning + +# Title : Documentation text should end with a period +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1629.md +dotnet_diagnostic.SA1629.severity = warning + +# Title : Documentation text should contain whitespace +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1630.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1630.severity = warning + +# Title : Documentation should meet character percentage +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1631.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1631.severity = warning + +# Title : Documentation text should meet minimum character length +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1632.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1632.severity = warning + +# Title : File should have header +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1633.md +dotnet_diagnostic.SA1633.severity = none + +# Title : File header should show copyright +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1634.md +dotnet_diagnostic.SA1634.severity = none + +# Title : File header should have copyright text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1635.md +dotnet_diagnostic.SA1635.severity = none + +# Title : File header copyright text should match +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1636.md +dotnet_diagnostic.SA1636.severity = none + +# Title : File header should contain file name +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1637.md +dotnet_diagnostic.SA1637.severity = none + +# Title : File header file name documentation should match file name +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1638.md +dotnet_diagnostic.SA1638.severity = none + +# Title : File header should have summary +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1639.md +dotnet_diagnostic.SA1639.severity = none + +# Title : File header should have valid company text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1640.md +dotnet_diagnostic.SA1640.severity = none + +# Title : File header company name text should match +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1641.md +dotnet_diagnostic.SA1641.severity = none + +# Title : Constructor summary documentation should begin with standard text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1642.md +dotnet_diagnostic.SA1642.severity = warning + +# Title : Destructor summary documentation should begin with standard text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1643.md +dotnet_diagnostic.SA1643.severity = warning + +# Title : Documentation headers should not contain blank lines +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1644.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1644.severity = warning + +# Title : Included documentation file does not exist +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1645.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1645.severity = warning + +# Title : Included documentation XPath does not exist +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1646.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1646.severity = warning + +# Title : Include node does not contain valid file and path +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1647.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1647.severity = warning + +# Title : inheritdoc should be used with inheriting class +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1648.md +dotnet_diagnostic.SA1648.severity = warning + +# Title : File name should match first type name +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1649.md +dotnet_diagnostic.SA1649.severity = warning + +# Title : Element documentation should be spelled correctly +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1650.md +# Tags : NotConfigurable +# Comment : Deprecated +dotnet_diagnostic.SA1650.severity = none + +# Title : Do not use placeholder elements +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1651.md +dotnet_diagnostic.SA1651.severity = warning + +# Title : Do not prefix local calls with 'this.' +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SX1101.md +# Tags : Unnecessary +dotnet_diagnostic.SX1101.severity = warning + +# Title : Field names should begin with underscore +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SX1309.md +dotnet_diagnostic.SX1309.severity = none + +# Title : Static field names should begin with underscore +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SX1309S.md +dotnet_diagnostic.SX1309S.severity = none + +# Title : Avoid legacy thread switching APIs +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD001.md +dotnet_diagnostic.VSTHRD001.severity = warning + +# Title : Avoid problematic synchronous waits +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD002.md +dotnet_diagnostic.VSTHRD002.severity = warning + +# Title : Avoid awaiting foreign Tasks +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD003.md +dotnet_diagnostic.VSTHRD003.severity = warning + +# Title : Await SwitchToMainThreadAsync +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD004.md +dotnet_diagnostic.VSTHRD004.severity = error + +# Title : Invoke single-threaded types on Main thread +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD010.md +# Tags : CompilationEnd +dotnet_diagnostic.VSTHRD010.severity = warning + +# Title : Use AsyncLazy +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD011.md +dotnet_diagnostic.VSTHRD011.severity = error + +# Title : Provide JoinableTaskFactory where allowed +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD012.md +dotnet_diagnostic.VSTHRD012.severity = warning + +# Title : Avoid async void methods +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD100.md +dotnet_diagnostic.VSTHRD100.severity = error + +# Title : Avoid unsupported async delegates +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD101.md +dotnet_diagnostic.VSTHRD101.severity = error + +# Title : Implement internal logic asynchronously +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD102.md +dotnet_diagnostic.VSTHRD102.severity = suggestion + +# Title : Call async methods when in an async method +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD103.md +dotnet_diagnostic.VSTHRD103.severity = warning + +# Title : Offer async methods +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD104.md +dotnet_diagnostic.VSTHRD104.severity = none + +# Title : Avoid method overloads that assume TaskScheduler.Current +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD105.md +dotnet_diagnostic.VSTHRD105.severity = warning + +# Title : Use InvokeAsync to raise async events +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD106.md +dotnet_diagnostic.VSTHRD106.severity = warning + +# Title : Await Task within using expression +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD107.md +dotnet_diagnostic.VSTHRD107.severity = error + +# Title : Assert thread affinity unconditionally +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD108.md +dotnet_diagnostic.VSTHRD108.severity = warning + +# Title : Switch instead of assert in async methods +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD109.md +dotnet_diagnostic.VSTHRD109.severity = error + +# Title : Observe result of async calls +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD110.md +dotnet_diagnostic.VSTHRD110.severity = none + +# Title : Use ConfigureAwait(bool) +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD111.md +dotnet_diagnostic.VSTHRD111.severity = none + +# Title : Implement System.IAsyncDisposable +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD112.md +dotnet_diagnostic.VSTHRD112.severity = suggestion + +# Title : Check for System.IAsyncDisposable +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD113.md +dotnet_diagnostic.VSTHRD113.severity = suggestion + +# Title : Avoid returning a null Task +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD114.md +dotnet_diagnostic.VSTHRD114.severity = warning + +# Title : Avoid returning a null Task +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD114.md +dotnet_diagnostic.VSTHRD114.severity = error + +# Title : Use "Async" suffix for async methods +# Category : Style +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD200.md +dotnet_diagnostic.VSTHRD200.severity = none + diff --git a/bench/Directory.Build.props b/bench/Directory.Build.props new file mode 100644 index 0000000000..ba8eefefd0 --- /dev/null +++ b/bench/Directory.Build.props @@ -0,0 +1,18 @@ + + + false + + + + + + Exe + $(LatestTargetFramework) + false + + + + + + + diff --git a/bench/Directory.Build.targets b/bench/Directory.Build.targets new file mode 100644 index 0000000000..1443711870 --- /dev/null +++ b/bench/Directory.Build.targets @@ -0,0 +1,3 @@ + + + diff --git a/bench/Generators/Microsoft.Gen.EnumStrings.PerformanceTests/EnumStrings.cs b/bench/Generators/Microsoft.Gen.EnumStrings.PerformanceTests/EnumStrings.cs new file mode 100644 index 0000000000..6ce2a3967c --- /dev/null +++ b/bench/Generators/Microsoft.Gen.EnumStrings.PerformanceTests/EnumStrings.cs @@ -0,0 +1,163 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.EnumStrings; + +namespace Microsoft.Gen.EnumStrings.Bench; + +#pragma warning disable CA1822 // Mark members as static +#pragma warning disable R9A033 // Replace uses of 'Enum.GetName' and 'Enum.ToString' with the '[EnumStrings]' code generator for improved performance + +[MemoryDiagnoser] +public class EnumStrings +{ + private static readonly int[] _randomValues = new int[1000]; + + [EnumStrings] + internal enum Color + { + Red, + Green, + Blue, + } + + [Flags] + [EnumStrings] + internal enum SmallOptions + { + Options1 = 1, + Options2 = 2, + Options4 = 4, + Options8 = 8, + Options16 = 16, + } + + [Flags] + [EnumStrings] + internal enum LargeOptions + { + Options1 = 1, + Options2 = 2, + Options4 = 4, + Options8 = 8, + Options16 = 16, + Options32 = 32, + Options64 = 64, + Options128 = 128, + } + + static EnumStrings() + { + var r = new Random(); + for (int i = 0; i < _randomValues.Length; i++) + { + _randomValues[i] = r.Next(); + } + } + + [Benchmark] + public void ToStringColor() + { + _ = Color.Red.ToString(); + _ = Color.Green.ToString(); + _ = Color.Blue.ToString(); + } + + [Benchmark] + public void GetNameColor() + { + _ = Enum.GetName(Color.Red); + _ = Enum.GetName(Color.Green); + _ = Enum.GetName(Color.Blue); + } + + [Benchmark] + public void ToInvariantStringColor() + { + _ = Color.Red.ToInvariantString(); + _ = Color.Green.ToInvariantString(); + _ = Color.Blue.ToInvariantString(); + } + + [Benchmark] + public void ToStringSmallOptions() + { + _ = SmallOptions.Options1.ToString(); + _ = SmallOptions.Options2.ToString(); + _ = SmallOptions.Options4.ToString(); + _ = SmallOptions.Options8.ToString(); + _ = SmallOptions.Options16.ToString(); + + _ = (SmallOptions.Options1 | SmallOptions.Options16).ToString(); + _ = (SmallOptions.Options2 | SmallOptions.Options4).ToString(); + } + + [Benchmark] + public void ToInvariantStringSmallOptions() + { + _ = SmallOptions.Options1.ToInvariantString(); + _ = SmallOptions.Options2.ToInvariantString(); + _ = SmallOptions.Options4.ToInvariantString(); + _ = SmallOptions.Options8.ToInvariantString(); + _ = SmallOptions.Options16.ToInvariantString(); + + _ = (SmallOptions.Options1 | SmallOptions.Options16).ToInvariantString(); + _ = (SmallOptions.Options2 | SmallOptions.Options4).ToInvariantString(); + } + + [Benchmark] + public void ToStringLargeOptions() + { + _ = LargeOptions.Options1.ToString(); + _ = LargeOptions.Options2.ToString(); + _ = LargeOptions.Options4.ToString(); + _ = LargeOptions.Options8.ToString(); + _ = LargeOptions.Options16.ToString(); + _ = LargeOptions.Options32.ToString(); + _ = LargeOptions.Options64.ToString(); + _ = LargeOptions.Options128.ToString(); + + _ = (LargeOptions.Options1 | LargeOptions.Options16).ToString(); + _ = (LargeOptions.Options2 | LargeOptions.Options4).ToString(); + } + + [Benchmark] + public void ToInvariantStringLargeOptions() + { + _ = LargeOptions.Options1.ToInvariantString(); + _ = LargeOptions.Options2.ToInvariantString(); + _ = LargeOptions.Options4.ToInvariantString(); + _ = LargeOptions.Options8.ToInvariantString(); + _ = LargeOptions.Options16.ToInvariantString(); + _ = LargeOptions.Options32.ToInvariantString(); + _ = LargeOptions.Options64.ToInvariantString(); + _ = LargeOptions.Options128.ToInvariantString(); + + _ = (LargeOptions.Options1 | LargeOptions.Options16).ToInvariantString(); + _ = (LargeOptions.Options2 | LargeOptions.Options4).ToInvariantString(); + } + + // the next two benchmarks aren't representative of expected real-world use cases, but let's see the impact the code gen has relative to naked Enum.ToString + + [Benchmark] + public void ToStringRandom() + { + for (int i = 0; i < _randomValues.Length; i++) + { + var o = (LargeOptions)_randomValues[i]; + _ = o.ToString(); + } + } + + [Benchmark] + public void ToInvariantStringRandom() + { + for (int i = 0; i < _randomValues.Length; i++) + { + var o = (LargeOptions)_randomValues[i]; + _ = o.ToInvariantString(); + } + } +} diff --git a/bench/Generators/Microsoft.Gen.EnumStrings.PerformanceTests/Microsoft.Gen.EnumStrings.PerformanceTests.csproj b/bench/Generators/Microsoft.Gen.EnumStrings.PerformanceTests/Microsoft.Gen.EnumStrings.PerformanceTests.csproj new file mode 100644 index 0000000000..af53a3cc94 --- /dev/null +++ b/bench/Generators/Microsoft.Gen.EnumStrings.PerformanceTests/Microsoft.Gen.EnumStrings.PerformanceTests.csproj @@ -0,0 +1,16 @@ + + + Microsoft.Gen.EnumStrings.PerformanceTests + Benchmarks for Gen.EnumStrings. + true + true + + + + + + + + + + diff --git a/bench/Generators/Microsoft.Gen.EnumStrings.PerformanceTests/Program.cs b/bench/Generators/Microsoft.Gen.EnumStrings.PerformanceTests/Program.cs new file mode 100644 index 0000000000..9f45ee6d5e --- /dev/null +++ b/bench/Generators/Microsoft.Gen.EnumStrings.PerformanceTests/Program.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.InProcess.Emit; + +namespace Microsoft.Gen.EnumStrings.Bench; + +internal static class Program +{ + public static void Main(string[] args) + { + var dontRequireSlnToRunBenchmarks = ManualConfig + .Create(DefaultConfig.Instance) + .AddJob(Job.MediumRun.WithToolchain(InProcessEmitToolchain.Instance)); + + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, dontRequireSlnToRunBenchmarks); + } +} diff --git a/bench/Generators/Microsoft.Gen.EnumStrings.PerformanceTests/README.md b/bench/Generators/Microsoft.Gen.EnumStrings.PerformanceTests/README.md new file mode 100644 index 0000000000..6c7f983f12 --- /dev/null +++ b/bench/Generators/Microsoft.Gen.EnumStrings.PerformanceTests/README.md @@ -0,0 +1,21 @@ +``` +BenchmarkDotNet=v0.13.2, OS=Windows 11 (10.0.22621.963) +Intel Core i7-9700K CPU 3.60GHz (Coffee Lake), 1 CPU, 8 logical and 8 physical cores +.NET SDK=7.0.100 + [Host] : .NET 7.0.1 (7.0.122.56804), X64 RyuJIT AVX2 + +Job=MediumRun Toolchain=InProcessEmitToolchain IterationCount=15 +LaunchCount=2 WarmupCount=10 + +| Method | Mean | Error | StdDev | Gen0 | Allocated | +|------------------------------ |--------------:|--------------:|--------------:|--------:|----------:| +| ToStringColor | 40.991 ns | 0.5249 ns | 0.7358 ns | 0.0114 | 72 B | +| GetNameColor | 33.143 ns | 0.4681 ns | 0.7007 ns | - | - | +| ToInvariantStringColor | 3.834 ns | 0.0178 ns | 0.0267 ns | - | - | +| ToStringSmallOptions | 135.437 ns | 1.7399 ns | 2.5503 ns | 0.0470 | 296 B | +| ToInvariantStringSmallOptions | 12.207 ns | 0.0620 ns | 0.0889 ns | - | - | +| ToStringLargeOptions | 193.580 ns | 2.6614 ns | 3.8169 ns | 0.0587 | 368 B | +| ToInvariantStringLargeOptions | 16.424 ns | 0.2146 ns | 0.3145 ns | - | - | +| ToStringRandom | 72,026.761 ns | 1,205.1409 ns | 1,728.3770 ns | 10.8643 | 68232 B | +| ToInvariantStringRandom | 64,554.832 ns | 581.9061 ns | 852.9504 ns | 8.0566 | 50758 B | +``` diff --git a/bench/Generators/Microsoft.Gen.Logging.PerformanceTests/Log.cs b/bench/Generators/Microsoft.Gen.Logging.PerformanceTests/Log.cs new file mode 100644 index 0000000000..b6deab1177 --- /dev/null +++ b/bench/Generators/Microsoft.Gen.Logging.PerformanceTests/Log.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +#pragma warning disable S109 + +namespace Microsoft.Gen.Logging.Bench; + +internal static partial class Log +{ + [LogMethod(LogLevel.Error, @"Connection id '{connectionId}' received {type} frame for stream ID {streamId} with length {length} and flags {flags} and {other}")] + public static partial void RefTypes_Error(ILogger logger, string connectionId, string type, string streamId, string length, string flags, string other); + + [LogMethod(LogLevel.Debug, @"Connection id '{connectionId}' received {type} frame for stream ID {streamId} with length {length} and flags {flags} and {other}")] + public static partial void RefTypes_Debug(ILogger logger, string connectionId, string type, string streamId, string length, string flags, string other); + + [LogMethod(LogLevel.Error, @"Range [{start}..{end}], options {options}, guid {guid}")] + public static partial void ValueTypes_Error(ILogger logger, long start, long end, int options, Guid guid); + + [LogMethod(LogLevel.Debug, @"Range [{start}..{end}], options {options}, guid {guid}")] + public static partial void ValueTypes_Debug(ILogger logger, long start, long end, int options, Guid guid); +} diff --git a/bench/Generators/Microsoft.Gen.Logging.PerformanceTests/LogMethod.cs b/bench/Generators/Microsoft.Gen.Logging.PerformanceTests/LogMethod.cs new file mode 100644 index 0000000000..14faf1163a --- /dev/null +++ b/bench/Generators/Microsoft.Gen.Logging.PerformanceTests/LogMethod.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.Logging; + +#pragma warning disable CA1822 // Mark members as static +#pragma warning disable R9A000 // Switch to updated logging methods using the [LogMethod] attribute for additional performance. + +namespace Microsoft.Gen.Logging.Bench; + +[MemoryDiagnoser] +public class LogMethod +{ + private const string ConnectionId = "0x345334534678"; + private const string Type = "some string"; + private const string StreamId = "some string some string"; + private const string Length = "some string some string some string"; + private const string Flags = "some string some string some string some string"; + private const string Other = "some string some string some string some string some string"; + private const long Start = 42; + private const long End = 123_456_789; + private const int Options = 0x1234; + + private static readonly Guid _guid = new(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); + + private static readonly Action _loggerMessage_refTypes = LoggerMessage.Define( + LogLevel.Error, + eventId: 380, + formatString: @"Connection id '{connectionId}' received {type} frame for stream ID {streamId} with length {length} and flags {flags} and {other}"); + + private static readonly Action _loggerMessage_valueTypes = LoggerMessage.Define( + LogLevel.Error, + eventId: 381, + formatString: @"Range [{start}..{end}], options {options}, guid {guid}"); + + private static readonly MockLogger _logger = new(); + + [Params(true, false)] + public bool Enabled; + + [Benchmark] + public void Classic_RefTypes() + { + _logger.Enabled = Enabled; + _logger.LogError( + @"Connection id '{connectionId}' received {type} frame for stream ID {streamId} with length {length} and flags {flags} and {other}", + ConnectionId, + Type, + StreamId, + Length, + Flags, + Other); + } + + [Benchmark] + public void Classic_ValueTypes() + { + _logger.Enabled = Enabled; + _logger.LogError(@"Range [{start}..{end}], options {options}, guid {guid}", + Start, + End, + Options, + _guid); + } + + [Benchmark] + public void LoggerMessage_RefTypes() + { + _logger.Enabled = Enabled; + _loggerMessage_refTypes(_logger, ConnectionId, Type, StreamId, Length, Flags, Other, null); + } + + [Benchmark] + public void LoggerMessage_ValueTypes() + { + _logger.Enabled = Enabled; + _loggerMessage_valueTypes(_logger, Start, End, Options, _guid, null); + } + + [Benchmark] + public void LogMethod_RefTypes_Error() + { + _logger.Enabled = Enabled; + Log.RefTypes_Error(_logger, ConnectionId, Type, StreamId, Length, Flags, Other); + } + + [Benchmark] + public void LogMethod_RefTypes_Debug() + { + _logger.Enabled = Enabled; + Log.RefTypes_Debug(_logger, ConnectionId, Type, StreamId, Length, Flags, Other); + } + + [Benchmark] + public void LogMethod_ValueTypes_Error() + { + _logger.Enabled = Enabled; + Log.ValueTypes_Error(_logger, Start, End, Options, _guid); + } + + [Benchmark] + public void LogMethod_ValueTypes_Debug() + { + _logger.Enabled = Enabled; + Log.ValueTypes_Debug(_logger, Start, End, Options, _guid); + } +} diff --git a/bench/Generators/Microsoft.Gen.Logging.PerformanceTests/Microsoft.Gen.Logging.PerformanceTests.csproj b/bench/Generators/Microsoft.Gen.Logging.PerformanceTests/Microsoft.Gen.Logging.PerformanceTests.csproj new file mode 100644 index 0000000000..13918a14f4 --- /dev/null +++ b/bench/Generators/Microsoft.Gen.Logging.PerformanceTests/Microsoft.Gen.Logging.PerformanceTests.csproj @@ -0,0 +1,13 @@ + + + Microsoft.Gen.Logging.Bench + Benchmarks for Gen.Logging. + true + true + true + + + + + + diff --git a/bench/Generators/Microsoft.Gen.Logging.PerformanceTests/MockLogger.cs b/bench/Generators/Microsoft.Gen.Logging.PerformanceTests/MockLogger.cs new file mode 100644 index 0000000000..c13f0ec2d7 --- /dev/null +++ b/bench/Generators/Microsoft.Gen.Logging.PerformanceTests/MockLogger.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Telemetry.Logging; +using Microsoft.Shared.Pools; + +namespace Microsoft.Gen.Logging.Bench; + +/// +/// A logger which captures the last log state logged to it. +/// +internal sealed class MockLogger : ILogger +{ + private readonly ObjectPool>> _listPool = PoolFactory.CreateListPool>(); + + private sealed class Disposable : IDisposable + { + public void Dispose() + { + // nothing to do + } + } + +#pragma warning disable CS8633 +#pragma warning disable CS8766 + public IDisposable? BeginScope(TState state) + where TState : notnull +#pragma warning restore CS8633 +#pragma warning restore CS8766 + { + return new Disposable(); + } + + public bool IsEnabled(LogLevel logLevel) + { + return Enabled; + } + + public bool Enabled { get; set; } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + switch (state) + { + case LogMethodHelper helper: + { + // this path is optimized in the real Logger implementation, so it is here too... + break; + } + + case IEnumerable> enumerable: + { + var l = _listPool.Get(); + + foreach (var e in enumerable) + { + // Any non-primitive value type will be turned into a string on this path. + // But when using the generated code, this conversion to string happens in the + // generated code, which eliminates the overhead of boxing the value type. + if (e.Value is Guid) + { + _ = e.Value.ToString(); + } + + l.Add(e); + } + + _listPool.Return(l); + break; + } + } + } +} diff --git a/bench/Generators/Microsoft.Gen.Logging.PerformanceTests/Program.cs b/bench/Generators/Microsoft.Gen.Logging.PerformanceTests/Program.cs new file mode 100644 index 0000000000..6706d8c1c6 --- /dev/null +++ b/bench/Generators/Microsoft.Gen.Logging.PerformanceTests/Program.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.InProcess.Emit; + +namespace Microsoft.Gen.Logging.Bench; + +internal static class Program +{ + public static void Main(string[] args) + { + var dontRequireSlnToRunBenchmarks = ManualConfig + .Create(DefaultConfig.Instance) + .AddJob(Job.MediumRun.WithToolchain(InProcessEmitToolchain.Instance)); + + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, dontRequireSlnToRunBenchmarks); + } +} diff --git a/bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/Constants.cs b/bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/Constants.cs new file mode 100644 index 0000000000..55b6b65b0d --- /dev/null +++ b/bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/Constants.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Telemetry; + +internal static class Constants +{ + public const string AttributeHttpPath = "http.path"; + public const string AttributeHttpRoute = "http.route"; + public const string AttributeHttpTarget = "http.target"; + public const string AttributeHttpUrl = "http.url"; + public const string Redacted = "redacted"; + public const string CustomPropertyHttpRequest = "Tracing.CustomProperty.HttpRequest"; + public const string ActivityStartEvent = "OnStartActivity"; +} diff --git a/bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/Microsoft.AspNetCore.Telemetry.PerformanceTests.csproj b/bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/Microsoft.AspNetCore.Telemetry.PerformanceTests.csproj new file mode 100644 index 0000000000..aed8531f66 --- /dev/null +++ b/bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/Microsoft.AspNetCore.Telemetry.PerformanceTests.csproj @@ -0,0 +1,15 @@ + + + Microsoft.AspNetCore.Telemetry.Bench + Benchmarks for Microsoft.AspNetCore.Telemetry. + + + + true + + + + + + + \ No newline at end of file diff --git a/bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/Program.cs b/bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/Program.cs new file mode 100644 index 0000000000..9e5646cf86 --- /dev/null +++ b/bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/Program.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.InProcess.Emit; + +namespace Microsoft.AspNetCore.Telemetry.Bench; + +internal static class Program +{ + public static void Main(string[] args) + { + var dontRequireSlnToRunBenchmarks = ManualConfig + .Create(DefaultConfig.Instance) + .AddJob(Job.MediumRun.WithToolchain(InProcessEmitToolchain.Instance)); + + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, dontRequireSlnToRunBenchmarks); + } +} diff --git a/bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/RedactionBenchmark.cs b/bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/RedactionBenchmark.cs new file mode 100644 index 0000000000..938ba55b99 --- /dev/null +++ b/bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/RedactionBenchmark.cs @@ -0,0 +1,346 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text; +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.Pools; + +namespace Microsoft.AspNetCore.Telemetry.Bench; + +[GcServer(true)] +[MinColumn] +[MaxColumn] +[MemoryDiagnoser] +public class RedactionBenchmark +{ + private readonly HttpTracingOptions _options; + private readonly string _httpPath; + private readonly ObjectPool _stringBuilderPool; + private readonly Dictionary _routeValues = new() + { + { "userId", "testUserId" }, + { "chatId", "testChatId" }, + }; + + public RedactionBenchmark() + { + _options = new HttpTracingOptions(); + _options.RouteParameterDataClasses.Add("userId", SimpleClassifications.PrivateData); + _options.RouteParameterDataClasses.Add("chatId", SimpleClassifications.PrivateData); + + _httpPath = "/users/{userId}/chats/{chatId}/test1/test2/{userId}"; + _stringBuilderPool = PoolFactory.CreateStringBuilderPool(); + } + + private static RouteSegment[] GetRouteSegments(string httpRoute) + { + var routeSegments = new List(); + + int startIndex = 0; + while (startIndex < httpRoute.Length) + { + var startIndexOfParam = httpRoute.IndexOf('{', startIndex); + if (startIndexOfParam == -1) + { + // We have reached to the end of the segment, no more parameters + routeSegments.Add(new RouteSegment(httpRoute.Substring(startIndex), false)); + break; + } + + var endIndexOfParam = httpRoute.IndexOf('}', startIndexOfParam); + + var routeNonParamameterSegment = httpRoute.Substring(startIndex, startIndexOfParam - startIndex); + var routeParameterSegment = httpRoute.Substring(startIndexOfParam + 1, endIndexOfParam - startIndexOfParam - 1); + + routeSegments.Add(new RouteSegment(routeNonParamameterSegment, false)); + routeSegments.Add(new RouteSegment(routeParameterSegment, true)); + + startIndex = endIndexOfParam + 1; + } + + return routeSegments.ToArray(); + } + + [Benchmark] + public void RedactHttpPathStringBuilderNETStd() + { + Span destinationBuffer = stackalloc char[256]; + var startIndex = 0; + var span = _httpPath.AsSpan(); + var isRouteKeyFound = false; + var pathStringBuilder = _stringBuilderPool.Get(); + ReadOnlySpan segment; + try + { + for (int i = 0; i <= span.Length; i++) + { + if (i == span.Length || span[i] == '/') + { + segment = span.Slice(startIndex, i - startIndex); + + foreach (var item in _routeValues) + { + if (((string)item.Value!).AsSpan().SequenceEqual(segment)) + { + if (_options.RouteParameterDataClasses.TryGetValue(item.Key, out DataClassification classification)) + { + pathStringBuilder.Append(Redact(segment, destinationBuffer)); + isRouteKeyFound = true; + } + + break; + } + } + + if (!isRouteKeyFound) + { + pathStringBuilder.Append(segment); + } + + if (i < span.Length) + { + _ = pathStringBuilder.Append('/'); + } + + startIndex = i + 1; + + isRouteKeyFound = true; + } + } + + _ = pathStringBuilder.ToString(); + } + finally + { + _stringBuilderPool.Return(pathStringBuilder); + } + } + + [Benchmark] + public void RedactHttpPathStringBuilderOptimizedForSpeedNETStd() + { + Span destinationBuffer = stackalloc char[256]; + var startIndex = 0; + var span = _httpPath.AsSpan(); + var pathStringBuilder = _stringBuilderPool.Get(); + + var newDict = new Dictionary(_routeValues.Count); + foreach (var item in _routeValues) + { + if (item.Value is not null) + { + newDict.Add((string)item.Value, item.Key); + } + } + + try + { + for (int i = 0; i <= span.Length; i++) + { + if (i == span.Length || span[i] == '/') + { + var segment = span.Slice(startIndex, i - startIndex).ToString(); + if (newDict.TryGetValue(segment, out var value) && + _options.RouteParameterDataClasses.TryGetValue(value, out DataClassification classification)) + { + _ = pathStringBuilder.Append(Redact(segment, destinationBuffer)); + } + else + { + _ = pathStringBuilder.Append(segment); + } + + if (i < span.Length) + { + _ = pathStringBuilder.Append('/'); + } + + startIndex = i + 1; + } + } + + _ = pathStringBuilder.ToString(); + } + finally + { + _stringBuilderPool.Return(pathStringBuilder); + } + } + + [Benchmark] + public void RedactHttpPathUsingIndexOfNETStd() + { + Span destinationBuffer = stackalloc char[256]; + var span = _httpPath.AsSpan(); + var isRouteKeyFound = false; + var pathStringBuilder = _stringBuilderPool.Get(); + try + { + var startIndex = 0; + var endIndex = 0; + var segment = span; + + while (startIndex < span.Length) + { + _ = pathStringBuilder.Append('/'); + startIndex = span.Slice(startIndex).IndexOf('/') + startIndex; + endIndex = span.Slice(startIndex + 1).IndexOf('/') + startIndex; + if (endIndex < startIndex) + { + endIndex = span.Length - 1; + } + + segment = span.Slice(startIndex + 1, endIndex - startIndex); + if (segment.Length > 0) + { + foreach (var item in _routeValues) + { + if (((string)item.Value!).AsSpan().SequenceEqual(segment)) + { + if (_options.RouteParameterDataClasses.TryGetValue(item.Key, out DataClassification classification)) + { + _ = pathStringBuilder.Append(Redact(segment, destinationBuffer)); + isRouteKeyFound = true; + } + + break; + } + } + + if (!isRouteKeyFound) + { + _ = pathStringBuilder.Append(segment); + } + } + + startIndex = endIndex + 1; + + isRouteKeyFound = false; + } + + _ = pathStringBuilder.ToString(); + } + finally + { + _stringBuilderPool.Return(pathStringBuilder); + } + } + + private static ReadOnlySpan Redact(ReadOnlySpan source, Span destination) + { + return destination.Slice(0, source.Length); + } + + private static int[] GetPathSegments(ReadOnlySpan httpPath) + { + var numSegments = 0; + for (var i = 0; i < httpPath.Length; i++) + { + if (httpPath[i] == '/') + { + numSegments++; + } + } + + var routeSegments = new int[numSegments + 1]; + var j = 0; + for (var i = 0; i < httpPath.Length; i++) + { + if (httpPath[i] == '/') + { + routeSegments[j] = i + 1; + j++; + } + } + + routeSegments[numSegments] = httpPath.Length + 1; + return routeSegments; + } + + [Benchmark] + public void RedactHttpPathWithSegmentsNETStd() + { + Span destinationBuffer = stackalloc char[256]; + var span = _httpPath.AsSpan(); + var segments = GetPathSegments(span); + var isRouteKeyFound = false; + var pathStringBuilder = _stringBuilderPool.Get(); + try + { + for (int i = 1; i < segments.Length; i++) + { + _ = pathStringBuilder.Append('/'); + var segment = span.Slice(segments[i - 1], segments[i] - segments[i - 1] - 1); + foreach (var item in _routeValues) + { + if (((string)item.Value!).AsSpan().SequenceEqual(segment)) + { + if (_options.RouteParameterDataClasses.TryGetValue(item.Key, out DataClassification classification)) + { + _ = pathStringBuilder.Append(Redact(segment, destinationBuffer)); + isRouteKeyFound = true; + } + + break; + } + } + + if (!isRouteKeyFound) + { + _ = pathStringBuilder.Append(segment); + } + + isRouteKeyFound = false; + } + + _ = pathStringBuilder.ToString(); + } + finally + { + _stringBuilderPool.Return(pathStringBuilder); + } + } + + [Benchmark] + public void RedactHttpRouteNETCore() + { + Span destinationBuffer = stackalloc char[256]; + var routeSegments = GetRouteSegments(_httpPath); + var pathStringBuilder = _stringBuilderPool.Get(); + try + { + foreach (var routeSegment in routeSegments) + { + if (routeSegment.IsParameter) + { + if (_routeValues.TryGetValue(routeSegment.Segment, out var paramValue)) + { + if (_options.RouteParameterDataClasses.TryGetValue(routeSegment.Segment, out DataClassification classification)) + { + _ = pathStringBuilder.Append(Redact((string)paramValue!, destinationBuffer)); + } + else + { + _ = pathStringBuilder.Append(paramValue); + } + } + } + else + { + _ = pathStringBuilder.Append(routeSegment.Segment); + } + } + + _ = pathStringBuilder.ToString(); + } + finally + { + _stringBuilderPool.Return(pathStringBuilder); + } + } +} diff --git a/bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/RouteSegment.cs b/bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/RouteSegment.cs new file mode 100644 index 0000000000..d0ceb055d0 --- /dev/null +++ b/bench/Libraries/Microsoft.AspNetCore.Telemetry.PerformanceTests/RouteSegment.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Telemetry; + +internal readonly struct RouteSegment +{ + public RouteSegment(string segment, bool isParameter) + { + Segment = segment; + IsParameter = isParameter; + } + + public string Segment { get; } + + public bool IsParameter { get; } +} diff --git a/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/Benchmark.cs b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/Benchmark.cs new file mode 100644 index 0000000000..fb565f14cb --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/Benchmark.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Http.Resilience.Bench; + +public class Benchmark +{ + private static HttpRequestMessage Request => new(HttpMethod.Post, "https://bogus"); + + private System.Net.Http.HttpClient _client = null!; + + [GlobalSetup] + public void GlobalSetup() + { + var type = RoutingStrategy; + + if (ManyRoutes) + { + type |= HedgingClientType.ManyRoutes; + } + + var serviceProvider = HttpClientFactory.InitializeServiceProvider(type); + var factory = serviceProvider.GetRequiredService(); +#pragma warning disable R9A033 // Replace uses of 'Enum.GetName' and 'Enum.ToString' with the '[EnumStrings]' code generator for improved performance + _client = factory.CreateClient(type.ToString()); +#pragma warning restore R9A033 // Replace uses of 'Enum.GetName' and 'Enum.ToString' with the '[EnumStrings]' code generator for improved performance + } + + [Params(HedgingClientType.Weighted, HedgingClientType.Ordered)] + public HedgingClientType RoutingStrategy { get; set; } + + [Params(true, false)] + public bool ManyRoutes { get; set; } + + [Benchmark] + public Task HedgingCall() + { + return _client.SendAsync(Request, CancellationToken.None); + } +} diff --git a/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/FaultInjectionRequestBenchmarks.cs b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/FaultInjectionRequestBenchmarks.cs new file mode 100644 index 0000000000..0a9fff3338 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/FaultInjectionRequestBenchmarks.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Resilience.FaultInjection; +using Microsoft.Extensions.Telemetry.Metering; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.PerformanceTests; + +public class FaultInjectionRequestBenchmarks +{ + private const string HttpClientIdentifier = "HttpClientClass"; + private static readonly Uri _defaultUrl = new("https://www.google.ca/"); + private ServiceProvider _serviceProvider = null!; + private System.Net.Http.HttpClient _client = null!; + + [GlobalSetup] + public void GlobalSetup() + { + var services = new ServiceCollection(); + + if (SetupFaultInjection) + { + Action action = builder => + builder + .Configure(option => + { + option.ChaosPolicyOptionsGroups = new Dictionary(); + }); + + services + .RegisterMetering() + .AddHttpClientFaultInjection(action); + } + + services.AddHttpClient(HttpClientIdentifier); + + _serviceProvider = services.BuildServiceProvider(); + var clientFactory = _serviceProvider.GetRequiredService(); + _client = clientFactory.CreateClient(HttpClientIdentifier); + } + + [GlobalCleanup] + public void GlobalCleanup() + { + _serviceProvider.Dispose(); + } + + [Params( + true, + false)] + public bool SetupFaultInjection { get; set; } + + [Benchmark] + public async Task SendAsync() + { + using var request = new HttpRequestMessage(HttpMethod.Get, _defaultUrl); + await _client.SendAsync(request, HttpCompletionOption.ResponseContentRead, CancellationToken.None).ConfigureAwait(false); + } +} diff --git a/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/HttpClientFactory.cs b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/HttpClientFactory.cs new file mode 100644 index 0000000000..6d36543189 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/HttpClientFactory.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Telemetry.Metering; + +#pragma warning disable R9A033 // Replace uses of 'Enum.GetName' and 'Enum.ToString' with the '[EnumStrings]' code generator for improved performance + +namespace Microsoft.Extensions.Http.Resilience.Bench; + +[Flags] +[SuppressMessage("Performance", "R9A031:Make types declared in an executable internal", Justification = "Needs to be public for BenchmarkDotNet consumption")] +public enum HedgingClientType +{ + Weighted = 1 << 0, + Ordered = 1 << 1, + ManyRoutes = 1 << 2, +} + +internal static class HttpClientFactory +{ + private const string HedgingEndpoint1 = "http://localhost1"; + private const string HedgingEndpoint2 = "http://localhost2"; + + public static ServiceProvider InitializeServiceProvider(HedgingClientType clientType) + { + var services = new ServiceCollection(); + services + .RegisterMetering() + .AddSingleton(NullRedactorProvider.Instance) + .AddTransient() + .AddHedging(clientType); + + return services.BuildServiceProvider(); + } + + private static IServiceCollection AddHedging(this IServiceCollection services, HedgingClientType clientType) + { + var clientBuilder = services.AddHttpClient(clientType.ToString()); + var hedgingBuilder = clientBuilder.AddStandardHedgingHandler().SelectPipelineByAuthority(SimpleClassifications.PublicData); + _ = clientBuilder.AddHttpMessageHandler(); + + int routes = clientType.HasFlag(HedgingClientType.ManyRoutes) ? 50 : 2; + + if (clientType.HasFlag(HedgingClientType.Ordered)) + { + hedgingBuilder.RoutingStrategyBuilder.ConfigureOrderedGroups(options => + { + options.Groups = Enumerable.Repeat(0, routes).Select(_ => + { + return new EndpointGroup + { + Endpoints = new[] + { + new WeightedEndpoint + { + Uri = new Uri(HedgingEndpoint1) + }, + new WeightedEndpoint + { + Uri = new Uri(HedgingEndpoint2) + } + } + }; + }).ToArray(); + }); + } + else + { + hedgingBuilder.RoutingStrategyBuilder.ConfigureWeightedGroups(options => + { + options.Groups = Enumerable.Repeat(0, routes).Select(_ => + { + return new WeightedEndpointGroup + { + Endpoints = new[] + { + new WeightedEndpoint + { + Uri = new Uri(HedgingEndpoint1) + }, + new WeightedEndpoint + { + Uri = new Uri(HedgingEndpoint2) + } + } + }; + }).ToArray(); + }); + } + + return services; + } +} diff --git a/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/Microsoft.Extensions.Http.Resilience.PerformanceTests.csproj b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/Microsoft.Extensions.Http.Resilience.PerformanceTests.csproj new file mode 100644 index 0000000000..d914962cc9 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/Microsoft.Extensions.Http.Resilience.PerformanceTests.csproj @@ -0,0 +1,13 @@ + + + Microsoft.Extensions.Http.Resilience.FaultInjection.PerformanceTests + Benchmarks for Microsoft.Extensions.Http.Resilience. + + + + + + + + + diff --git a/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/NoRemoteCallHandler.cs b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/NoRemoteCallHandler.cs new file mode 100644 index 0000000000..a2d9a00f34 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/NoRemoteCallHandler.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Http.Resilience.Bench; + +internal sealed class NoRemoteCallHandler : DelegatingHandler +{ + private readonly Task _completedResponse; + + public NoRemoteCallHandler() + { + _completedResponse = Task.FromResult(new HttpResponseMessage + { + StatusCode = System.Net.HttpStatusCode.OK + }); + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { +#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks + return _completedResponse; +#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks + } +} diff --git a/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/Program.cs b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/Program.cs new file mode 100644 index 0000000000..af0e0367af --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/Program.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.InProcess.Emit; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Benchmark +{ + internal static class Program + { + private static void Main(string[] args) + { + var doNotRequireSlnToRunBenchmarks = ManualConfig + .Create(DefaultConfig.Instance) + .AddJob(Job.MediumRun.WithToolchain(InProcessEmitToolchain.Instance).WithIterationCount(100)) + .AddDiagnoser(MemoryDiagnoser.Default); + + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, doNotRequireSlnToRunBenchmarks); + } + } +} diff --git a/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/BenchEnricher.cs b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/BenchEnricher.cs new file mode 100644 index 0000000000..f0e1ebb9f0 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/BenchEnricher.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Bench; + +internal sealed class BenchEnricher : IHttpClientLogEnricher +{ + private const string Key = "Performance in R9"; + private const string Value = "is paramount."; + + public void Enrich(IEnrichmentPropertyBag enrichmentBag, HttpRequestMessage? request = null, + HttpResponseMessage? response = null) + { + if (request is not null) + { + enrichmentBag.Add(Key, Value); + } + } +} diff --git a/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Benchmarks/HugeHttpCLientLoggingBenchmark.cs b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Benchmarks/HugeHttpCLientLoggingBenchmark.cs new file mode 100644 index 0000000000..6a1c0a2c40 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Benchmarks/HugeHttpCLientLoggingBenchmark.cs @@ -0,0 +1,202 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Bench.Benchmarks; + +[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Bench")] +public class HugeHttpClientLoggingBenchmark +{ + private const string DataFileName = "HugeBody.txt"; + private const int ReadSizeLimit = 53248; + private static HttpRequestMessage Request => new(HttpMethod.Post, "https://www.microsoft.com"); + + private static readonly System.Net.Http.HttpClient _hugeNoLog + = HttpClientFactory.CreateWithoutLogging(DataFileName); + + private static readonly System.Net.Http.HttpClient _hugeLogAll + = HttpClientFactory.CreateWithLoggingLogAll(DataFileName, ReadSizeLimit); + + private static readonly System.Net.Http.HttpClient _hugeLogRequest + = HttpClientFactory.CreateWithLoggingLogRequest(DataFileName, ReadSizeLimit); + + private static readonly System.Net.Http.HttpClient _hugeLogResponse + = HttpClientFactory.CreateWithLoggingLogResponse(DataFileName, ReadSizeLimit); + + private static readonly System.Net.Http.HttpClient _hugeNoLogChunked + = HttpClientFactory.CreateWithoutLogging_ChunkedEncoding(DataFileName); + + private static readonly System.Net.Http.HttpClient _hugeLogAllChunked + = HttpClientFactory.CreateWithLoggingLogAll_ChunkedEncoding(DataFileName, ReadSizeLimit); + + private static readonly System.Net.Http.HttpClient _hugeLogRequestChunked + = HttpClientFactory.CreateWithLoggingLogRequest_ChunkedEncoding(DataFileName, ReadSizeLimit); + + private static readonly System.Net.Http.HttpClient _hugeLogResponseChunked + = HttpClientFactory.CreateWithLoggingLogResponse_ChunkedEncoding(DataFileName, ReadSizeLimit); + + [Benchmark(Baseline = true)] + [BenchmarkCategory("Seekable")] + public async Task Huge_No_Log_HeadersRead() + { + var response = await _hugeNoLog.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Huge_No_Log_ContentRead() + { + var response = await _hugeNoLog.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Huge_Log_All_HeadersRead() + { + var response = await _hugeLogAll.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Huge_Log_All_ContentRead() + { + var response = await _hugeLogAll.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Huge_Log_Request_HeadersRead() + { + var response = await _hugeLogRequest.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Huge_Log_Request_ContentRead() + { + var response = await _hugeLogRequest.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Huge_Log_Response_HeadersRead() + { + var response = await _hugeLogResponse.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Huge_Log_Response_ContentRead() + { + var response = await _hugeLogResponse.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Huge_No_Log_HeadersRead_ChunkedEncoding() + { + var response = await _hugeNoLogChunked.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Huge_No_Log_ContentRead_ChunkedEncoding() + { + var response = await _hugeNoLogChunked.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Huge_Log_All_HeadersRead_ChunkedEncoding() + { + var response = await _hugeLogAllChunked.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Huge_Log_All_ContentRead_ChunkedEncoding() + { + var response = await _hugeLogAllChunked.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Huge_Log_Request_HeadersRead_ChunkedEncoding() + { + var response = await _hugeLogRequestChunked.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Huge_Log_Request_ContentRead_ChunkedEncoding() + { + var response = await _hugeLogRequestChunked.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Huge_Log_Response_HeadersRead_ChunkedEncoding() + { + var response = await _hugeLogResponseChunked.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Huge_Log_Response_ContentRead_ChunkedEncoding() + { + var response = await _hugeLogResponseChunked.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } +} diff --git a/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Benchmarks/MediumHttpClientLoggingBenchmark.cs b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Benchmarks/MediumHttpClientLoggingBenchmark.cs new file mode 100644 index 0000000000..40cdad4041 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Benchmarks/MediumHttpClientLoggingBenchmark.cs @@ -0,0 +1,202 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Bench.Benchmarks; + +[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Bench")] +public class MediumHttpClientLoggingBenchmark +{ + private const string DataFileName = "MediumBody.txt"; + private const int ReadSizeLimit = 16384; + private static HttpRequestMessage Request => new(HttpMethod.Post, "https://www.microsoft.com"); + + private static readonly System.Net.Http.HttpClient _mediumNoLog + = HttpClientFactory.CreateWithoutLogging(DataFileName); + + private static readonly System.Net.Http.HttpClient _mediumLogAll + = HttpClientFactory.CreateWithLoggingLogAll(DataFileName, ReadSizeLimit); + + private static readonly System.Net.Http.HttpClient _mediumLogRequest + = HttpClientFactory.CreateWithLoggingLogRequest(DataFileName, ReadSizeLimit); + + private static readonly System.Net.Http.HttpClient _mediumLogResponse + = HttpClientFactory.CreateWithLoggingLogResponse(DataFileName, ReadSizeLimit); + + private static readonly System.Net.Http.HttpClient _mediumNoLogChunked + = HttpClientFactory.CreateWithoutLogging_ChunkedEncoding(DataFileName); + + private static readonly System.Net.Http.HttpClient _mediumLogAllChunked + = HttpClientFactory.CreateWithLoggingLogAll_ChunkedEncoding(DataFileName, ReadSizeLimit); + + private static readonly System.Net.Http.HttpClient _mediumLogRequestChunked + = HttpClientFactory.CreateWithLoggingLogRequest_ChunkedEncoding(DataFileName, ReadSizeLimit); + + private static readonly System.Net.Http.HttpClient _mediumLogResponseChunked + = HttpClientFactory.CreateWithLoggingLogResponse_ChunkedEncoding(DataFileName, ReadSizeLimit); + + [Benchmark(Baseline = true)] + [BenchmarkCategory("Seekable")] + public async Task Medium_No_Log_HeadersRead() + { + var response = await _mediumNoLog.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Medium_No_Log_ContentRead() + { + var response = await _mediumNoLog.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Medium_Log_All_HeadersRead() + { + var response = await _mediumLogAll.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Medium_Log_All_ContentRead() + { + var response = await _mediumLogAll.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Medium_Log_Request_HeadersRead() + { + var response = await _mediumLogRequest.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Medium_Log_Request_ContentRead() + { + var response = await _mediumLogRequest.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Medium_Log_Response_HeadersRead() + { + var response = await _mediumLogResponse.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Medium_Log_Response_ContentRead() + { + var response = await _mediumLogResponse.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Medium_No_Log_HeadersRead_ChunkedEncoding() + { + var response = await _mediumNoLogChunked.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Medium_No_Log_ContentRead_ChunkedEncoding() + { + var response = await _mediumNoLogChunked.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Medium_Log_All_HeadersRead_ChunkedEncoding() + { + var response = await _mediumLogAllChunked.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Medium_Log_All_ContentRead_ChunkedEncoding() + { + var response = await _mediumLogAllChunked.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Medium_Log_Request_HeadersRead_ChunkedEncoding() + { + var response = await _mediumLogRequestChunked.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Medium_Log_Request_ContentRead_ChunkedEncoding() + { + var response = await _mediumLogRequestChunked.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Medium_Log_Response_HeadersRead_ChunkedEncoding() + { + var response = await _mediumLogResponseChunked.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Medium_Log_Response_ContentRead_ChunkedEncoding() + { + var response = await _mediumLogResponseChunked.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } +} diff --git a/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Benchmarks/SmallHttpClientLoggingBenchmark.cs b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Benchmarks/SmallHttpClientLoggingBenchmark.cs new file mode 100644 index 0000000000..8e01a16e8a --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Benchmarks/SmallHttpClientLoggingBenchmark.cs @@ -0,0 +1,202 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Bench.Benchmarks; + +[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Bench")] +public class SmallHttpClientLoggingBenchmark +{ + private const string DataFileName = "SmallBody.txt"; + private const int ReadSizeLimit = 8192; + private static HttpRequestMessage Request => new(HttpMethod.Post, "https://www.microsoft.com"); + + private static readonly System.Net.Http.HttpClient _smallNoLog + = HttpClientFactory.CreateWithoutLogging(DataFileName); + + private static readonly System.Net.Http.HttpClient _smallLogAll + = HttpClientFactory.CreateWithLoggingLogAll(DataFileName, ReadSizeLimit); + + private static readonly System.Net.Http.HttpClient _smallLogRequest + = HttpClientFactory.CreateWithLoggingLogRequest(DataFileName, ReadSizeLimit); + + private static readonly System.Net.Http.HttpClient _smallLogResponse + = HttpClientFactory.CreateWithLoggingLogResponse(DataFileName, ReadSizeLimit); + + private static readonly System.Net.Http.HttpClient _smallNoLogChunked + = HttpClientFactory.CreateWithoutLogging_ChunkedEncoding(DataFileName); + + private static readonly System.Net.Http.HttpClient _smallLogAllChunked + = HttpClientFactory.CreateWithLoggingLogAll_ChunkedEncoding(DataFileName, ReadSizeLimit); + + private static readonly System.Net.Http.HttpClient _smallLogRequestChunked + = HttpClientFactory.CreateWithLoggingLogRequest_ChunkedEncoding(DataFileName, ReadSizeLimit); + + private static readonly System.Net.Http.HttpClient _smallLogResponseChunked + = HttpClientFactory.CreateWithLoggingLogResponse_ChunkedEncoding(DataFileName, ReadSizeLimit); + + [Benchmark(Baseline = true)] + [BenchmarkCategory("Seekable")] + public async Task Small_No_Log_HeadersRead() + { + var response = await _smallNoLog.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Small_No_Log_ContentRead() + { + var response = await _smallNoLog.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Small_Log_All_HeadersRead() + { + var response = await _smallLogAll.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Small_Log_All_ContentRead() + { + var response = await _smallLogAll.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Small_Log_Request_HeadersRead() + { + var response = await _smallLogRequest.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Small_Log_Request_ContentRead() + { + var response = await _smallLogRequest.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Small_Log_Response_HeadersRead() + { + var response = await _smallLogResponse.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("Seekable")] + public async Task Small_Log_Response_ContentRead() + { + var response = await _smallLogResponse.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Small_No_Log_HeadersRead_ChunkedEncoding() + { + var response = await _smallNoLogChunked.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Small_No_Log_ContentRead_ChunkedEncoding() + { + var response = await _smallNoLogChunked.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Small_Log_All_HeadersRead_ChunkedEncoding() + { + var response = await _smallLogAllChunked.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Small_Log_All_ContentRead_ChunkedEncoding() + { + var response = await _smallLogAllChunked.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Small_Log_Request_HeadersRead_ChunkedEncoding() + { + var response = await _smallLogRequestChunked.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Small_Log_Request_ContentRead_ChunkedEncoding() + { + var response = await _smallLogRequestChunked.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Small_Log_Response_HeadersRead_ChunkedEncoding() + { + var response = await _smallLogResponseChunked.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } + + [Benchmark] + [BenchmarkCategory("NonSeekable")] + public async Task Small_Log_Response_ContentRead_ChunkedEncoding() + { + var response = await _smallLogResponseChunked.SendAsync(Request, HttpCompletionOption.ResponseContentRead, CancellationToken.None) + .ConfigureAwait(false); + + return response; + } +} diff --git a/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/DropMessageLogger.cs b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/DropMessageLogger.cs new file mode 100644 index 0000000000..2aa5924efa --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/DropMessageLogger.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Bench; + +internal sealed class DropMessageLogger : ILogger +{ + internal static readonly Func CreateLogRecord + = (_, _, _, _) => null; + +#pragma warning disable CS8633 +#pragma warning disable CS8766 + public IDisposable? BeginScope(TState state) + where TState : notnull => null; +#pragma warning restore CS8633 +#pragma warning restore CS8766 + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { +#pragma warning disable CS8604 // Possible null reference argument. + _ = CreateLogRecord(logLevel, eventId, state, exception); +#pragma warning restore CS8604 // Possible null reference argument. + } +} diff --git a/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/DropMessageLoggerProvider.cs b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/DropMessageLoggerProvider.cs new file mode 100644 index 0000000000..72f180ac31 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/DropMessageLoggerProvider.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Bench; + +internal sealed class DropMessageLoggerProvider : ILoggerProvider +{ + public ILogger CreateLogger(string categoryName) => new DropMessageLogger(); + + [SuppressMessage("Critical Code Smell", "S1186:Methods should not be empty", + Justification = "Noop")] + public void Dispose() + { + } +} diff --git a/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/HttpClientFactory.cs b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/HttpClientFactory.cs new file mode 100644 index 0000000000..70e46d50e8 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/HttpClientFactory.cs @@ -0,0 +1,180 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Bench; + +internal static class HttpClientFactory +{ + public static System.Net.Http.HttpClient CreateWithLoggingLogRequest(string fileName, int readLimit) + { + var services = new ServiceCollection(); + + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + return services + .AddFakeRedaction() + .AddSingleton(_ => NoRemoteCallHandler.Create(fileName)) + .AddHttpClientLogEnricher() + .AddHttpClient(nameof(fileName)) + .AddHttpClientLogging(options => + { + options.BodySizeLimit = readLimit; + options.RequestBodyContentTypes.Add(new("application/json")); + options.RequestHeadersDataClasses.Add("Content-Type", SimpleClassifications.PrivateData); + }) + .AddHttpMessageHandler() + .Services + .BuildServiceProvider() + .GetRequiredService() + .CreateClient(nameof(fileName)); + } + + public static System.Net.Http.HttpClient CreateWithLoggingLogResponse(string fileName, int readLimit) + { + var services = new ServiceCollection(); + + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + return services + .AddFakeRedaction() + .AddSingleton(_ => NoRemoteCallHandler.Create(fileName)) + .AddHttpClient(nameof(fileName)) + .AddHttpClientLogging(options => + { + options.BodySizeLimit = readLimit; + options.ResponseBodyContentTypes.Add(new("application/json")); + options.ResponseHeadersDataClasses.Add("Content-Type", SimpleClassifications.PrivateData); + }) + .AddHttpMessageHandler() + .Services + .BuildServiceProvider() + .GetRequiredService() + .CreateClient(nameof(fileName)); + } + + public static System.Net.Http.HttpClient CreateWithLoggingLogAll(string fileName, int readLimit) + { + var services = new ServiceCollection(); + + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + return services + .AddFakeRedaction() + .AddSingleton(_ => NoRemoteCallHandler.Create(fileName)) + .AddHttpClient(nameof(fileName)) + .AddHttpClientLogging(options => + { + options.BodySizeLimit = readLimit; + + options.RequestBodyContentTypes.Add(new("application/json")); + options.RequestHeadersDataClasses.Add("Content-Type", SimpleClassifications.PrivateData); + + options.ResponseBodyContentTypes.Add(new("application/json")); + options.ResponseHeadersDataClasses.Add("Content-Type", SimpleClassifications.PrivateData); + }) + .AddHttpMessageHandler() + .Services + .BuildServiceProvider() + .GetRequiredService() + .CreateClient(nameof(fileName)); + } + + public static System.Net.Http.HttpClient CreateWithLoggingLogRequest_ChunkedEncoding(string fileName, int readLimit) + { + var services = new ServiceCollection(); + + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + return services + .AddFakeRedaction() + .AddSingleton(_ => NoRemoteCallNotSeekableHandler.Create(fileName)) + .AddHttpClient(nameof(fileName)) + .AddHttpClientLogging(options => + { + options.BodySizeLimit = readLimit; + options.RequestBodyContentTypes.Add("application/json"); + options.RequestHeadersDataClasses.Add("Content-Type", SimpleClassifications.PrivateData); + }) + .AddHttpMessageHandler() + .Services + .BuildServiceProvider() + .GetRequiredService() + .CreateClient(nameof(fileName)); + } + + public static System.Net.Http.HttpClient CreateWithLoggingLogResponse_ChunkedEncoding(string fileName, int readLimit) + { + var services = new ServiceCollection(); + + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + return services + .AddFakeRedaction() + .AddSingleton(_ => NoRemoteCallNotSeekableHandler.Create(fileName)) + .AddHttpClient(nameof(fileName)) + .AddHttpClientLogging(options => + { + options.BodySizeLimit = readLimit; + options.ResponseBodyContentTypes.Add("application/json"); + options.ResponseHeadersDataClasses.Add("Content-Type", SimpleClassifications.PrivateData); + }) + .AddHttpMessageHandler() + .Services + .BuildServiceProvider() + .GetRequiredService() + .CreateClient(nameof(fileName)); + } + + public static System.Net.Http.HttpClient CreateWithLoggingLogAll_ChunkedEncoding(string fileName, int readLimit) + { + var services = new ServiceCollection(); + + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + return services + .AddFakeRedaction() + .AddSingleton(_ => NoRemoteCallNotSeekableHandler.Create(fileName)) + .AddHttpClient(nameof(fileName)) + .AddHttpClientLogging(options => + { + options.BodySizeLimit = readLimit; + + options.RequestBodyContentTypes.Add("application/json"); + options.RequestHeadersDataClasses.Add("Content-Type", SimpleClassifications.PrivateData); + + options.ResponseBodyContentTypes.Add("application/json"); + options.ResponseHeadersDataClasses.Add("Content-Type", SimpleClassifications.PrivateData); + }) + .AddHttpMessageHandler() + .Services + .BuildServiceProvider() + .GetRequiredService() + .CreateClient(nameof(fileName)); + } + + public static System.Net.Http.HttpClient CreateWithoutLogging(string fileName) + => new ServiceCollection() + .AddSingleton(_ => NoRemoteCallHandler.Create(fileName)) + .AddHttpClient(nameof(fileName)) + .AddHttpMessageHandler() + .Services + .BuildServiceProvider() + .GetRequiredService() + .CreateClient(nameof(fileName)); + + public static System.Net.Http.HttpClient CreateWithoutLogging_ChunkedEncoding(string fileName) + => new ServiceCollection() + .AddSingleton(_ => NoRemoteCallNotSeekableHandler.Create(fileName)) + .AddHttpClient(nameof(fileName)) + .AddHttpMessageHandler() + .Services + .BuildServiceProvider() + .GetRequiredService() + .CreateClient(nameof(fileName)); +} diff --git a/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/HugeBody.txt b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/HugeBody.txt new file mode 100644 index 0000000000..751b3f4110 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/HugeBody.txt @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur. \ No newline at end of file diff --git a/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/MediumBody.txt b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/MediumBody.txt new file mode 100644 index 0000000000..a58017a7b0 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/MediumBody.txt @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur. \ No newline at end of file diff --git a/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Microsoft.Extensions.Http.Telemetry.PerformanceTests.csproj b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Microsoft.Extensions.Http.Telemetry.PerformanceTests.csproj new file mode 100644 index 0000000000..3a525cf295 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Microsoft.Extensions.Http.Telemetry.PerformanceTests.csproj @@ -0,0 +1,17 @@ + + + Microsoft.Extensions.Http.Telemetry.Logging.Bench + Benchmarks for Microsoft.Extensions.Http.Telemetry.PerformanceTests. + + + + + + + + + + + + + \ No newline at end of file diff --git a/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/NoRemoteCallHandler.cs b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/NoRemoteCallHandler.cs new file mode 100644 index 0000000000..c0c0e02b84 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/NoRemoteCallHandler.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Bench; + +internal sealed class NoRemoteCallHandler : DelegatingHandler +{ + private readonly byte[] _data; + + private NoRemoteCallHandler(byte[] data) + { + _data = data; + } + + [SuppressMessage("Performance", "R9A017:Switch to an asynchronous method for increased performance.", + Justification = "No async overload for `Directory.GetFiles`.")] + [SuppressMessage("Performance Analysis", "CPR120:File.ReadAllXXX should be replaced by using a StreamReader to avoid adding objects to the large object heap (LOH).", + Justification = "We can live with it here")] + public static NoRemoteCallHandler Create(string fileName) + { + var assemblyFileLocation = Path.GetDirectoryName(typeof(NoRemoteCallHandler).Assembly.Location)!; + var uri = new Uri(assemblyFileLocation).LocalPath; + + var responseFilePath = Path.Combine(Directory.GetFiles( + path: uri, + searchPattern: fileName)); + + var fileContent = File.ReadAllBytes(responseFilePath); + + return new NoRemoteCallHandler(fileContent); + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var response = new HttpResponseMessage + { + StatusCode = System.Net.HttpStatusCode.OK, + RequestMessage = request, + Content = new StreamContent(new MemoryStream(_data)) + }; + + response.Content.Headers.ContentType = new("application/json"); + + return Task.FromResult(response); + } +} diff --git a/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/NoRemoteCallNotSeekableHandler.cs b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/NoRemoteCallNotSeekableHandler.cs new file mode 100644 index 0000000000..13008b3b74 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/NoRemoteCallNotSeekableHandler.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Bench; + +internal sealed class NoRemoteCallNotSeekableHandler : DelegatingHandler +{ + private readonly byte[] _data; + + private NoRemoteCallNotSeekableHandler(byte[] data) + { + _data = data; + } + + [SuppressMessage("Performance", "R9A017:Switch to an asynchronous method for increased performance.", + Justification = "No async overload for `Directory.GetFiles`.")] + [SuppressMessage("Performance Analysis", "CPR120:File.ReadAllXXX should be replaced by using a StreamReader to avoid adding objects to the large object heap (LOH).", + Justification = "We can live with it here")] + public static NoRemoteCallNotSeekableHandler Create(string fileName) + { + var assemblyFileLocation = Path.GetDirectoryName(typeof(NoRemoteCallHandler).Assembly.Location)!; + var uri = new Uri(assemblyFileLocation).LocalPath; + + var responseFilePath = Path.Combine(Directory.GetFiles( + path: uri, + searchPattern: fileName)); + + var fileContent = File.ReadAllBytes(responseFilePath); + + return new NoRemoteCallNotSeekableHandler(fileContent); + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var response = new HttpResponseMessage + { + StatusCode = System.Net.HttpStatusCode.OK, + RequestMessage = request, + Content = new StreamContent(new NotSeekableStream(new MemoryStream(_data))) + }; + + response.Content.Headers.ContentType = new("application/json"); + + return Task.FromResult(response); + } +} diff --git a/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/NotSeekableStream.cs b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/NotSeekableStream.cs new file mode 100644 index 0000000000..ab46051ab0 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/NotSeekableStream.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Bench; + +internal sealed class NotSeekableStream : Stream +{ + private readonly MemoryStream _innerStream; + + public NotSeekableStream(MemoryStream memoryStream) + { + _innerStream = memoryStream; + } + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => true; + + public override long Length => _innerStream.Length; + + public override long Position { get => _innerStream.Position; set => throw new NotSupportedException("This is not a seekable stream."); } + + public override void Flush() => _innerStream.Flush(); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _innerStream.ReadAsync(buffer, offset, count, cancellationToken); + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) => _innerStream.ReadAsync(buffer, cancellationToken); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _innerStream.WriteAsync(buffer, offset, count, cancellationToken); + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => _innerStream.WriteAsync(buffer, cancellationToken); + + public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException("This is not a seekable stream."); + + public override void SetLength(long value) => _innerStream.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) => _innerStream.Write(buffer, offset, count); +} diff --git a/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Program.cs b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Program.cs new file mode 100644 index 0000000000..e4971190e1 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/Program.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Order; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.InProcess.Emit; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Bench; + +internal static class Program +{ + private static void Main(string[] args) + { + var dontRequireSlnToRunBenchmarks = ManualConfig + .Create(DefaultConfig.Instance) + .AddJob(Job.MediumRun + .WithRuntime(CoreRuntime.Core50) + .WithGcServer(true) + .WithJit(Jit.RyuJit) + .WithPlatform(Platform.X64) + .WithToolchain(InProcessEmitToolchain.Instance)) + .WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared)) + .AddDiagnoser(MemoryDiagnoser.Default); + + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, dontRequireSlnToRunBenchmarks); + } +} diff --git a/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/RedactionProcessorBench.cs b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/RedactionProcessorBench.cs new file mode 100644 index 0000000000..0f8da6cc61 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/RedactionProcessorBench.cs @@ -0,0 +1,145 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Net.Http; +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Telemetry; +using Microsoft.Extensions.Telemetry.Internal; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing.Bench; + +#pragma warning disable S1075 // URIs should not be hardcoded +#pragma warning disable SA1203 // Constants should appear before fields + +[GcServer(true)] +[MinColumn] +[MaxColumn] +[MemoryDiagnoser] +#pragma warning disable CA1063 // Implement IDisposable Correctly +public class RedactionProcessorBench : IDisposable +#pragma warning restore CA1063 // Implement IDisposable Correctly +{ + private const string ShortPath = "http://test.com/api/routes/routeId123/chats/chatId123/routeId123/chats/chatId123"; + private const string ShortRoute = "/api/routes/{routeId}/chats/{chatId}/{routeId}/chats/{chatId}"; + private HttpClientRedactionProcessor? _shortRedactionProcessor; + private Activity? _shortAcivity; + private HttpRequestMessage? _shortMessage; + + private const string LongPath + = "http://test.com/api/something/something/something/something/something/FJWIEFNJIWEFJI/FJWIEFNJIWEFJI/something/FJWIEFNJIWEFJI/FJWIEFNJIWEFJI/routes/route123/users/user232/chats/chatr213"; + private const string LongRoute = "/api/something/something/something/something/something/{blabla}/{blabla}/something/{blabla}/{blabla}/routes/{routeId}/users/{userId}/chats/{chatId}"; + private HttpClientRedactionProcessor? _longRedactionProcessor; + private Activity? _longAcivity; + private HttpRequestMessage? _longMessage; + + [GlobalSetup] + public void Setup() + { + var builder = new ServiceCollection() + .AddFakeRedaction(options => options.RedactionFormat = "Redacted:{0}") + .AddHttpRouteProcessor() + .AddOutgoingRequestContext() + .BuildServiceProvider(); + + IRedactorProvider redactorProvider = new RP(); + IHttpPathRedactor httpPathRedactor = builder.GetService()!; + var requestMetadataContext = builder.GetService()!; + + var options = new HttpClientTracingOptions(); + + options.RouteParameterDataClasses.Add("routeId", SimpleClassifications.PrivateData); + options.RouteParameterDataClasses.Add("chatId", SimpleClassifications.PrivateData); + options.RouteParameterDataClasses.Add("userId", SimpleClassifications.PrivateData); + + _shortRedactionProcessor = new HttpClientRedactionProcessor( + Microsoft.Extensions.Options.Options.Create(options), + httpPathRedactor, + requestMetadataContext); + + _shortAcivity = new Activity("short_activity"); + _shortMessage = new() + { + RequestUri = new Uri(ShortPath) + }; + _shortMessage.SetRequestMetadata(new RequestMetadata { RequestRoute = ShortRoute }); + _shortAcivity.SetCustomProperty(Constants.CustomPropertyHttpRequestMessage, _shortMessage); + + var longOptions = new HttpClientTracingOptions(); + + longOptions.RouteParameterDataClasses.Add("routeId", SimpleClassifications.PrivateData); + longOptions.RouteParameterDataClasses.Add("chatId", SimpleClassifications.PrivateData); + longOptions.RouteParameterDataClasses.Add("userId", SimpleClassifications.PrivateData); + longOptions.RouteParameterDataClasses.Add("blabla", SimpleClassifications.PrivateData); + + _longRedactionProcessor = new HttpClientRedactionProcessor( + Microsoft.Extensions.Options.Options.Create(longOptions), + httpPathRedactor, + requestMetadataContext); + + _longAcivity = new Activity("long_activity"); + _longMessage = new() + { + RequestUri = new Uri(LongPath) + }; + _longMessage.SetRequestMetadata(new RequestMetadata { RequestRoute = LongRoute }); + _longAcivity.SetCustomProperty(Constants.CustomPropertyHttpRequestMessage, _longMessage); + } + + [Benchmark] + public int Short_Uri_Processing() + { + _shortRedactionProcessor!.Process(_shortAcivity!, _shortMessage!); + + return 0; + } + + [Benchmark] + public int Long_Uri_Processing() + { + _longRedactionProcessor!.Process(_longAcivity!, _longMessage!); + + return 0; + } + +#pragma warning disable CA1063 // Implement IDisposable Correctly +#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize + public void Dispose() +#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize +#pragma warning restore CA1063 // Implement IDisposable Correctly + { + _shortAcivity?.Dispose(); + _shortMessage?.Dispose(); + + _longAcivity?.Dispose(); + _longMessage?.Dispose(); + } + + private sealed class RP : IRedactorProvider + { + private static readonly Redactor _r = new MockRedactor(); + + public Redactor GetRedactor(DataClassification classification) => _r; + } + + internal sealed class MockRedactor : Redactor + { + private const string Template = "[REDACTED]"; + + public override int GetRedactedLength(ReadOnlySpan source) + { + return Template.Length; + } + + public override int Redact(ReadOnlySpan source, Span destination) + { + Template.AsSpan().CopyTo(destination); + return Template.Length; + } + } +} diff --git a/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/SmallBody.txt b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/SmallBody.txt new file mode 100644 index 0000000000..200fe3670d --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Telemetry.PerformanceTests/SmallBody.txt @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dLorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris lobortis ornare tempor. Morbi sit amet arcu mauris. Praesent vel turpis sed mauris scelerisque semper. Donec tincidunt, est vel hendrerit volutpat, libero purus placerat neque, eget auctor augue sapien sed lacus. Maecenas ante elit, pretium facilisis augue ut, porta laoreet libero. Nam vestibulum quam vitae justo tincidunt accumsan. Nulla facilisi. Aenean odio risus, dapibus a blandit et, porta sit amet nisi. Integer sollicitudin nec quam ac cursus. Etiam fringilla dictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur.ictum nisi, ut viverra nisl facilisis eget. Donec tristique turpis vitae volutpat efficitur. \ No newline at end of file diff --git a/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Hedging.cs b/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Hedging.cs new file mode 100644 index 0000000000..cc11abfe1a --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Hedging.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Extensions.Resilience.Polly.Bench.Internals; +using Polly; + +namespace Microsoft.Extensions.Resilience.Polly.Bench; + +public class Hedging +{ + private const int HedgingAttempts = 2; + private readonly TimeSpan _hedgingDelay = TimeSpan.FromMilliseconds(3); + private readonly CancellationToken _cancellationToken = new CancellationTokenSource().Token; + private IAsyncPolicy _hedgingPolicy = null!; + + [GlobalSetup] + public void GlobalSetup() + { + var factory = HedgingUtilities.CreatePolicyFactory(); + + _hedgingPolicy = factory.CreateHedgingPolicy( + "hedging-policy", +#pragma warning disable CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes). + (HedgingTaskProviderArguments _, [NotNullWhen(true)] out Task? result) => +#pragma warning restore CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes). + { + result = HedgingUtilities.SuccessTask; + return true; + }, + new HedgingPolicyOptions + { + HedgingDelay = _hedgingDelay, + MaxHedgedAttempts = HedgingAttempts, + ShouldHandleException = (e) => e is InvalidOperationException, + ShouldHandleResultAsError = r => r == Result.TransientError + }); + } + + [Benchmark(Baseline = true)] + public Task HedgingManual_Success() + { + return HedgingUtilities.ExecuteWithManualHedging(_successFunc, _successFunc, _hedgingDelay, _cancellationToken); + } + + [Benchmark] + public Task Hedging_Success() + { + return _hedgingPolicy.ExecuteAsync(_successFunc, _cancellationToken); + } + + [Benchmark] + public Task HedgingManual_SuccessOnSecondaryTry() + { + return HedgingUtilities.ExecuteWithManualHedging(_errorFunc, _successFunc, _hedgingDelay, _cancellationToken); + } + + [Benchmark] + public Task HedgingManual_RealAwaiting_SuccessOnSecondaryTry() + { + return HedgingUtilities.ExecuteWithManualHedging(_errorFunc, _longSuccessFunc, _hedgingDelay, _cancellationToken); + } + + [Benchmark] + public Task Hedging_SuccessOnSecondaryTry() + { + return _hedgingPolicy.ExecuteAsync(_errorFunc, _cancellationToken); + } + + [Benchmark] + public Task HedgingManual_FirstTryThrows_SuccessOnSecondaryTry() + { + return HedgingUtilities.ExecuteWithManualHedging(_exceptionFunc, _successFunc, _hedgingDelay, _cancellationToken); + } + + [Benchmark] + public Task Hedging_FirstTryThrows_SuccessOnSecondaryTry() + { + return _hedgingPolicy.ExecuteAsync(_exceptionFunc, _cancellationToken); + } + + private static readonly Func> _successFunc = SuccessFunc; + private static readonly Func> _longSuccessFunc = LongSuccessFunc; + private static readonly Func> _errorFunc = ErrorFunc; + private static readonly Func> _exceptionFunc = ExceptionFunc; + + private static Task SuccessFunc(CancellationToken cancellationToken) => HedgingUtilities.SuccessTask; + + private static async Task LongSuccessFunc(CancellationToken cancellationToken) + { + await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken).ConfigureAwait(false); + + return default; + } + + private static Task ErrorFunc(CancellationToken cancellationToken) => HedgingUtilities.TransientErrorTask; + + private static Task ExceptionFunc(CancellationToken cancellationToken) => HedgingUtilities.TransientExceptionTask; +} diff --git a/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Internals/HedgingUtilities.cs b/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Internals/HedgingUtilities.cs new file mode 100644 index 0000000000..87ebd2d358 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Internals/HedgingUtilities.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Telemetry.Metering; + +namespace Microsoft.Extensions.Resilience.Polly.Bench.Internals; + +internal enum Result +{ + Success, + TransientError +} + +internal static class HedgingUtilities +{ + public static readonly Task SuccessTask = Task.FromResult(Result.Success); + + public static readonly Task TransientErrorTask = Task.FromResult(Result.TransientError); + + public static readonly Task TransientExceptionTask = Task.FromException(new InvalidOperationException("Failed hedged attempt.")); + + public static Resilience.Internal.IPolicyFactory CreatePolicyFactory() + { + var services = new ServiceCollection(); + PolicyFactoryServiceCollectionExtensions.AddPolicyFactory(services); + services.RegisterMetering(); + services.AddLogging(); + return services.BuildServiceProvider().GetRequiredService(); + } + + public static async Task ExecuteWithManualHedging( + Func> executeFunc, + Func> retryFunc, + TimeSpan hedginDelay, + CancellationToken cancellationToken) + { + using var delayCancellation = new CancellationTokenSource(); + var delay = Task.Delay(hedginDelay, delayCancellation.Token); + var executeTask = executeFunc(cancellationToken); + var finishedTask = await Task.WhenAny(delay, executeTask).ConfigureAwait(false); + + if (finishedTask == delay) + { + return await retryFunc(cancellationToken).ConfigureAwait(false); + } + + await delayCancellation.CancelAsync().ConfigureAwait(false); + + Result result = default; + + try + { + result = await executeTask.ConfigureAwait(false); + } + catch (InvalidOperationException) + { + result = Result.TransientError; + } + + if (result == Result.TransientError) + { + return await retryFunc(cancellationToken).ConfigureAwait(false); + } + + return result; + } +} diff --git a/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Microsoft.Extensions.Resilience.PerformanceTests.csproj b/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Microsoft.Extensions.Resilience.PerformanceTests.csproj new file mode 100644 index 0000000000..a6f614b1cb --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Microsoft.Extensions.Resilience.PerformanceTests.csproj @@ -0,0 +1,11 @@ + + + Microsoft.Extensions.Resilience + Benchmarks for Microsoft.Extensions.Resilience. + + + + + + + diff --git a/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/PipelineProvider.cs b/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/PipelineProvider.cs new file mode 100644 index 0000000000..7c3618bb58 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/PipelineProvider.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Telemetry.Metering; + +namespace Microsoft.Extensions.Resilience.Bench; + +public class PipelineProvider +{ + private IResiliencePipelineProvider? _pipelineProvider; + + [GlobalSetup] + public void GlobalSetup() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.RegisterMetering(); + services.AddResiliencePipeline("dummy").AddBulkheadPolicy("dummy"); + + _pipelineProvider = services.BuildServiceProvider().GetRequiredService(); + } + + [Benchmark(Baseline = true)] + public void GetPipeline() => _pipelineProvider!.GetPipeline("dummy"); + + [Benchmark] + public void GetPipelineByKey() => _pipelineProvider!.GetPipeline("dummy", "dummy-key"); +} diff --git a/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Pipelines.cs b/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Pipelines.cs new file mode 100644 index 0000000000..c8a4eccf0c --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Pipelines.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Telemetry.Metering; +using Polly; +using Polly.NoOp; + +namespace Microsoft.Extensions.Resilience.Bench; + +public class Pipelines +{ + private static readonly Task _completed = Task.FromResult("dummy"); + private IAsyncPolicy? _bulkheadPolicy; + private IAsyncPolicy? _timeoutPolicy; + private IAsyncPolicy? _circuitBreaker; + private IAsyncPolicy? _retryPolicy; + private IAsyncPolicy? _fallbackPolicy; + private IAsyncPolicy? _hedgingPolicy; + private AsyncNoOpPolicy? _noOp; + private CancellationToken _cancellation; + private Context? _context; + + [GlobalSetup] + public void GlobalSetup() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.RegisterMetering(); + services.AddResiliencePipeline(nameof(SupportedPolicies.BulkheadPolicy)).AddBulkheadPolicy("dummy"); + services.AddResiliencePipeline(nameof(SupportedPolicies.TimeoutPolicy)).AddTimeoutPolicy("dummy"); + services.AddResiliencePipeline(nameof(SupportedPolicies.CircuitBreaker)).AddCircuitBreakerPolicy("dummy"); + services.AddResiliencePipeline(nameof(SupportedPolicies.RetryPolicy)).AddRetryPolicy("dummy"); + services.AddResiliencePipeline(nameof(SupportedPolicies.FallbackPolicy)).AddFallbackPolicy("dummy", task => _completed); + services.AddResiliencePipeline(nameof(SupportedPolicies.HedgingPolicy)).AddHedgingPolicy("dummy", (HedgingTaskProviderArguments _, out Task? result) => + { + result = null; + return false; + }); + + var pipelineProvider = services.BuildServiceProvider().GetRequiredService(); + _context = new Context(); + _bulkheadPolicy = pipelineProvider.GetPipeline(nameof(SupportedPolicies.BulkheadPolicy)); + _timeoutPolicy = pipelineProvider.GetPipeline(nameof(SupportedPolicies.TimeoutPolicy)); + _circuitBreaker = pipelineProvider.GetPipeline(nameof(SupportedPolicies.CircuitBreaker)); + _retryPolicy = pipelineProvider.GetPipeline(nameof(SupportedPolicies.RetryPolicy)); + _fallbackPolicy = pipelineProvider.GetPipeline(nameof(SupportedPolicies.FallbackPolicy)); + _hedgingPolicy = pipelineProvider.GetPipeline(nameof(SupportedPolicies.HedgingPolicy)); + _noOp = Policy.NoOpAsync(); + _cancellation = new CancellationTokenSource().Token; + } + + [Benchmark(Baseline = true)] + public Task NoOp() => _noOp!.ExecuteAsync(static (_, _) => _completed, _context, _cancellation); + + [Benchmark] + public Task Bulkhead() => _bulkheadPolicy!.ExecuteAsync(static (_, _) => _completed, _context, _cancellation); + + [Benchmark] + public Task Timeout() => _timeoutPolicy!.ExecuteAsync(static (_, _) => _completed, _context, _cancellation); + + [Benchmark] + public Task CircuitBreaker() => _circuitBreaker!.ExecuteAsync(static (_, _) => _completed, _context, _cancellation); + + [Benchmark] + public Task Retry() => _retryPolicy!.ExecuteAsync(static (_, _) => _completed, _context, _cancellation); + + [Benchmark] + public Task Fallback() => _fallbackPolicy!.ExecuteAsync(static (_, _) => _completed, _context, _cancellation); + + [Benchmark] + public Task Hedging() => _hedgingPolicy!.ExecuteAsync(static (_, _) => _completed, _context, _cancellation); +} diff --git a/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Program.cs b/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Program.cs new file mode 100644 index 0000000000..47de22c098 --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Program.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.InProcess.Emit; + +namespace Microsoft.Extensions.Resilience.Bench; + +internal static class Program +{ + private static void Main(string[] args) + { + var dontRequireSlnToRunBenchmarks = ManualConfig + .Create(DefaultConfig.Instance) + .AddJob(Job.MediumRun.WithToolchain(InProcessEmitToolchain.Instance)) + .AddDiagnoser(MemoryDiagnoser.Default); + + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, dontRequireSlnToRunBenchmarks); + } +} diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000000..4412f18708 --- /dev/null +++ b/build.cmd @@ -0,0 +1,32 @@ +@ECHO OFF +SETLOCAL EnableDelayedExpansion + +SET _args=%* +IF "%~1"=="-?" SET _args=-help +IF "%~1"=="/?" SET _args=-help + +IF ["%_args%"] == [""] ( + :: Perform restore and build, IF no args are supplied. + SET _args=-restore -build +) + +FOR %%x IN (%*) DO ( + SET _arg=%%x + + IF /I "%%x%"=="-coverage" GOTO RUN_CODE_COVERAGE +) + +powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0eng\build.ps1""" %_args%" +EXIT /b %ERRORLEVEL% + + +:RUN_CODE_COVERAGE + SET DOTNET_ROOT=%~dp0.dotnet + :: This tells .NET Core not to go looking for .NET Core in other places + SET DOTNET_MULTILEVEL_LOOKUP=0 + + dotnet dotnet-coverage collect --settings ./eng/CodeCoverage.config --output ./artifacts/TestResults/ "build.cmd -test -bl" + dotnet reportgenerator -reports:./artifacts/TestResults/*.cobertura.xml -targetdir:./artifacts/TestResults/CoverageResultsHtml -reporttypes:HtmlInline_AzurePipelines + start ./artifacts/TestResults/CoverageResultsHtml/index.html + powershell -ExecutionPolicy ByPass -NoProfile -command "./scripts/ValidateProjectCoverage.ps1 -CoberturaReportXml ./artifacts/TestResults/.cobertura.xml" + EXIT /b %ErrorLevel% diff --git a/build.sh b/build.sh new file mode 100755 index 0000000000..15cbc641df --- /dev/null +++ b/build.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# Stop script if unbound variable found (use ${var:-} if intentional) +set -u + +# Stop script if command returns non-zero exit code. +# Prevents hidden errors caused by missing error code propagation. +set -e + +set -euo pipefail + +if [[ $# < 1 ]] +then + # Perform restore and build, if no args are supplied. + set -- --restore --build; +fi + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +"$DIR/eng/build.sh" "$@" diff --git a/docs/building.md b/docs/building.md new file mode 100644 index 0000000000..dcc6e01c5e --- /dev/null +++ b/docs/building.md @@ -0,0 +1,159 @@ +# Build instructions + +- [Introduction](#introduction) +- [Ubuntu](#ubuntu) + * [Building from command line](#building-from-command-line) + * [Building from Visual Studio Code](#building-from-visual-studio-code) +- [Windows](#windows) + * [Building from command line](#building-from-command-line-1) + * [Building from Visual Studio](#building-from-visual-studio) + * [Building from Visual Studio Code](#building-from-visual-studio-code-1) +- [Build outputs](#build-outputs) +- [Troubleshooting build errors](#troubleshooting-build-errors) + +## Introduction + +Like many other .NET repositories, this repository uses the [.NET Arcade SDK][arcade-sdk]. With that, most repository interactions are facilitated with `.\eng\common\build.cmd` or `.\eng\common\build.sh`. We added helper scripts to simplify some of the most common interactions with those, such as `restore.cmd`/`restore.sh` and `build.cmd`/`build.sh`. + +Also, like all the Arcadified repositories, this repository does **not** build using your machine-wide installed version of the .NET SDK. Instead it builds using the repo-local .NET SDK specified in the [`global.json`](..\global.json) file located in the repository root.
+:warning: This also means that you **cannot** double click on any of the projects or a solution file in this repository and open those in Visual Studio. Generally, it won't work as Visual Studio won't be able to resolve the required .NET SDK. You will need to use our helper scripts provided for your convenience. + +However, unlike pretty much all .NET repositories, this repository does not contain a solution file due to the number of projects in the solutions and the target framework monikers (TFMs) those projects use. It's very unlikely that you, as a developer, will be dealing with all projects in this repository at the same time. Instead, we expect that you will work on a subset of projects, using a "filtered" solution generated with the [slngen tool][slngen-tool]. Please read below for detailed instructions. + +## Ubuntu + +### Building from command line + +#### TL;DR + +Building the solution is as easy as running: + +```bash +$ ./build.sh +``` + +#### Various scripts to build and test the solution + +The repo provides the following helper scripts for your convenience: + +* `restore.sh` - this script will install the required .NET SDK, .NET tools and the toolset.
+This script is equivalent to running `./build.sh --restore`. + +* `build.sh` - this is a "one-stop shop" script that accepts a whole plethora of commands.
+Here are few commands that you will likely use the most: + - `build.sh`: without any parameters this is equivalent to running `./build.sh --restore --build`. + - `build.sh --restore`: to install the required .NET SDK, .NET tools and the toolset. This is equivalent to running `./restore.sh`. + - `build.sh --build`: to build the solution1. + - `build.sh --test`: to run all unit tests in the solution1. + - `build.sh --vs `: to generate a "filtered" solution and save it as `SDK.sln`. It also performs the "restore" operation. For example: `./build.sh --vs Http,Fakes`.
+ If for some reason you wish to generate a solution with all projects you can pass `*` for the keyword, e.g.: `./build.sh -vs '*'` (Note: you have to escape the asterisk or use `set -f` to turn off expansion).
+ > Under the hood, this invokes `scripts/Slngen.ps1` script, which in turn executes [slngen tool][slngen-tool]. If you want to customize how the "filtered" solution is generated, you will need to invoke `scripts/Slngen.ps1` script directly.
+ Run `./scripts/Slngen.ps1 -help` for more details. + + To find out more about the script and its parameters run: `./build.sh --help`. + +* `start-code.sh` - this script sets up necessary environmental variables and opens the repository in VS Code, so that you can interact with the repository in "dotnet"-way, that is run `dotnet` commands from the VS Code's terminal. + +### Building from Visual Studio Code + +To open the solution in VS Code run the following command: + +```bash +$ ./start-code.sh +``` + +This sets up necessary environmental variables and opens the repository in VS Code. It is advisable that you continue to interact with the solution (i.e., restore, build or test) via the [helper scripts described above](#building-from-command-line), however, it should be generally possible to interact with the repository in "dotnet"-way, that is run `dotnet` commands from the VS Code's terminal. + + +## Windows + +### Building from command line + +#### TL;DR + +Building the solution is as easy as running: + +```bash +> build.cmd +``` + +#### Various scripts to build and test the solution + +* `restore.cmd` - this script will install the required .NET SDK, .NET tools and the toolset.
+This script is equivalent to running `.\build.cmd -restore`. +* `build.cmd` - this is a "one-stop shop" script that accepts a whole plethora of commands.
+Here are few commands that you will likely use the most: + - `build.cmd`: without any parameters this is equivalent to running `.\build.cmd -restore -build`. + - `build.cmd -restore`: to install the required .NET SDK, .NET tools and the toolset. This is equivalent to running `.\restore.cmd`. + - `build.cmd -build`: to build the solution1. + - `build.cmd -test`: to run all unit tests in the solution1. + - `build.cmd -vs `: to generate a "filtered" solution, save it as `SDK.sln` and then open in Visual Studio. For example: `.\build.cmd -vs Http,Fakes`.
+ If for some reason you wish to generate a solution with all projects you can pass `*` for the keyword, e.g.: `.\build.cmd -vs *`.
+ > Under the hood, this invokes `scripts\Slngen.ps1` script, which in turn executes [slngen tool][slngen-tool]. If you want to customize how the "filtered" solution is generated, you will need to invoke `scripts\Slngen.ps1` script directly.
+ Run `.\scripts\Slngen.ps1 -help` for more details. + +To find out more about the script and its parameters run: `.\build.cmd -help`. + +### Building from Visual Studio + +#### TL;DR + +Generating a new solution and opening it in Visual Studio is as easy as running: + +```powershell +> build.cmd -vs +``` + +If you already have a solution you'd like to open in Visual Studio then run the following command: + +```powershell +> start-vs.cmd +``` + +#### Various scripts to build and test the solution + +1. If you don't have a solution file, or you wish to generate a new one: run `.\restore.cmd -vs `. +1. If you have a solution file already (e.g., SDK.sln), then you can simply run `.\start-vs.cmd`. +1. (Advanced) If you want to customize how the "filtered" solution is generated, you will need to invoke `scripts\Slngen.ps1` script directly followed by `start-vs.cmd`.
+ Run `.\scripts\Slngen.ps1 -help` for more details. + +### Building from Visual Studio Code + +To open the solution in VS Code run the following command: + +```powershell +> start-code.cmd +``` + +This sets up necessary environmental variables and opens the repository in VS Code. It is advisable that you continue to interact with the solution (i.e., restore, build or test) via the [helper scripts described above](#building-from-command-line-1), however, it should be generally possible to interact with the repository in "dotnet"-way, that is run `dotnet` commands from the VS Code's terminal. + + +## Build outputs + +* All build outputs are generated under the `artifacts` folder. +* Binaries are under `artifacts\bin`. +* Logs are found under `artifacts\log`. +* Packages are found under `artifacts\packages`. + + +## Troubleshooting build errors + +* Most build errors are compile errors and can be dealt with accordingly. +* Other error may be from MSBuild tasks. You need to examine the build logs to investigate. + * The logs are generated at `.\artifacts\log\Debug\Build.binlog` + * The file format is an MSBuild Binary Log. Install the [MSBuild Structured Log Viewer][msbuild-log-viewer] to view them. +* Windows Forms uses Visual Studio MSBuild but for certain features we require the latest MSBuild from .NET Core/.NET SDK. If you are on an official version of [Visual Studio][VS-download] (i.e. not a Preview version), then you may need to enable previews for .NET Core/.NET SDKs in VS. + * you can do this in VS under Tools->Options->Environment->Preview Features->Use previews of the .Net Core SDK (Requires restart) + + +--- + +1. **"Solution"** means the collections of projects specified in `eng/build.proj` or an actual "sln" file at the root of the repository that represents the generated "filtered" solution (e.g., `SDK.sln`). + +[comment]: <> (URI Links) + +[arcade-sdk]: https://github.com/dotnet/arcade/blob/main/Documentation/Overview.md +[msbuild-log-viewer]: https://msbuildlog.com/ +[slngen-tool]: https://github.com/microsoft/slngen +[VS-download]: https://visualstudio.microsoft.com/downloads/ + diff --git a/eng/AfterSolutionBuild.targets b/eng/AfterSolutionBuild.targets new file mode 100644 index 0000000000..ab179d89d5 --- /dev/null +++ b/eng/AfterSolutionBuild.targets @@ -0,0 +1,5 @@ + + + + + diff --git a/eng/Build.props b/eng/Build.props new file mode 100644 index 0000000000..1b31c13b70 --- /dev/null +++ b/eng/Build.props @@ -0,0 +1,15 @@ + + + + + true + + + false + + + + + + + \ No newline at end of file diff --git a/eng/CodeCoverage.config b/eng/CodeCoverage.config new file mode 100644 index 0000000000..259c779351 --- /dev/null +++ b/eng/CodeCoverage.config @@ -0,0 +1,37 @@ + + + cobertura + + + + + .*Microsoft.*\.dll$ + .*System.*\.dll$ + + + .*tests\.dll + .*xunit.* + .*moq.* + + + + + + + + ^System\.Diagnostics\.DebuggerHiddenAttribute$ + ^System\.Diagnostics\.DebuggerNonUserCodeAttribute$ + ^System\.CodeDom\.Compiler\.GeneratedCodeAttribute$ + ^System\.Diagnostics\.CodeAnalysis\.ExcludeFromCodeCoverageAttribute$ + + + + + false + + false + false + false + + + diff --git a/eng/Common.runsettings b/eng/Common.runsettings new file mode 100644 index 0000000000..6921b68754 --- /dev/null +++ b/eng/Common.runsettings @@ -0,0 +1,20 @@ + + + + x64 + true + + + + + + + cobertura + [*.Tests]* + [Microsoft.*]*,[System*]* + ExcludeFromCodeCoverageAttribute,DebuggerNonUserCodeAttribute,DebuggerHiddenAttribute,GeneratedCodeAttribute + + + + + \ No newline at end of file diff --git a/eng/CredScanSuppressions.json b/eng/CredScanSuppressions.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/eng/CredScanSuppressions.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/eng/MSBuild/Analyzers.props b/eng/MSBuild/Analyzers.props new file mode 100644 index 0000000000..c06818c212 --- /dev/null +++ b/eng/MSBuild/Analyzers.props @@ -0,0 +1,33 @@ + + + + false + false + false + false + + + + true + true + true + true + latest + true + + + + + + + + + + + + + + + + + diff --git a/eng/MSBuild/Analyzers.targets b/eng/MSBuild/Analyzers.targets new file mode 100644 index 0000000000..40bbe34acb --- /dev/null +++ b/eng/MSBuild/Analyzers.targets @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + <_TargetPathsToSymbols Include="@(_AnalyzerFile)" TargetPath="/%(_AnalyzerFile.PackagePath)" Condition="%(_AnalyzerFile.IsSymbol)" /> + + + + + <_MultiTargetRoslynComponentTargetsTemplate>$(MSBuildThisFileDirectory)MultiTargetRoslynComponent.targets.template + $(IntermediateOutputPath)MultiTargetRoslynComponent.targets + true + + + + + + + + + + + + <_MultiTargetRoslynComponentTargetPrefix>$(PackageId.Replace('.', '_')) + Disable$(PackageId.Replace('.', ''))SourceGenerator + + + + + + + $(BuildProjectReferences) + false + + + + + + + + + + <_analyzerPath>analyzers/dotnet + <_analyzerPath Condition="'$(AnalyzerRoslynVersion)' != ''">$(_analyzerPath)/roslyn$(AnalyzerRoslynVersion) + <_analyzerPath Condition="'$(AnalyzerLanguage)' != ''">$(_analyzerPath)/$(AnalyzerLanguage) + + + + <_AnalyzerPackFile IsSymbol="false" + Include="%(_BuildOutputInPackage.FinalOutputPath)" + TargetPath="%(_BuildOutputInPackage.TargetPath)" + TargetFramework="%(_BuildOutputInPackage.TargetFramework)" /> + + + <_AnalyzerPackFile IsSymbol="true" + Include="%(_TargetPathsToSymbols.FinalOutputPath)" + TargetPath="%(_TargetPathsToSymbols.TargetPath)" + TargetFramework="%(_TargetPathsToSymbols.TargetFramework)" /> + + <_AnalyzerPackFile PackagePath="$(_analyzerPath)/%(TargetPath)" /> + + + + diff --git a/eng/MSBuild/Generators.props b/eng/MSBuild/Generators.props new file mode 100644 index 0000000000..580c36c559 --- /dev/null +++ b/eng/MSBuild/Generators.props @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/eng/MSBuild/Generators.targets b/eng/MSBuild/Generators.targets new file mode 100644 index 0000000000..8b169cd8f8 --- /dev/null +++ b/eng/MSBuild/Generators.targets @@ -0,0 +1,10 @@ + + + false + + + + + + + \ No newline at end of file diff --git a/eng/MSBuild/LegacySupport.props b/eng/MSBuild/LegacySupport.props new file mode 100644 index 0000000000..7bd83e61e2 --- /dev/null +++ b/eng/MSBuild/LegacySupport.props @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eng/MSBuild/LegacySupport.targets b/eng/MSBuild/LegacySupport.targets new file mode 100644 index 0000000000..b585332dc5 --- /dev/null +++ b/eng/MSBuild/LegacySupport.targets @@ -0,0 +1,6 @@ + + + true + true + + diff --git a/eng/MSBuild/MultiTargetRoslynComponent.targets.template b/eng/MSBuild/MultiTargetRoslynComponent.targets.template new file mode 100644 index 0000000000..e7302626d5 --- /dev/null +++ b/eng/MSBuild/MultiTargetRoslynComponent.targets.template @@ -0,0 +1,69 @@ + + + + + <_{TargetPrefix}Analyzer Include="@(Analyzer)" Condition="'%(Analyzer.NuGetPackageId)' == '{NuGetPackageId}'" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/eng/MSBuild/Packaging.props b/eng/MSBuild/Packaging.props new file mode 100644 index 0000000000..cde0a1f973 --- /dev/null +++ b/eng/MSBuild/Packaging.props @@ -0,0 +1,31 @@ + + + + $(SignAssembly) + + + true + + + + + true + true + $(MSBuildThisFileDirectory)\MSSharedLibSN2048.snk + 002400000c80000014010000060200000024000052534131000800000100010085aad0bef0688d1b994a0d78e1fd29fc24ac34ed3d3ac3fb9b3d0c48386ba834aa880035060a8848b2d8adf58e670ed20914be3681a891c9c8c01eef2ab22872547c39be00af0e6c72485d7cfd1a51df8947d36ceba9989106b58abe79e6a3e71a01ed6bdc867012883e0b1a4d35b1b5eeed6df21e401bb0c22f2246ccb69979dc9e61eef262832ed0f2064853725a75485fa8a3efb7e027319c86dec03dc3b1bca2b5081bab52a627b9917450dfad534799e1c7af58683bdfa135f1518ff1ea60e90d7b993a6c87fd3dd93408e35d1296f9a7f9a97c5db56c0f3cc25ad11e9777f94d138b3cea53b9a8331c2e6dcb8d2ea94e18bf1163ff112a22dbd92d429a + + + + + true + snupkg + + + + + $(CopyrightNetFoundation) + https://dot.net/ + MIT + + + diff --git a/eng/MSBuild/Packaging.targets b/eng/MSBuild/Packaging.targets new file mode 100644 index 0000000000..f5c7102b83 --- /dev/null +++ b/eng/MSBuild/Packaging.targets @@ -0,0 +1,90 @@ + + + + + IncludeAnalyzersInPackage;$(BeforePack) + $(BuildProjectReferences) + false + + + + + false + $(BeforePack);_VerifyMinimumSupportedTfmForPackagingIsUsed;_AddNETStandardCompatErrorFileForPackaging + + + + + + + + + + + + + + + + + <_NETStandardCompatErrorFilePath>$(BaseIntermediateOutputPath)netstandardcompaterror_%(NETStandardCompatError.Identity).targets + <_NETStandardCompatErrorFileTarget>NETStandardCompatError_$(PackageId.Replace('.', '_'))_$([System.String]::new('%(NETStandardCompatError.Supported)').Replace('.', '_')) + <_NETStandardCompatErrorFileContent> + + + + +]]> + + <_NETStandardCompatErrorPlaceholderFilePackagePath>buildTransitive$([System.IO.Path]::DirectorySeparatorChar)%(NETStandardCompatError.Supported) + $(RepositoryEngineeringDir)_._ + + + + + + <_PackageBuildFile Include="@(None->Metadata('PackagePath')); + @(Content->Metadata('PackagePath'))" /> + <_PackageBuildFile PackagePathWithoutFilename="$([System.IO.Path]::GetDirectoryName('%(Identity)'))" /> + + + + + + + + + + + + + + + + diff --git a/eng/MSBuild/ProjectStaging.targets b/eng/MSBuild/ProjectStaging.targets new file mode 100644 index 0000000000..f8c126d5f2 --- /dev/null +++ b/eng/MSBuild/ProjectStaging.targets @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + DEVELOPMENT BUILD - DO NOT USE IN PRODUCTION - $(Description) + OBSOLETE PACKAGE - $(Description) + + + diff --git a/eng/MSBuild/Shared.props b/eng/MSBuild/Shared.props new file mode 100644 index 0000000000..faa7aa2bbc --- /dev/null +++ b/eng/MSBuild/Shared.props @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eng/MSBuild/Shared.targets b/eng/MSBuild/Shared.targets new file mode 100644 index 0000000000..86f66c3926 --- /dev/null +++ b/eng/MSBuild/Shared.targets @@ -0,0 +1,31 @@ + + + + true + true + + + + true + true + true + true + + + + true + true + true + true + + + + true + + + + true + true + true + + diff --git a/eng/MSSharedLibSN2048.snk b/eng/MSSharedLibSN2048.snk new file mode 100644 index 0000000000000000000000000000000000000000..bd766f84a23ac0323c0589df14827e0644f6a6ae GIT binary patch literal 288 zcmV+*0pI=rBme*mfB*m#0RR970ssI2Bme+XQ$aBR2mk;90097ns?fghXpI}0N)347 z{VDt;tTgRCI>Y;$Jq$=VYp67;hyXPP3W!Lu*sb-BXAaT{6uvfrsFBIYz#i`^vM6#? zd^x@VuMTW-NL_sW8d2YgN7HQUshE)lwTixZ=A-8t0qtwthHw&yJ_{O6HLp+enc>H;SZF)np*8VQkMZhY=?#0CI&6piJ=rt}<6RP#`KS4*d|kC{4?MzJ m(H@s~`ArjxJnB=qs52ZcZOe@=sZJQb5o7-mDk9t2Ekc^_F@YHX literal 0 HcmV?d00001 diff --git a/eng/PSScriptAnalyzerSettings.psd1 b/eng/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 0000000000..a3d421cddb --- /dev/null +++ b/eng/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,15 @@ +# PSScriptAnalyzerSettings.psd1 +# Settings for PSScriptAnalyzer invocation. +@{ + Severity = @('Error', 'Warning') + IncludeRules = 'PSAvoid*' + + # Do not analyze the following rules. Use ExcludeRules when you have + # commented out the IncludeRules settings above and want to include all + # the default rules except for those you exclude below. + # Note: if a rule is in both IncludeRules and ExcludeRules, the rule + # will be excluded. + ExcludeRules = @( + 'PSUseShouldProcessForStateChangingFunctions', + 'PSUseSingularNouns') +} \ No newline at end of file diff --git a/eng/Packages/BuildOnly.props b/eng/Packages/BuildOnly.props new file mode 100644 index 0000000000..e120261807 --- /dev/null +++ b/eng/Packages/BuildOnly.props @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/eng/Packages/General-latest.props b/eng/Packages/General-latest.props new file mode 100644 index 0000000000..a016a7037b --- /dev/null +++ b/eng/Packages/General-latest.props @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eng/Packages/General-net462.props b/eng/Packages/General-net462.props new file mode 100644 index 0000000000..ec2d3268f3 --- /dev/null +++ b/eng/Packages/General-net462.props @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eng/Packages/General-net6.0.props b/eng/Packages/General-net6.0.props new file mode 100644 index 0000000000..0ecbeb4a1b --- /dev/null +++ b/eng/Packages/General-net6.0.props @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eng/Packages/General-netcoreapp3.1.props b/eng/Packages/General-netcoreapp3.1.props new file mode 100644 index 0000000000..663ed29a8b --- /dev/null +++ b/eng/Packages/General-netcoreapp3.1.props @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eng/Packages/General.props b/eng/Packages/General.props new file mode 100644 index 0000000000..4951d1cb85 --- /dev/null +++ b/eng/Packages/General.props @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eng/Packages/Packages.sidepush.config b/eng/Packages/Packages.sidepush.config new file mode 100644 index 0000000000..197350d2c9 --- /dev/null +++ b/eng/Packages/Packages.sidepush.config @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eng/Packages/Packages.sign3rdparty.config b/eng/Packages/Packages.sign3rdparty.config new file mode 100644 index 0000000000..b3fc49abb7 --- /dev/null +++ b/eng/Packages/Packages.sign3rdparty.config @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/eng/Packages/TestOnly-latest.props b/eng/Packages/TestOnly-latest.props new file mode 100644 index 0000000000..8a0ad70962 --- /dev/null +++ b/eng/Packages/TestOnly-latest.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/eng/Packages/TestOnly-net462.props b/eng/Packages/TestOnly-net462.props new file mode 100644 index 0000000000..c0a648284a --- /dev/null +++ b/eng/Packages/TestOnly-net462.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/eng/Packages/TestOnly-net6.0.props b/eng/Packages/TestOnly-net6.0.props new file mode 100644 index 0000000000..17fb73cb11 --- /dev/null +++ b/eng/Packages/TestOnly-net6.0.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/eng/Packages/TestOnly-netcoreapp3.1.props b/eng/Packages/TestOnly-netcoreapp3.1.props new file mode 100644 index 0000000000..bcb65139b8 --- /dev/null +++ b/eng/Packages/TestOnly-netcoreapp3.1.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/eng/Packages/TestOnly.props b/eng/Packages/TestOnly.props new file mode 100644 index 0000000000..726a9b69ed --- /dev/null +++ b/eng/Packages/TestOnly.props @@ -0,0 +1,46 @@ + + + + + false + + + $([MSBuild]::NormalizeDirectory('$(ArtifactsLogDir)', 'TestLogs')) + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eng/Publishing.props b/eng/Publishing.props new file mode 100644 index 0000000000..85915f0380 --- /dev/null +++ b/eng/Publishing.props @@ -0,0 +1,6 @@ + + + + 3 + + \ No newline at end of file diff --git a/eng/Stylecop.json b/eng/Stylecop.json new file mode 100644 index 0000000000..9cad70f365 --- /dev/null +++ b/eng/Stylecop.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "indentation": { + }, + "layoutRules": { + }, + "orderingRules": { + "usingDirectivesPlacement": "outsideNamespace", + "systemUsingDirectivesFirst": true + }, + "documentationRules": { + "xmlHeader": false, + "documentExposedElements": true, + "documentInternalElements": false + }, + "spacingRules": { + }, + "maintainabilityRules": { + }, + "namingRules": { + "tupleElementNameCasing": "camelCase" + }, + "readabilityRules": { + } + } +} \ No newline at end of file diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml new file mode 100644 index 0000000000..b52b0107a5 --- /dev/null +++ b/eng/Version.Details.xml @@ -0,0 +1,178 @@ + + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 367a360a9fedc8c3c59ac7f6b4b5d92790ace993 + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/runtime + 9e49620aeee78c0d37dd741f88aff89850a6f5fb + + + https://github.com/dotnet/aspnetcore + c760054e1fbc8749bc0c6f2116b22532424ff60f + + + https://github.com/dotnet/aspnetcore + c760054e1fbc8749bc0c6f2116b22532424ff60f + + + https://github.com/dotnet/aspnetcore + c760054e1fbc8749bc0c6f2116b22532424ff60f + + + https://github.com/dotnet/aspnetcore + c760054e1fbc8749bc0c6f2116b22532424ff60f + + + https://github.com/dotnet/aspnetcore + c760054e1fbc8749bc0c6f2116b22532424ff60f + + + https://github.com/dotnet/aspnetcore + c760054e1fbc8749bc0c6f2116b22532424ff60f + + + https://github.com/dotnet/aspnetcore + c760054e1fbc8749bc0c6f2116b22532424ff60f + + + https://github.com/dotnet/aspnetcore + c760054e1fbc8749bc0c6f2116b22532424ff60f + + + + + https://github.com/dotnet/arcade + 1aff4eb33aa7cbf26ccd9fc43c17cb609a14dad4 + + + https://github.com/dotnet/arcade + 1aff4eb33aa7cbf26ccd9fc43c17cb609a14dad4 + + + diff --git a/eng/Versions.props b/eng/Versions.props new file mode 100644 index 0000000000..d73ff508a8 --- /dev/null +++ b/eng/Versions.props @@ -0,0 +1,82 @@ + + + 8 + 0 + 0 + alpha + 1 + $(MajorVersion).$(MinorVersion).$(PatchVersion) + true + $(MajorVersion).$(MinorVersion).0.0 + + false + + + + + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.3.23164.10 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + 8.0.0-preview.5.23272.1 + + 8.0.0-preview.5.23271.1 + 8.0.0-preview.5.23271.1 + 8.0.0-preview.5.23271.1 + 8.0.0-preview.5.23271.1 + 8.0.0-preview.5.23271.1 + 8.0.0-preview.5.23271.1 + 8.0.0-preview.5.23271.1 + 8.0.0-preview.5.23271.1 + + + + + diff --git a/eng/_._ b/eng/_._ new file mode 100644 index 0000000000..e69de29bb2 diff --git a/eng/build.proj b/eng/build.proj new file mode 100644 index 0000000000..e190928306 --- /dev/null +++ b/eng/build.proj @@ -0,0 +1,12 @@ + + + + <_ProjectsToBuild Include="$(MSBuildThisFileDirectory)..\src\**\*.csproj" Exclude="@(_ProjectsToExclude)" /> + + <_ProjectsToBuild Include="$(MSBuildThisFileDirectory)..\test\**\*.csproj" /> + <_ProjectsToBuild Include="$(MSBuildThisFileDirectory)..\bench\**\*.csproj" /> + + + + + \ No newline at end of file diff --git a/eng/build.ps1 b/eng/build.ps1 new file mode 100644 index 0000000000..fcc7477afa --- /dev/null +++ b/eng/build.ps1 @@ -0,0 +1,177 @@ +[CmdletBinding(PositionalBinding=$false, DefaultParameterSetName = 'CommandLine')] +Param( + [Parameter(ParameterSetName='CommandLine')] + [string][Alias('c')]$configuration = "Debug", + [Parameter(ParameterSetName='CommandLine')] + [string]$platform = $null, + [Parameter(ParameterSetName='CommandLine')] + [string] $projects, + [Parameter(ParameterSetName='CommandLine')] + [string][Alias('v')]$verbosity = "minimal", + [Parameter(ParameterSetName='CommandLine')] + [string] $msbuildEngine = $null, + [Parameter(ParameterSetName='CommandLine')] + [boolean] $warnAsError = $false, # NOTE: inverted the Arcade's default + [Parameter(ParameterSetName='CommandLine')] + [boolean] $nodeReuse = $true, + [Parameter(ParameterSetName='CommandLine')] + [Parameter(ParameterSetName='VisualStudio')] + [switch][Alias('r')]$restore, + [Parameter(ParameterSetName='CommandLine')] + [switch] $deployDeps, + [Parameter(ParameterSetName='CommandLine')] + [switch][Alias('b')]$build, + [Parameter(ParameterSetName='CommandLine')] + [switch] $rebuild, + [Parameter(ParameterSetName='CommandLine')] + [switch] $deploy, + [Parameter(ParameterSetName='CommandLine')] + [switch][Alias('t')]$test, + [Parameter(ParameterSetName='CommandLine')] + [switch] $integrationTest, + [Parameter(ParameterSetName='CommandLine')] + [switch] $performanceTest, + [Parameter(ParameterSetName='CommandLine')] + [switch] $sign, + [Parameter(ParameterSetName='CommandLine')] + [switch] $pack, + [Parameter(ParameterSetName='CommandLine')] + [switch] $publish, + [Parameter(ParameterSetName='CommandLine')] + [switch] $clean, + [Parameter(ParameterSetName='CommandLine')] + [switch][Alias('bl')]$binaryLog, + [Parameter(ParameterSetName='CommandLine')] + [switch][Alias('nobl')]$excludeCIBinarylog, + [Parameter(ParameterSetName='CommandLine')] + [switch] $ci, + [Parameter(ParameterSetName='CommandLine')] + [switch] $prepareMachine, + [Parameter(ParameterSetName='CommandLine')] + [string] $runtimeSourceFeed = '', + [Parameter(ParameterSetName='CommandLine')] + [string] $runtimeSourceFeedKey = '', + [Parameter(ParameterSetName='CommandLine')] + [switch] $excludePrereleaseVS, + [Parameter(ParameterSetName='CommandLine')] + [switch] $nativeToolsOnMachine, + [Parameter(ParameterSetName='CommandLine')] + [switch] $help, + + [Parameter(ParameterSetName='CommandLine')] + [Parameter(ParameterSetName='VisualStudio')] + [string[]] $onlyTfms = $null, + + [Parameter(ParameterSetName='VisualStudio')] + [string[]] $vs = $null, + [Parameter(ParameterSetName='VisualStudio')] + [switch] $noLaunch = $false, + + [Parameter(ValueFromRemainingArguments=$true)][String[]]$properties +) + +function Print-Usage() { + Write-Host "Custom settings:" + Write-Host " -vs Comma delimited list of keywords to filter the projects in the solution." + Write-Host " Pass '*' to generate a solution with all projects." + Write-Host " -noLaunch Don't open the generated solution in Visual Studio (only if -vs specified)" + Write-Host " -onlyTfms Semi-colon delimited list of TFMs to build (e.g. 'net8.0;net6.0')" + Write-Host "" +} + +if ($help) { + Get-Help $PSCommandPath + + Print-Usage; + . $PSScriptRoot/common/build.ps1 -help + exit 0 +} + +if ($onlyTfms.Count -gt 0) { + $onlyTfms -join ';' | Out-File '.targetframeworks' +} + +$filter = $vs +if ($filter.Count -ne 0) { + try { + # Install required toolset + . $PSScriptRoot/common/tools.ps1 + InitializeDotNetCli -install $true + + Push-Location $PSScriptRoot/../ + if ($filter -eq '*') { + ./scripts/Slngen.ps1 -All -OutSln SDK.sln -NoLaunch + } + else { + ./scripts/Slngen.ps1 $filter -OutSln SDK.sln -NoLaunch + } + + if ($noLaunch -eq $false) { + Write-Host "Launching Visual Studio..." + Start-Process ./start-vs.cmd + exit 0; + } + + # We generated a new solution and we'll need to restore it before it's usable. + $restore = $true; + } + catch { + exit $LASTEXITCODE; + } + finally { + Pop-Location + } +} + +# If no projects explicitly specified, look for a top-level solution file. +# - If there's no solution file found - no worries, build the default project configured in eng\Build.props. +# - If a solution file is found - buid it. +# - If more than one solution is found - fail. +if ([string]::IsNullOrWhiteSpace($projects)) { + [object[]]$slnFiles = Get-ChildItem -Path $PSScriptRoot/../ -Filter *.sln; + + if ($slnFiles.Count -gt 1) { + Write-Host "[ERROR] Multiple .sln files found in the root of the repository. Use '-projects' to specify the one you wish to build." -ForegroundColor Red; + exit -1; + } + + if ($slnFiles.Count -eq 1) { + $projects = $slnFiles[0].FullName; + Write-Host "[INFO] Building $projects..." -ForegroundColor DarkYellow + } + else { + Write-Host "[INFO] Building the default project as configured in eng\Build.props..." -ForegroundColor Cyan + } +} + +. $PSScriptRoot/common/build.ps1 ` + -configuration $configuration ` + -platform $platform ` + -projects $projects ` + -verbosity $verbosity ` + -msbuildEngine $msbuildEngine ` + -warnAsError $([boolean]::Parse("$warnAsError")) ` + -nodeReuse $nodeReuse ` + -restore:$restore ` + -deployDeps:$deployDeps ` + -build:$build ` + -rebuild:$rebuild ` + -deploy:$deploy ` + -test:$test ` + -integrationTest:$integrationTest ` + -performanceTest:$performanceTest ` + -sign:$sign ` + -pack:$pack ` + -publish:$publish ` + -clean:$clean ` + -binaryLog:$binaryLog ` + -excludeCIBinarylog:$excludeCIBinarylog ` + -ci:$ci ` + -prepareMachine:$prepareMachine ` + -runtimeSourceFeed $runtimeSourceFeed ` + -runtimeSourceFeedKey $runtimeSourceFeedKey ` + -excludePrereleaseVS:$excludePrereleaseVS ` + -nativeToolsOnMachine:$nativeToolsOnMachine ` + -help:$help ` + @properties + diff --git a/eng/build.sh b/eng/build.sh new file mode 100755 index 0000000000..15b80357f5 --- /dev/null +++ b/eng/build.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash + +# Stop script if unbound variable found (use ${var:-} if intentional) +set -u + +# Stop script if command returns non-zero exit code. +# Prevents hidden errors caused by missing error code propagation. +set -e + +usage() +{ + echo "Custom settings:" + echo " --vs Comma delimited list of keywords to filter the projects in the solution" + echo " Pass '*' to generate a solution with all projects." + echo " --onlyTfms Semi-colon delimited list of TFMs to build (e.g. 'net8.0;net6.0')" + echo "" +} + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +filter=false +keywords='' +onlyTfms='' +hasProjects=false +hasWarnAsError=false +hasRestore=false + +properties='' + +while [[ $# > 0 ]]; do + opt="$(echo "${1/#--/-}" | tr "[:upper:]" "[:lower:]")" + case "$opt" in + -help|-h) + usage + "$DIR/common/build.sh" --help + exit 0 + ;; + -vs) + filter=true + shift + keywords=$1 + ;; + -onlytfms) + shift + onlyTfms=$1 + ;; + -projects) + hasProjects=true + # Pass through resolving the full path to the project + properties="$properties $1 $(realpath $2)" + shift + ;; + -restore) + hasRestore=true + properties="$properties $1" + ;; + -warnaserror) + hasWarnAsError=true + # Pass through converting to boolean + value=false + if [[ "${2,,}" == "true" || "$2" == "1" ]]; then + value=true + fi + properties="$properties $1 $value" + shift + ;; + *) + properties="$properties $1" + ;; + esac + + shift +done + +if [[ -n "${onlyTfms// /}" ]]; then + echo $onlyTfms > ./.targetframeworks +fi + +if [[ "$filter" == true ]]; then + # Install required toolset + . "$DIR/common/tools.sh" + InitializeDotNetCli true + + # Invoke the solution generator + script=$(realpath $DIR/../scripts/Slngen.ps1) + + if [[ "$keywords" == '*' ]]; then + pwsh -Command "&{ $script -All -OutSln SDK.sln -NoLaunch }" + else + pwsh -Command "&{ $script -Keywords $keywords -OutSln SDK.sln -NoLaunch }" + fi + + # We generated a new solution and we'll need to restore it before it's usable. + if [[ "$hasRestore" == false ]]; then + properties="$properties --restore" + fi +fi + +# If no projects explicitly specified, look for a top-level solution file. +# - If there's no solution file found - no worries, build the default project configured in eng\Build.props. +# - If a solution file is found - buid it. +# - If more than one solution is found - fail. +if [[ "$hasProjects" == false ]]; then + repoRoot=$(realpath $DIR/../) + fileCount=$(find $repoRoot -path "$repoRoot/*.sln" | wc -l) + if [[ $fileCount > 1 ]]; then + echo -e '\e[31m[ERROR] Multiple .sln files found in the root of the repository. Use '--projects' to specify the one you wish to build.\e[0m' >&2 + exit -1 + fi + + if [[ $fileCount == 1 ]]; then + solution=$(realpath $(find $repoRoot/*.sln)) + echo -e "\e[33m[INFO] Building $solution...\e[0m" + properties="$properties --projects $solution" + else + echo -e '\e[34m[INFO] Building the default project as configured in eng/Build.props...\e[0m' + fi +fi + +# The Arcade's default is "warnAsError=true", we want the opposite by default. +if [[ "$hasWarnAsError" == false ]]; then + properties="$properties --warnAsError false" +fi + +"$DIR/common/build.sh" $properties diff --git a/eng/common/BuildConfiguration/build-configuration.json b/eng/common/BuildConfiguration/build-configuration.json new file mode 100644 index 0000000000..3d1cc89894 --- /dev/null +++ b/eng/common/BuildConfiguration/build-configuration.json @@ -0,0 +1,4 @@ +{ + "RetryCountLimit": 1, + "RetryByAnyError": false +} diff --git a/eng/common/CIBuild.cmd b/eng/common/CIBuild.cmd new file mode 100644 index 0000000000..56c2f25ac2 --- /dev/null +++ b/eng/common/CIBuild.cmd @@ -0,0 +1,2 @@ +@echo off +powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0Build.ps1""" -restore -build -test -sign -pack -publish -ci %*" \ No newline at end of file diff --git a/eng/common/PSScriptAnalyzerSettings.psd1 b/eng/common/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 0000000000..4c1ea7c98e --- /dev/null +++ b/eng/common/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,11 @@ +@{ + IncludeRules=@('PSAvoidUsingCmdletAliases', + 'PSAvoidUsingWMICmdlet', + 'PSAvoidUsingPositionalParameters', + 'PSAvoidUsingInvokeExpression', + 'PSUseDeclaredVarsMoreThanAssignments', + 'PSUseCmdletCorrectly', + 'PSStandardDSCFunctionsInResource', + 'PSUseIdenticalMandatoryParametersForDSC', + 'PSUseIdenticalParametersForDSC') +} \ No newline at end of file diff --git a/eng/common/README.md b/eng/common/README.md new file mode 100644 index 0000000000..ff49c37152 --- /dev/null +++ b/eng/common/README.md @@ -0,0 +1,28 @@ +# Don't touch this folder + + uuuuuuuuuuuuuuuuuuuu + u" uuuuuuuuuuuuuuuuuu "u + u" u$$$$$$$$$$$$$$$$$$$$u "u + u" u$$$$$$$$$$$$$$$$$$$$$$$$u "u + u" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$u "u + u" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$u "u + u" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$u "u + $ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $ + $ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $ + $ $$$" ... "$... ...$" ... "$$$ ... "$$$ $ + $ $$$u `"$$$$$$$ $$$ $$$$$ $$ $$$ $$$ $ + $ $$$$$$uu "$$$$ $$$ $$$$$ $$ """ u$$$ $ + $ $$$""$$$ $$$$ $$$u "$$$" u$$ $$$$$$$$ $ + $ $$$$....,$$$$$..$$$$$....,$$$$..$$$$$$$$ $ + $ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $ + "u "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" u" + "u "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" u" + "u "$$$$$$$$$$$$$$$$$$$$$$$$$$$$" u" + "u "$$$$$$$$$$$$$$$$$$$$$$$$" u" + "u "$$$$$$$$$$$$$$$$$$$$" u" + "u """""""""""""""""" u" + """""""""""""""""""" + +!!! Changes made in this directory are subject to being overwritten by automation !!! + +The files in this directory are shared by all Arcade repos and managed by automation. If you need to make changes to these files, open an issue or submit a pull request to https://github.com/dotnet/arcade first. diff --git a/eng/common/SetupNugetSources.ps1 b/eng/common/SetupNugetSources.ps1 new file mode 100644 index 0000000000..6e99723945 --- /dev/null +++ b/eng/common/SetupNugetSources.ps1 @@ -0,0 +1,167 @@ +# This file is a temporary workaround for internal builds to be able to restore from private AzDO feeds. +# This file should be removed as part of this issue: https://github.com/dotnet/arcade/issues/4080 +# +# What the script does is iterate over all package sources in the pointed NuGet.config and add a credential entry +# under for each Maestro managed private feed. Two additional credential +# entries are also added for the two private static internal feeds: dotnet3-internal and dotnet3-internal-transport. +# +# This script needs to be called in every job that will restore packages and which the base repo has +# private AzDO feeds in the NuGet.config. +# +# See example YAML call for this script below. Note the use of the variable `$(dn-bot-dnceng-artifact-feeds-rw)` +# from the AzureDevOps-Artifact-Feeds-Pats variable group. +# +# Any disabledPackageSources entries which start with "darc-int" will be re-enabled as part of this script executing +# +# - task: PowerShell@2 +# displayName: Setup Private Feeds Credentials +# condition: eq(variables['Agent.OS'], 'Windows_NT') +# inputs: +# filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.ps1 +# arguments: -ConfigFile $(Build.SourcesDirectory)/NuGet.config -Password $Env:Token +# env: +# Token: $(dn-bot-dnceng-artifact-feeds-rw) + +[CmdletBinding()] +param ( + [Parameter(Mandatory = $true)][string]$ConfigFile, + [Parameter(Mandatory = $true)][string]$Password +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 2.0 +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +. $PSScriptRoot\tools.ps1 + +# Add source entry to PackageSources +function AddPackageSource($sources, $SourceName, $SourceEndPoint, $creds, $Username, $Password) { + $packageSource = $sources.SelectSingleNode("add[@key='$SourceName']") + + if ($packageSource -eq $null) + { + $packageSource = $doc.CreateElement("add") + $packageSource.SetAttribute("key", $SourceName) + $packageSource.SetAttribute("value", $SourceEndPoint) + $sources.AppendChild($packageSource) | Out-Null + } + else { + Write-Host "Package source $SourceName already present." + } + + AddCredential -Creds $creds -Source $SourceName -Username $Username -Password $Password +} + +# Add a credential node for the specified source +function AddCredential($creds, $source, $username, $password) { + # Looks for credential configuration for the given SourceName. Create it if none is found. + $sourceElement = $creds.SelectSingleNode($Source) + if ($sourceElement -eq $null) + { + $sourceElement = $doc.CreateElement($Source) + $creds.AppendChild($sourceElement) | Out-Null + } + + # Add the node to the credential if none is found. + $usernameElement = $sourceElement.SelectSingleNode("add[@key='Username']") + if ($usernameElement -eq $null) + { + $usernameElement = $doc.CreateElement("add") + $usernameElement.SetAttribute("key", "Username") + $sourceElement.AppendChild($usernameElement) | Out-Null + } + $usernameElement.SetAttribute("value", $Username) + + # Add the to the credential if none is found. + # Add it as a clear text because there is no support for encrypted ones in non-windows .Net SDKs. + # -> https://github.com/NuGet/Home/issues/5526 + $passwordElement = $sourceElement.SelectSingleNode("add[@key='ClearTextPassword']") + if ($passwordElement -eq $null) + { + $passwordElement = $doc.CreateElement("add") + $passwordElement.SetAttribute("key", "ClearTextPassword") + $sourceElement.AppendChild($passwordElement) | Out-Null + } + $passwordElement.SetAttribute("value", $Password) +} + +function InsertMaestroPrivateFeedCredentials($Sources, $Creds, $Username, $Password) { + $maestroPrivateSources = $Sources.SelectNodes("add[contains(@key,'darc-int')]") + + Write-Host "Inserting credentials for $($maestroPrivateSources.Count) Maestro's private feeds." + + ForEach ($PackageSource in $maestroPrivateSources) { + Write-Host "`tInserting credential for Maestro's feed:" $PackageSource.Key + AddCredential -Creds $creds -Source $PackageSource.Key -Username $Username -Password $Password + } +} + +function EnablePrivatePackageSources($DisabledPackageSources) { + $maestroPrivateSources = $DisabledPackageSources.SelectNodes("add[contains(@key,'darc-int')]") + ForEach ($DisabledPackageSource in $maestroPrivateSources) { + Write-Host "`tEnsuring private source '$($DisabledPackageSource.key)' is enabled by deleting it from disabledPackageSource" + # Due to https://github.com/NuGet/Home/issues/10291, we must actually remove the disabled entries + $DisabledPackageSources.RemoveChild($DisabledPackageSource) + } +} + +if (!(Test-Path $ConfigFile -PathType Leaf)) { + Write-PipelineTelemetryError -Category 'Build' -Message "Eng/common/SetupNugetSources.ps1 returned a non-zero exit code. Couldn't find the NuGet config file: $ConfigFile" + ExitWithExitCode 1 +} + +if (!$Password) { + Write-PipelineTelemetryError -Category 'Build' -Message 'Eng/common/SetupNugetSources.ps1 returned a non-zero exit code. Please supply a valid PAT' + ExitWithExitCode 1 +} + +# Load NuGet.config +$doc = New-Object System.Xml.XmlDocument +$filename = (Get-Item $ConfigFile).FullName +$doc.Load($filename) + +# Get reference to or create one if none exist already +$sources = $doc.DocumentElement.SelectSingleNode("packageSources") +if ($sources -eq $null) { + $sources = $doc.CreateElement("packageSources") + $doc.DocumentElement.AppendChild($sources) | Out-Null +} + +# Looks for a node. Create it if none is found. +$creds = $doc.DocumentElement.SelectSingleNode("packageSourceCredentials") +if ($creds -eq $null) { + $creds = $doc.CreateElement("packageSourceCredentials") + $doc.DocumentElement.AppendChild($creds) | Out-Null +} + +# Check for disabledPackageSources; we'll enable any darc-int ones we find there +$disabledSources = $doc.DocumentElement.SelectSingleNode("disabledPackageSources") +if ($disabledSources -ne $null) { + Write-Host "Checking for any darc-int disabled package sources in the disabledPackageSources node" + EnablePrivatePackageSources -DisabledPackageSources $disabledSources +} + +$userName = "dn-bot" + +# Insert credential nodes for Maestro's private feeds +InsertMaestroPrivateFeedCredentials -Sources $sources -Creds $creds -Username $userName -Password $Password + +# 3.1 uses a different feed url format so it's handled differently here +$dotnet31Source = $sources.SelectSingleNode("add[@key='dotnet3.1']") +if ($dotnet31Source -ne $null) { + AddPackageSource -Sources $sources -SourceName "dotnet3.1-internal" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/_packaging/dotnet3.1-internal/nuget/v2" -Creds $creds -Username $userName -Password $Password + AddPackageSource -Sources $sources -SourceName "dotnet3.1-internal-transport" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/_packaging/dotnet3.1-internal-transport/nuget/v2" -Creds $creds -Username $userName -Password $Password +} + +$dotnetVersions = @('5','6','7') + +foreach ($dotnetVersion in $dotnetVersions) { + $feedPrefix = "dotnet" + $dotnetVersion; + $dotnetSource = $sources.SelectSingleNode("add[@key='$feedPrefix']") + if ($dotnetSource -ne $null) { + AddPackageSource -Sources $sources -SourceName "$feedPrefix-internal" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/internal/_packaging/$feedPrefix-internal/nuget/v2" -Creds $creds -Username $userName -Password $Password + AddPackageSource -Sources $sources -SourceName "$feedPrefix-internal-transport" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/internal/_packaging/$feedPrefix-internal-transport/nuget/v2" -Creds $creds -Username $userName -Password $Password + } +} + +$doc.Save($filename) diff --git a/eng/common/SetupNugetSources.sh b/eng/common/SetupNugetSources.sh new file mode 100755 index 0000000000..8af7d899db --- /dev/null +++ b/eng/common/SetupNugetSources.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash + +# This file is a temporary workaround for internal builds to be able to restore from private AzDO feeds. +# This file should be removed as part of this issue: https://github.com/dotnet/arcade/issues/4080 +# +# What the script does is iterate over all package sources in the pointed NuGet.config and add a credential entry +# under for each Maestro's managed private feed. Two additional credential +# entries are also added for the two private static internal feeds: dotnet3-internal and dotnet3-internal-transport. +# +# This script needs to be called in every job that will restore packages and which the base repo has +# private AzDO feeds in the NuGet.config. +# +# See example YAML call for this script below. Note the use of the variable `$(dn-bot-dnceng-artifact-feeds-rw)` +# from the AzureDevOps-Artifact-Feeds-Pats variable group. +# +# Any disabledPackageSources entries which start with "darc-int" will be re-enabled as part of this script executing. +# +# - task: Bash@3 +# displayName: Setup Private Feeds Credentials +# inputs: +# filePath: $(Build.SourcesDirectory)/eng/common/SetupNugetSources.sh +# arguments: $(Build.SourcesDirectory)/NuGet.config $Token +# condition: ne(variables['Agent.OS'], 'Windows_NT') +# env: +# Token: $(dn-bot-dnceng-artifact-feeds-rw) + +ConfigFile=$1 +CredToken=$2 +NL='\n' +TB=' ' + +source="${BASH_SOURCE[0]}" + +# resolve $source until the file is no longer a symlink +while [[ -h "$source" ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +. "$scriptroot/tools.sh" + +if [ ! -f "$ConfigFile" ]; then + Write-PipelineTelemetryError -Category 'Build' "Error: Eng/common/SetupNugetSources.sh returned a non-zero exit code. Couldn't find the NuGet config file: $ConfigFile" + ExitWithExitCode 1 +fi + +if [ -z "$CredToken" ]; then + Write-PipelineTelemetryError -category 'Build' "Error: Eng/common/SetupNugetSources.sh returned a non-zero exit code. Please supply a valid PAT" + ExitWithExitCode 1 +fi + +if [[ `uname -s` == "Darwin" ]]; then + NL=$'\\\n' + TB='' +fi + +# Ensure there is a ... section. +grep -i "" $ConfigFile +if [ "$?" != "0" ]; then + echo "Adding ... section." + ConfigNodeHeader="" + PackageSourcesTemplate="${TB}${NL}${TB}" + + sed -i.bak "s|$ConfigNodeHeader|$ConfigNodeHeader${NL}$PackageSourcesTemplate|" $ConfigFile +fi + +# Ensure there is a ... section. +grep -i "" $ConfigFile +if [ "$?" != "0" ]; then + echo "Adding ... section." + + PackageSourcesNodeFooter="" + PackageSourceCredentialsTemplate="${TB}${NL}${TB}" + + sed -i.bak "s|$PackageSourcesNodeFooter|$PackageSourcesNodeFooter${NL}$PackageSourceCredentialsTemplate|" $ConfigFile +fi + +PackageSources=() + +# Ensure dotnet3.1-internal and dotnet3.1-internal-transport are in the packageSources if the public dotnet3.1 feeds are present +grep -i "" + + sed -i.bak "s|$PackageSourcesNodeFooter|$PackageSourceTemplate${NL}$PackageSourcesNodeFooter|" $ConfigFile + fi + PackageSources+=('dotnet3.1-internal') + + grep -i "" $ConfigFile + if [ "$?" != "0" ]; then + echo "Adding dotnet3.1-internal-transport to the packageSources." + PackageSourcesNodeFooter="" + PackageSourceTemplate="${TB}" + + sed -i.bak "s|$PackageSourcesNodeFooter|$PackageSourceTemplate${NL}$PackageSourcesNodeFooter|" $ConfigFile + fi + PackageSources+=('dotnet3.1-internal-transport') +fi + +DotNetVersions=('5' '6' '7') + +for DotNetVersion in ${DotNetVersions[@]} ; do + FeedPrefix="dotnet${DotNetVersion}"; + grep -i "" + + sed -i.bak "s|$PackageSourcesNodeFooter|$PackageSourceTemplate${NL}$PackageSourcesNodeFooter|" $ConfigFile + fi + PackageSources+=("$FeedPrefix-internal") + + grep -i "" $ConfigFile + if [ "$?" != "0" ]; then + echo "Adding $FeedPrefix-internal-transport to the packageSources." + PackageSourcesNodeFooter="" + PackageSourceTemplate="${TB}" + + sed -i.bak "s|$PackageSourcesNodeFooter|$PackageSourceTemplate${NL}$PackageSourcesNodeFooter|" $ConfigFile + fi + PackageSources+=("$FeedPrefix-internal-transport") + fi +done + +# I want things split line by line +PrevIFS=$IFS +IFS=$'\n' +PackageSources+="$IFS" +PackageSources+=$(grep -oh '"darc-int-[^"]*"' $ConfigFile | tr -d '"') +IFS=$PrevIFS + +for FeedName in ${PackageSources[@]} ; do + # Check if there is no existing credential for this FeedName + grep -i "<$FeedName>" $ConfigFile + if [ "$?" != "0" ]; then + echo "Adding credentials for $FeedName." + + PackageSourceCredentialsNodeFooter="" + NewCredential="${TB}${TB}<$FeedName>${NL}${NL}${NL}" + + sed -i.bak "s|$PackageSourceCredentialsNodeFooter|$NewCredential${NL}$PackageSourceCredentialsNodeFooter|" $ConfigFile + fi +done + +# Re-enable any entries in disabledPackageSources where the feed name contains darc-int +grep -i "" $ConfigFile +if [ "$?" == "0" ]; then + DisabledDarcIntSources=() + echo "Re-enabling any disabled \"darc-int\" package sources in $ConfigFile" + DisabledDarcIntSources+=$(grep -oh '"darc-int-[^"]*" value="true"' $ConfigFile | tr -d '"') + for DisabledSourceName in ${DisabledDarcIntSources[@]} ; do + if [[ $DisabledSourceName == darc-int* ]] + then + OldDisableValue="" + NewDisableValue="" + sed -i.bak "s|$OldDisableValue|$NewDisableValue|" $ConfigFile + echo "Neutralized disablePackageSources entry for '$DisabledSourceName'" + fi + done +fi diff --git a/eng/common/build.ps1 b/eng/common/build.ps1 new file mode 100644 index 0000000000..33a6f2d0e2 --- /dev/null +++ b/eng/common/build.ps1 @@ -0,0 +1,166 @@ +[CmdletBinding(PositionalBinding=$false)] +Param( + [string][Alias('c')]$configuration = "Debug", + [string]$platform = $null, + [string] $projects, + [string][Alias('v')]$verbosity = "minimal", + [string] $msbuildEngine = $null, + [bool] $warnAsError = $true, + [bool] $nodeReuse = $true, + [switch][Alias('r')]$restore, + [switch] $deployDeps, + [switch][Alias('b')]$build, + [switch] $rebuild, + [switch] $deploy, + [switch][Alias('t')]$test, + [switch] $integrationTest, + [switch] $performanceTest, + [switch] $sign, + [switch] $pack, + [switch] $publish, + [switch] $clean, + [switch][Alias('bl')]$binaryLog, + [switch][Alias('nobl')]$excludeCIBinarylog, + [switch] $ci, + [switch] $prepareMachine, + [string] $runtimeSourceFeed = '', + [string] $runtimeSourceFeedKey = '', + [switch] $excludePrereleaseVS, + [switch] $nativeToolsOnMachine, + [switch] $help, + [Parameter(ValueFromRemainingArguments=$true)][String[]]$properties +) + +# Unset 'Platform' environment variable to avoid unwanted collision in InstallDotNetCore.targets file +# some computer has this env var defined (e.g. Some HP) +if($env:Platform) { + $env:Platform="" +} +function Print-Usage() { + Write-Host "Common settings:" + Write-Host " -configuration Build configuration: 'Debug' or 'Release' (short: -c)" + Write-Host " -platform Platform configuration: 'x86', 'x64' or any valid Platform value to pass to msbuild" + Write-Host " -verbosity Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic] (short: -v)" + Write-Host " -binaryLog Output binary log (short: -bl)" + Write-Host " -help Print help and exit" + Write-Host "" + + Write-Host "Actions:" + Write-Host " -restore Restore dependencies (short: -r)" + Write-Host " -build Build solution (short: -b)" + Write-Host " -rebuild Rebuild solution" + Write-Host " -deploy Deploy built VSIXes" + Write-Host " -deployDeps Deploy dependencies (e.g. VSIXes for integration tests)" + Write-Host " -test Run all unit tests in the solution (short: -t)" + Write-Host " -integrationTest Run all integration tests in the solution" + Write-Host " -performanceTest Run all performance tests in the solution" + Write-Host " -pack Package build outputs into NuGet packages and Willow components" + Write-Host " -sign Sign build outputs" + Write-Host " -publish Publish artifacts (e.g. symbols)" + Write-Host " -clean Clean the solution" + Write-Host "" + + Write-Host "Advanced settings:" + Write-Host " -projects Semi-colon delimited list of sln/proj's to build. Globbing is supported (*.sln)" + Write-Host " -ci Set when running on CI server" + Write-Host " -excludeCIBinarylog Don't output binary log (short: -nobl)" + Write-Host " -prepareMachine Prepare machine for CI run, clean up processes after build" + Write-Host " -warnAsError Sets warnaserror msbuild parameter ('true' or 'false')" + Write-Host " -msbuildEngine Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)." + Write-Host " -excludePrereleaseVS Set to exclude build engines in prerelease versions of Visual Studio" + Write-Host " -nativeToolsOnMachine Sets the native tools on machine environment variable (indicating that the script should use native tools on machine)" + Write-Host "" + + Write-Host "Command line arguments not listed above are passed thru to msbuild." + Write-Host "The above arguments can be shortened as much as to be unambiguous (e.g. -co for configuration, -t for test, etc.)." +} + +. $PSScriptRoot\tools.ps1 + +function InitializeCustomToolset { + if (-not $restore) { + return + } + + $script = Join-Path $EngRoot 'restore-toolset.ps1' + + if (Test-Path $script) { + . $script + } +} + +function Build { + $toolsetBuildProj = InitializeToolset + InitializeCustomToolset + + $bl = if ($binaryLog) { '/bl:' + (Join-Path $LogDir 'Build.binlog') } else { '' } + $platformArg = if ($platform) { "/p:Platform=$platform" } else { '' } + + if ($projects) { + # Re-assign properties to a new variable because PowerShell doesn't let us append properties directly for unclear reasons. + # Explicitly set the type as string[] because otherwise PowerShell would make this char[] if $properties is empty. + [string[]] $msbuildArgs = $properties + + # Resolve relative project paths into full paths + $projects = ($projects.Split(';').ForEach({Resolve-Path $_}) -join ';') + + $msbuildArgs += "/p:Projects=$projects" + $properties = $msbuildArgs + } + + MSBuild $toolsetBuildProj ` + $bl ` + $platformArg ` + /p:Configuration=$configuration ` + /p:RepoRoot=$RepoRoot ` + /p:Restore=$restore ` + /p:DeployDeps=$deployDeps ` + /p:Build=$build ` + /p:Rebuild=$rebuild ` + /p:Deploy=$deploy ` + /p:Test=$test ` + /p:Pack=$pack ` + /p:IntegrationTest=$integrationTest ` + /p:PerformanceTest=$performanceTest ` + /p:Sign=$sign ` + /p:Publish=$publish ` + @properties +} + +try { + if ($clean) { + if (Test-Path $ArtifactsDir) { + Remove-Item -Recurse -Force $ArtifactsDir + Write-Host 'Artifacts directory deleted.' + } + exit 0 + } + + if ($help -or (($null -ne $properties) -and ($properties.Contains('/help') -or $properties.Contains('/?')))) { + Print-Usage + exit 0 + } + + if ($ci) { + if (-not $excludeCIBinarylog) { + $binaryLog = $true + } + $nodeReuse = $false + } + + if ($nativeToolsOnMachine) { + $env:NativeToolsOnMachine = $true + } + if ($restore) { + InitializeNativeTools + } + + Build +} +catch { + Write-Host $_.ScriptStackTrace + Write-PipelineTelemetryError -Category 'InitializeToolset' -Message $_ + ExitWithExitCode 1 +} + +ExitWithExitCode 0 diff --git a/eng/common/build.sh b/eng/common/build.sh new file mode 100755 index 0000000000..50af40cdd2 --- /dev/null +++ b/eng/common/build.sh @@ -0,0 +1,247 @@ +#!/usr/bin/env bash + +# Stop script if unbound variable found (use ${var:-} if intentional) +set -u + +# Stop script if command returns non-zero exit code. +# Prevents hidden errors caused by missing error code propagation. +set -e + +usage() +{ + echo "Common settings:" + echo " --configuration Build configuration: 'Debug' or 'Release' (short: -c)" + echo " --verbosity Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic] (short: -v)" + echo " --binaryLog Create MSBuild binary log (short: -bl)" + echo " --help Print help and exit (short: -h)" + echo "" + + echo "Actions:" + echo " --restore Restore dependencies (short: -r)" + echo " --build Build solution (short: -b)" + echo " --sourceBuild Source-build the solution (short: -sb)" + echo " Will additionally trigger the following actions: --restore, --build, --pack" + echo " If --configuration is not set explicitly, will also set it to 'Release'" + echo " --rebuild Rebuild solution" + echo " --test Run all unit tests in the solution (short: -t)" + echo " --integrationTest Run all integration tests in the solution" + echo " --performanceTest Run all performance tests in the solution" + echo " --pack Package build outputs into NuGet packages and Willow components" + echo " --sign Sign build outputs" + echo " --publish Publish artifacts (e.g. symbols)" + echo " --clean Clean the solution" + echo "" + + echo "Advanced settings:" + echo " --projects Project or solution file(s) to build" + echo " --ci Set when running on CI server" + echo " --excludeCIBinarylog Don't output binary log (short: -nobl)" + echo " --prepareMachine Prepare machine for CI run, clean up processes after build" + echo " --nodeReuse Sets nodereuse msbuild parameter ('true' or 'false')" + echo " --warnAsError Sets warnaserror msbuild parameter ('true' or 'false')" + echo "" + echo "Command line arguments not listed above are passed thru to msbuild." + echo "Arguments can also be passed in with a single hyphen." +} + +source="${BASH_SOURCE[0]}" + +# resolve $source until the file is no longer a symlink +while [[ -h "$source" ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +restore=false +build=false +source_build=false +rebuild=false +test=false +integration_test=false +performance_test=false +pack=false +publish=false +sign=false +public=false +ci=false +clean=false + +warn_as_error=true +node_reuse=true +binary_log=false +exclude_ci_binary_log=false +pipelines_log=false + +projects='' +configuration='' +prepare_machine=false +verbosity='minimal' +runtime_source_feed='' +runtime_source_feed_key='' + +properties='' +while [[ $# > 0 ]]; do + opt="$(echo "${1/#--/-}" | tr "[:upper:]" "[:lower:]")" + case "$opt" in + -help|-h) + usage + exit 0 + ;; + -clean) + clean=true + ;; + -configuration|-c) + configuration=$2 + shift + ;; + -verbosity|-v) + verbosity=$2 + shift + ;; + -binarylog|-bl) + binary_log=true + ;; + -excludeCIBinarylog|-nobl) + exclude_ci_binary_log=true + ;; + -pipelineslog|-pl) + pipelines_log=true + ;; + -restore|-r) + restore=true + ;; + -build|-b) + build=true + ;; + -rebuild) + rebuild=true + ;; + -pack) + pack=true + ;; + -sourcebuild|-sb) + build=true + source_build=true + restore=true + pack=true + ;; + -test|-t) + test=true + ;; + -integrationtest) + integration_test=true + ;; + -performancetest) + performance_test=true + ;; + -sign) + sign=true + ;; + -publish) + publish=true + ;; + -preparemachine) + prepare_machine=true + ;; + -projects) + projects=$2 + shift + ;; + -ci) + ci=true + ;; + -warnaserror) + warn_as_error=$2 + shift + ;; + -nodereuse) + node_reuse=$2 + shift + ;; + -runtimesourcefeed) + runtime_source_feed=$2 + shift + ;; + -runtimesourcefeedkey) + runtime_source_feed_key=$2 + shift + ;; + *) + properties="$properties $1" + ;; + esac + + shift +done + +if [[ -z "$configuration" ]]; then + if [[ "$source_build" = true ]]; then configuration="Release"; else configuration="Debug"; fi +fi + +if [[ "$ci" == true ]]; then + pipelines_log=true + node_reuse=false + if [[ "$exclude_ci_binary_log" == false ]]; then + binary_log=true + fi +fi + +. "$scriptroot/tools.sh" + +function InitializeCustomToolset { + local script="$eng_root/restore-toolset.sh" + + if [[ -a "$script" ]]; then + . "$script" + fi +} + +function Build { + InitializeToolset + InitializeCustomToolset + + if [[ ! -z "$projects" ]]; then + properties="$properties /p:Projects=$projects" + fi + + local bl="" + if [[ "$binary_log" == true ]]; then + bl="/bl:\"$log_dir/Build.binlog\"" + fi + + MSBuild $_InitializeToolset \ + $bl \ + /p:Configuration=$configuration \ + /p:RepoRoot="$repo_root" \ + /p:Restore=$restore \ + /p:Build=$build \ + /p:ArcadeBuildFromSource=$source_build \ + /p:Rebuild=$rebuild \ + /p:Test=$test \ + /p:Pack=$pack \ + /p:IntegrationTest=$integration_test \ + /p:PerformanceTest=$performance_test \ + /p:Sign=$sign \ + /p:Publish=$publish \ + $properties + + ExitWithExitCode 0 +} + +if [[ "$clean" == true ]]; then + if [ -d "$artifacts_dir" ]; then + rm -rf $artifacts_dir + echo "Artifacts directory deleted." + fi + exit 0 +fi + +if [[ "$restore" == true ]]; then + InitializeNativeTools +fi + +Build diff --git a/eng/common/cibuild.sh b/eng/common/cibuild.sh new file mode 100755 index 0000000000..1a02c0dec8 --- /dev/null +++ b/eng/common/cibuild.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" + +# resolve $SOURCE until the file is no longer a symlink +while [[ -h $source ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + + # if $source was a relative symlink, we need to resolve it relative to the path where + # the symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +. "$scriptroot/build.sh" --restore --build --test --pack --publish --ci $@ \ No newline at end of file diff --git a/eng/common/cross/arm/sources.list.bionic b/eng/common/cross/arm/sources.list.bionic new file mode 100644 index 0000000000..2109557409 --- /dev/null +++ b/eng/common/cross/arm/sources.list.bionic @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted universe multiverse diff --git a/eng/common/cross/arm/sources.list.focal b/eng/common/cross/arm/sources.list.focal new file mode 100644 index 0000000000..4de2600c17 --- /dev/null +++ b/eng/common/cross/arm/sources.list.focal @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ focal main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ focal main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ focal-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ focal-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ focal-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ focal-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ focal-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ focal-security main restricted universe multiverse diff --git a/eng/common/cross/arm/sources.list.jammy b/eng/common/cross/arm/sources.list.jammy new file mode 100644 index 0000000000..6bb0453029 --- /dev/null +++ b/eng/common/cross/arm/sources.list.jammy @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ jammy main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ jammy main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ jammy-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ jammy-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ jammy-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ jammy-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ jammy-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ jammy-security main restricted universe multiverse diff --git a/eng/common/cross/arm/sources.list.jessie b/eng/common/cross/arm/sources.list.jessie new file mode 100644 index 0000000000..4d142ac9b1 --- /dev/null +++ b/eng/common/cross/arm/sources.list.jessie @@ -0,0 +1,3 @@ +# Debian (sid) # UNSTABLE +deb http://ftp.debian.org/debian/ sid main contrib non-free +deb-src http://ftp.debian.org/debian/ sid main contrib non-free diff --git a/eng/common/cross/arm/sources.list.xenial b/eng/common/cross/arm/sources.list.xenial new file mode 100644 index 0000000000..56fbb36a59 --- /dev/null +++ b/eng/common/cross/arm/sources.list.xenial @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ xenial main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ xenial-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ xenial-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ xenial-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-security main restricted universe multiverse diff --git a/eng/common/cross/arm/sources.list.zesty b/eng/common/cross/arm/sources.list.zesty new file mode 100644 index 0000000000..ea2c14a787 --- /dev/null +++ b/eng/common/cross/arm/sources.list.zesty @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ zesty main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ zesty-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ zesty-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ zesty-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty-security main restricted universe multiverse diff --git a/eng/common/cross/arm/tizen/tizen.patch b/eng/common/cross/arm/tizen/tizen.patch new file mode 100644 index 0000000000..fb12ade725 --- /dev/null +++ b/eng/common/cross/arm/tizen/tizen.patch @@ -0,0 +1,9 @@ +diff -u -r a/usr/lib/libc.so b/usr/lib/libc.so +--- a/usr/lib/libc.so 2016-12-30 23:00:08.284951863 +0900 ++++ b/usr/lib/libc.so 2016-12-30 23:00:32.140951815 +0900 +@@ -2,4 +2,4 @@ + Use the shared library, but some functions are only in + the static library, so try that secondarily. */ + OUTPUT_FORMAT(elf32-littlearm) +-GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a AS_NEEDED ( /lib/ld-linux-armhf.so.3 ) ) ++GROUP ( libc.so.6 libc_nonshared.a AS_NEEDED ( ld-linux-armhf.so.3 ) ) diff --git a/eng/common/cross/arm64/sources.list.bionic b/eng/common/cross/arm64/sources.list.bionic new file mode 100644 index 0000000000..2109557409 --- /dev/null +++ b/eng/common/cross/arm64/sources.list.bionic @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted universe multiverse diff --git a/eng/common/cross/arm64/sources.list.buster b/eng/common/cross/arm64/sources.list.buster new file mode 100644 index 0000000000..7194ac64a9 --- /dev/null +++ b/eng/common/cross/arm64/sources.list.buster @@ -0,0 +1,11 @@ +deb http://deb.debian.org/debian buster main +deb-src http://deb.debian.org/debian buster main + +deb http://deb.debian.org/debian-security/ buster/updates main +deb-src http://deb.debian.org/debian-security/ buster/updates main + +deb http://deb.debian.org/debian buster-updates main +deb-src http://deb.debian.org/debian buster-updates main + +deb http://deb.debian.org/debian buster-backports main contrib non-free +deb-src http://deb.debian.org/debian buster-backports main contrib non-free diff --git a/eng/common/cross/arm64/sources.list.focal b/eng/common/cross/arm64/sources.list.focal new file mode 100644 index 0000000000..4de2600c17 --- /dev/null +++ b/eng/common/cross/arm64/sources.list.focal @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ focal main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ focal main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ focal-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ focal-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ focal-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ focal-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ focal-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ focal-security main restricted universe multiverse diff --git a/eng/common/cross/arm64/sources.list.jammy b/eng/common/cross/arm64/sources.list.jammy new file mode 100644 index 0000000000..6bb0453029 --- /dev/null +++ b/eng/common/cross/arm64/sources.list.jammy @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ jammy main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ jammy main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ jammy-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ jammy-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ jammy-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ jammy-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ jammy-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ jammy-security main restricted universe multiverse diff --git a/eng/common/cross/arm64/sources.list.stretch b/eng/common/cross/arm64/sources.list.stretch new file mode 100644 index 0000000000..0e12157743 --- /dev/null +++ b/eng/common/cross/arm64/sources.list.stretch @@ -0,0 +1,12 @@ +deb http://deb.debian.org/debian stretch main +deb-src http://deb.debian.org/debian stretch main + +deb http://deb.debian.org/debian-security/ stretch/updates main +deb-src http://deb.debian.org/debian-security/ stretch/updates main + +deb http://deb.debian.org/debian stretch-updates main +deb-src http://deb.debian.org/debian stretch-updates main + +deb http://deb.debian.org/debian stretch-backports main contrib non-free +deb-src http://deb.debian.org/debian stretch-backports main contrib non-free + diff --git a/eng/common/cross/arm64/sources.list.xenial b/eng/common/cross/arm64/sources.list.xenial new file mode 100644 index 0000000000..56fbb36a59 --- /dev/null +++ b/eng/common/cross/arm64/sources.list.xenial @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ xenial main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ xenial-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ xenial-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ xenial-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-security main restricted universe multiverse diff --git a/eng/common/cross/arm64/sources.list.zesty b/eng/common/cross/arm64/sources.list.zesty new file mode 100644 index 0000000000..ea2c14a787 --- /dev/null +++ b/eng/common/cross/arm64/sources.list.zesty @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ zesty main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ zesty-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ zesty-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ zesty-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty-security main restricted universe multiverse diff --git a/eng/common/cross/arm64/tizen/tizen.patch b/eng/common/cross/arm64/tizen/tizen.patch new file mode 100644 index 0000000000..af7c8be059 --- /dev/null +++ b/eng/common/cross/arm64/tizen/tizen.patch @@ -0,0 +1,9 @@ +diff -u -r a/usr/lib/libc.so b/usr/lib/libc.so +--- a/usr/lib64/libc.so 2016-12-30 23:00:08.284951863 +0900 ++++ b/usr/lib64/libc.so 2016-12-30 23:00:32.140951815 +0900 +@@ -2,4 +2,4 @@ + Use the shared library, but some functions are only in + the static library, so try that secondarily. */ + OUTPUT_FORMAT(elf64-littleaarch64) +-GROUP ( /lib64/libc.so.6 /usr/lib64/libc_nonshared.a AS_NEEDED ( /lib/ld-linux-aarch64.so.1 ) ) ++GROUP ( libc.so.6 libc_nonshared.a AS_NEEDED ( ld-linux-aarch64.so.1 ) ) diff --git a/eng/common/cross/armel/armel.jessie.patch b/eng/common/cross/armel/armel.jessie.patch new file mode 100644 index 0000000000..2d26156193 --- /dev/null +++ b/eng/common/cross/armel/armel.jessie.patch @@ -0,0 +1,43 @@ +diff -u -r a/usr/include/urcu/uatomic/generic.h b/usr/include/urcu/uatomic/generic.h +--- a/usr/include/urcu/uatomic/generic.h 2014-10-22 15:00:58.000000000 -0700 ++++ b/usr/include/urcu/uatomic/generic.h 2020-10-30 21:38:28.550000000 -0700 +@@ -69,10 +69,10 @@ + #endif + #ifdef UATOMIC_HAS_ATOMIC_SHORT + case 2: +- return __sync_val_compare_and_swap_2(addr, old, _new); ++ return __sync_val_compare_and_swap_2((uint16_t*) addr, old, _new); + #endif + case 4: +- return __sync_val_compare_and_swap_4(addr, old, _new); ++ return __sync_val_compare_and_swap_4((uint32_t*) addr, old, _new); + #if (CAA_BITS_PER_LONG == 64) + case 8: + return __sync_val_compare_and_swap_8(addr, old, _new); +@@ -109,7 +109,7 @@ + return; + #endif + case 4: +- __sync_and_and_fetch_4(addr, val); ++ __sync_and_and_fetch_4((uint32_t*) addr, val); + return; + #if (CAA_BITS_PER_LONG == 64) + case 8: +@@ -148,7 +148,7 @@ + return; + #endif + case 4: +- __sync_or_and_fetch_4(addr, val); ++ __sync_or_and_fetch_4((uint32_t*) addr, val); + return; + #if (CAA_BITS_PER_LONG == 64) + case 8: +@@ -187,7 +187,7 @@ + return __sync_add_and_fetch_2(addr, val); + #endif + case 4: +- return __sync_add_and_fetch_4(addr, val); ++ return __sync_add_and_fetch_4((uint32_t*) addr, val); + #if (CAA_BITS_PER_LONG == 64) + case 8: + return __sync_add_and_fetch_8(addr, val); diff --git a/eng/common/cross/armel/sources.list.jessie b/eng/common/cross/armel/sources.list.jessie new file mode 100644 index 0000000000..3d9c3059d8 --- /dev/null +++ b/eng/common/cross/armel/sources.list.jessie @@ -0,0 +1,3 @@ +# Debian (jessie) # Stable +deb http://ftp.debian.org/debian/ jessie main contrib non-free +deb-src http://ftp.debian.org/debian/ jessie main contrib non-free diff --git a/eng/common/cross/armel/tizen/tizen.patch b/eng/common/cross/armel/tizen/tizen.patch new file mode 100644 index 0000000000..ca7c7c1ff7 --- /dev/null +++ b/eng/common/cross/armel/tizen/tizen.patch @@ -0,0 +1,9 @@ +diff -u -r a/usr/lib/libc.so b/usr/lib/libc.so +--- a/usr/lib/libc.so 2016-12-30 23:00:08.284951863 +0900 ++++ b/usr/lib/libc.so 2016-12-30 23:00:32.140951815 +0900 +@@ -2,4 +2,4 @@ + Use the shared library, but some functions are only in + the static library, so try that secondarily. */ + OUTPUT_FORMAT(elf32-littlearm) +-GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a AS_NEEDED ( /lib/ld-linux.so.3 ) ) ++GROUP ( libc.so.6 libc_nonshared.a AS_NEEDED ( ld-linux.so.3 ) ) diff --git a/eng/common/cross/armv6/sources.list.buster b/eng/common/cross/armv6/sources.list.buster new file mode 100644 index 0000000000..f27fc4fb34 --- /dev/null +++ b/eng/common/cross/armv6/sources.list.buster @@ -0,0 +1,2 @@ +deb http://raspbian.raspberrypi.org/raspbian/ buster main contrib non-free rpi +deb-src http://raspbian.raspberrypi.org/raspbian/ buster main contrib non-free rpi diff --git a/eng/common/cross/build-android-rootfs.sh b/eng/common/cross/build-android-rootfs.sh new file mode 100755 index 0000000000..f163fb9dae --- /dev/null +++ b/eng/common/cross/build-android-rootfs.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +set -e +__NDK_Version=r21 + +usage() +{ + echo "Creates a toolchain and sysroot used for cross-compiling for Android." + echo. + echo "Usage: $0 [BuildArch] [ApiLevel]" + echo. + echo "BuildArch is the target architecture of Android. Currently only arm64 is supported." + echo "ApiLevel is the target Android API level. API levels usually match to Android releases. See https://source.android.com/source/build-numbers.html" + echo. + echo "By default, the toolchain and sysroot will be generated in cross/android-rootfs/toolchain/[BuildArch]. You can change this behavior" + echo "by setting the TOOLCHAIN_DIR environment variable" + echo. + echo "By default, the NDK will be downloaded into the cross/android-rootfs/android-ndk-$__NDK_Version directory. If you already have an NDK installation," + echo "you can set the NDK_DIR environment variable to have this script use that installation of the NDK." + echo "By default, this script will generate a file, android_platform, in the root of the ROOTFS_DIR directory that contains the RID for the supported and tested Android build: android.28-arm64. This file is to replace '/etc/os-release', which is not available for Android." + exit 1 +} + +__ApiLevel=28 # The minimum platform for arm64 is API level 21 but the minimum version that support glob(3) is 28. See $ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/glob.h +__BuildArch=arm64 +__AndroidArch=aarch64 +__AndroidToolchain=aarch64-linux-android + +for i in "$@" + do + lowerI="$(echo $i | tr "[:upper:]" "[:lower:]")" + case $lowerI in + -?|-h|--help) + usage + exit 1 + ;; + arm64) + __BuildArch=arm64 + __AndroidArch=aarch64 + __AndroidToolchain=aarch64-linux-android + ;; + arm) + __BuildArch=arm + __AndroidArch=arm + __AndroidToolchain=arm-linux-androideabi + ;; + *[0-9]) + __ApiLevel=$i + ;; + *) + __UnprocessedBuildArgs="$__UnprocessedBuildArgs $i" + ;; + esac +done + +# Obtain the location of the bash script to figure out where the root of the repo is. +__ScriptBaseDir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +__CrossDir="$__ScriptBaseDir/../../../.tools/android-rootfs" + +if [[ ! -f "$__CrossDir" ]]; then + mkdir -p "$__CrossDir" +fi + +# Resolve absolute path to avoid `../` in build logs +__CrossDir="$( cd "$__CrossDir" && pwd )" + +__NDK_Dir="$__CrossDir/android-ndk-$__NDK_Version" +__lldb_Dir="$__CrossDir/lldb" +__ToolchainDir="$__CrossDir/android-ndk-$__NDK_Version" + +if [[ -n "$TOOLCHAIN_DIR" ]]; then + __ToolchainDir=$TOOLCHAIN_DIR +fi + +if [[ -n "$NDK_DIR" ]]; then + __NDK_Dir=$NDK_DIR +fi + +echo "Target API level: $__ApiLevel" +echo "Target architecture: $__BuildArch" +echo "NDK location: $__NDK_Dir" +echo "Target Toolchain location: $__ToolchainDir" + +# Download the NDK if required +if [ ! -d $__NDK_Dir ]; then + echo Downloading the NDK into $__NDK_Dir + mkdir -p $__NDK_Dir + wget -q --progress=bar:force:noscroll --show-progress https://dl.google.com/android/repository/android-ndk-$__NDK_Version-linux-x86_64.zip -O $__CrossDir/android-ndk-$__NDK_Version-linux-x86_64.zip + unzip -q $__CrossDir/android-ndk-$__NDK_Version-linux-x86_64.zip -d $__CrossDir +fi + +if [ ! -d $__lldb_Dir ]; then + mkdir -p $__lldb_Dir + echo Downloading LLDB into $__lldb_Dir + wget -q --progress=bar:force:noscroll --show-progress https://dl.google.com/android/repository/lldb-2.3.3614996-linux-x86_64.zip -O $__CrossDir/lldb-2.3.3614996-linux-x86_64.zip + unzip -q $__CrossDir/lldb-2.3.3614996-linux-x86_64.zip -d $__lldb_Dir +fi + +echo "Download dependencies..." +__TmpDir=$__CrossDir/tmp/$__BuildArch/ +mkdir -p "$__TmpDir" + +# combined dependencies for coreclr, installer and libraries +__AndroidPackages="libicu" +__AndroidPackages+=" libandroid-glob" +__AndroidPackages+=" liblzma" +__AndroidPackages+=" krb5" +__AndroidPackages+=" openssl" + +for path in $(wget -qO- https://packages.termux.dev/termux-main-21/dists/stable/main/binary-$__AndroidArch/Packages |\ + grep -A15 "Package: \(${__AndroidPackages// /\\|}\)" | grep -v "static\|tool" | grep Filename); do + + if [[ "$path" != "Filename:" ]]; then + echo "Working on: $path" + wget -qO- https://packages.termux.dev/termux-main-21/$path | dpkg -x - "$__TmpDir" + fi +done + +cp -R "$__TmpDir/data/data/com.termux/files/usr/"* "$__ToolchainDir/sysroot/usr/" + +# Generate platform file for build.sh script to assign to __DistroRid +echo "Generating platform file..." +echo "RID=android.${__ApiLevel}-${__BuildArch}" > $__ToolchainDir/sysroot/android_platform + +echo "Now to build coreclr, libraries and installers; run:" +echo ROOTFS_DIR=\$\(realpath $__ToolchainDir/sysroot\) ./build.sh --cross --arch $__BuildArch \ + --subsetCategory coreclr +echo ROOTFS_DIR=\$\(realpath $__ToolchainDir/sysroot\) ./build.sh --cross --arch $__BuildArch \ + --subsetCategory libraries +echo ROOTFS_DIR=\$\(realpath $__ToolchainDir/sysroot\) ./build.sh --cross --arch $__BuildArch \ + --subsetCategory installer diff --git a/eng/common/cross/build-rootfs.sh b/eng/common/cross/build-rootfs.sh new file mode 100755 index 0000000000..9caf9b021d --- /dev/null +++ b/eng/common/cross/build-rootfs.sh @@ -0,0 +1,648 @@ +#!/usr/bin/env bash + +set -e + +usage() +{ + echo "Usage: $0 [BuildArch] [CodeName] [lldbx.y] [llvmx[.y]] [--skipunmount] --rootfsdir ]" + echo "BuildArch can be: arm(default), arm64, armel, armv6, ppc64le, riscv64, s390x, x64, x86" + echo "CodeName - optional, Code name for Linux, can be: xenial(default), zesty, bionic, alpine" + echo " for alpine can be specified with version: alpineX.YY or alpineedge" + echo " for FreeBSD can be: freebsd12, freebsd13" + echo " for illumos can be: illumos" + echo " for Haiku can be: haiku." + echo "lldbx.y - optional, LLDB version, can be: lldb3.9(default), lldb4.0, lldb5.0, lldb6.0 no-lldb. Ignored for alpine and FreeBSD" + echo "llvmx[.y] - optional, LLVM version for LLVM related packages." + echo "--skipunmount - optional, will skip the unmount of rootfs folder." + echo "--skipsigcheck - optional, will skip package signature checks (allowing untrusted packages)." + echo "--use-mirror - optional, use mirror URL to fetch resources, when available." + echo "--jobs N - optional, restrict to N jobs." + exit 1 +} + +__CodeName=xenial +__CrossDir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +__BuildArch=arm +__AlpineArch=armv7 +__FreeBSDArch=arm +__FreeBSDMachineArch=armv7 +__IllumosArch=arm7 +__HaikuArch=arm +__QEMUArch=arm +__UbuntuArch=armhf +__UbuntuRepo="http://ports.ubuntu.com/" +__LLDB_Package="liblldb-3.9-dev" +__SkipUnmount=0 + +# base development support +__UbuntuPackages="build-essential" + +__AlpinePackages="alpine-base" +__AlpinePackages+=" build-base" +__AlpinePackages+=" linux-headers" +__AlpinePackages+=" lldb-dev" +__AlpinePackages+=" python3" +__AlpinePackages+=" libedit" + +# symlinks fixer +__UbuntuPackages+=" symlinks" + +# runtime dependencies +__UbuntuPackages+=" libicu-dev" +__UbuntuPackages+=" liblttng-ust-dev" +__UbuntuPackages+=" libunwind8-dev" +__UbuntuPackages+=" libnuma-dev" + +__AlpinePackages+=" gettext-dev" +__AlpinePackages+=" icu-dev" +__AlpinePackages+=" libunwind-dev" +__AlpinePackages+=" lttng-ust-dev" +__AlpinePackages+=" compiler-rt" +__AlpinePackages+=" numactl-dev" + +# runtime libraries' dependencies +__UbuntuPackages+=" libcurl4-openssl-dev" +__UbuntuPackages+=" libkrb5-dev" +__UbuntuPackages+=" libssl-dev" +__UbuntuPackages+=" zlib1g-dev" + +__AlpinePackages+=" curl-dev" +__AlpinePackages+=" krb5-dev" +__AlpinePackages+=" openssl-dev" +__AlpinePackages+=" zlib-dev" + +__FreeBSDBase="12.4-RELEASE" +__FreeBSDPkg="1.17.0" +__FreeBSDABI="12" +__FreeBSDPackages="libunwind" +__FreeBSDPackages+=" icu" +__FreeBSDPackages+=" libinotify" +__FreeBSDPackages+=" openssl" +__FreeBSDPackages+=" krb5" +__FreeBSDPackages+=" terminfo-db" + +__IllumosPackages="icu" +__IllumosPackages+=" mit-krb5" +__IllumosPackages+=" openssl" +__IllumosPackages+=" zlib" + +__HaikuPackages="gcc_syslibs" +__HaikuPackages+=" gcc_syslibs_devel" +__HaikuPackages+=" gmp" +__HaikuPackages+=" gmp_devel" +__HaikuPackages+=" icu66" +__HaikuPackages+=" icu66_devel" +__HaikuPackages+=" krb5" +__HaikuPackages+=" krb5_devel" +__HaikuPackages+=" libiconv" +__HaikuPackages+=" libiconv_devel" +__HaikuPackages+=" llvm12_libunwind" +__HaikuPackages+=" llvm12_libunwind_devel" +__HaikuPackages+=" mpfr" +__HaikuPackages+=" mpfr_devel" +__HaikuPackages+=" openssl" +__HaikuPackages+=" openssl_devel" +__HaikuPackages+=" zlib" +__HaikuPackages+=" zlib_devel" + +# ML.NET dependencies +__UbuntuPackages+=" libomp5" +__UbuntuPackages+=" libomp-dev" + +# Taken from https://github.com/alpinelinux/alpine-chroot-install/blob/6d08f12a8a70dd9b9dc7d997c88aa7789cc03c42/alpine-chroot-install#L85-L133 +__AlpineKeys=' +4a6a0840:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1yHJxQgsHQREclQu4Ohe\nqxTxd1tHcNnvnQTu/UrTky8wWvgXT+jpveroeWWnzmsYlDI93eLI2ORakxb3gA2O\nQ0Ry4ws8vhaxLQGC74uQR5+/yYrLuTKydFzuPaS1dK19qJPXB8GMdmFOijnXX4SA\njixuHLe1WW7kZVtjL7nufvpXkWBGjsfrvskdNA/5MfxAeBbqPgaq0QMEfxMAn6/R\nL5kNepi/Vr4S39Xvf2DzWkTLEK8pcnjNkt9/aafhWqFVW7m3HCAII6h/qlQNQKSo\nGuH34Q8GsFG30izUENV9avY7hSLq7nggsvknlNBZtFUcmGoQrtx3FmyYsIC8/R+B\nywIDAQAB +5243ef4b:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvNijDxJ8kloskKQpJdx+\nmTMVFFUGDoDCbulnhZMJoKNkSuZOzBoFC94omYPtxnIcBdWBGnrm6ncbKRlR+6oy\nDO0W7c44uHKCFGFqBhDasdI4RCYP+fcIX/lyMh6MLbOxqS22TwSLhCVjTyJeeH7K\naA7vqk+QSsF4TGbYzQDDpg7+6aAcNzg6InNePaywA6hbT0JXbxnDWsB+2/LLSF2G\nmnhJlJrWB1WGjkz23ONIWk85W4S0XB/ewDefd4Ly/zyIciastA7Zqnh7p3Ody6Q0\nsS2MJzo7p3os1smGjUF158s6m/JbVh4DN6YIsxwl2OjDOz9R0OycfJSDaBVIGZzg\ncQIDAQAB +524d27bb:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr8s1q88XpuJWLCZALdKj\nlN8wg2ePB2T9aIcaxryYE/Jkmtu+ZQ5zKq6BT3y/udt5jAsMrhHTwroOjIsF9DeG\ne8Y3vjz+Hh4L8a7hZDaw8jy3CPag47L7nsZFwQOIo2Cl1SnzUc6/owoyjRU7ab0p\niWG5HK8IfiybRbZxnEbNAfT4R53hyI6z5FhyXGS2Ld8zCoU/R4E1P0CUuXKEN4p0\n64dyeUoOLXEWHjgKiU1mElIQj3k/IF02W89gDj285YgwqA49deLUM7QOd53QLnx+\nxrIrPv3A+eyXMFgexNwCKQU9ZdmWa00MjjHlegSGK8Y2NPnRoXhzqSP9T9i2HiXL\nVQIDAQAB +5261cecb:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwlzMkl7b5PBdfMzGdCT0\ncGloRr5xGgVmsdq5EtJvFkFAiN8Ac9MCFy/vAFmS8/7ZaGOXoCDWbYVLTLOO2qtX\nyHRl+7fJVh2N6qrDDFPmdgCi8NaE+3rITWXGrrQ1spJ0B6HIzTDNEjRKnD4xyg4j\ng01FMcJTU6E+V2JBY45CKN9dWr1JDM/nei/Pf0byBJlMp/mSSfjodykmz4Oe13xB\nCa1WTwgFykKYthoLGYrmo+LKIGpMoeEbY1kuUe04UiDe47l6Oggwnl+8XD1MeRWY\nsWgj8sF4dTcSfCMavK4zHRFFQbGp/YFJ/Ww6U9lA3Vq0wyEI6MCMQnoSMFwrbgZw\nwwIDAQAB +58199dcc:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3v8/ye/V/t5xf4JiXLXa\nhWFRozsnmn3hobON20GdmkrzKzO/eUqPOKTpg2GtvBhK30fu5oY5uN2ORiv2Y2ht\neLiZ9HVz3XP8Fm9frha60B7KNu66FO5P2o3i+E+DWTPqqPcCG6t4Znk2BypILcit\nwiPKTsgbBQR2qo/cO01eLLdt6oOzAaF94NH0656kvRewdo6HG4urbO46tCAizvCR\nCA7KGFMyad8WdKkTjxh8YLDLoOCtoZmXmQAiwfRe9pKXRH/XXGop8SYptLqyVVQ+\ntegOD9wRs2tOlgcLx4F/uMzHN7uoho6okBPiifRX+Pf38Vx+ozXh056tjmdZkCaV\naQIDAQAB +58cbb476:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoSPnuAGKtRIS5fEgYPXD\n8pSGvKAmIv3A08LBViDUe+YwhilSHbYXUEAcSH1KZvOo1WT1x2FNEPBEFEFU1Eyc\n+qGzbA03UFgBNvArurHQ5Z/GngGqE7IarSQFSoqewYRtFSfp+TL9CUNBvM0rT7vz\n2eMu3/wWG+CBmb92lkmyWwC1WSWFKO3x8w+Br2IFWvAZqHRt8oiG5QtYvcZL6jym\nY8T6sgdDlj+Y+wWaLHs9Fc+7vBuyK9C4O1ORdMPW15qVSl4Lc2Wu1QVwRiKnmA+c\nDsH/m7kDNRHM7TjWnuj+nrBOKAHzYquiu5iB3Qmx+0gwnrSVf27Arc3ozUmmJbLj\nzQIDAQAB +58e4f17d:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvBxJN9ErBgdRcPr5g4hV\nqyUSGZEKuvQliq2Z9SRHLh2J43+EdB6A+yzVvLnzcHVpBJ+BZ9RV30EM9guck9sh\nr+bryZcRHyjG2wiIEoduxF2a8KeWeQH7QlpwGhuobo1+gA8L0AGImiA6UP3LOirl\nI0G2+iaKZowME8/tydww4jx5vG132JCOScMjTalRsYZYJcjFbebQQolpqRaGB4iG\nWqhytWQGWuKiB1A22wjmIYf3t96l1Mp+FmM2URPxD1gk/BIBnX7ew+2gWppXOK9j\n1BJpo0/HaX5XoZ/uMqISAAtgHZAqq+g3IUPouxTphgYQRTRYpz2COw3NF43VYQrR\nbQIDAQAB +60ac2099:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwR4uJVtJOnOFGchnMW5Y\nj5/waBdG1u5BTMlH+iQMcV5+VgWhmpZHJCBz3ocD+0IGk2I68S5TDOHec/GSC0lv\n6R9o6F7h429GmgPgVKQsc8mPTPtbjJMuLLs4xKc+viCplXc0Nc0ZoHmCH4da6fCV\ntdpHQjVe6F9zjdquZ4RjV6R6JTiN9v924dGMAkbW/xXmamtz51FzondKC52Gh8Mo\n/oA0/T0KsCMCi7tb4QNQUYrf+Xcha9uus4ww1kWNZyfXJB87a2kORLiWMfs2IBBJ\nTmZ2Fnk0JnHDb8Oknxd9PvJPT0mvyT8DA+KIAPqNvOjUXP4bnjEHJcoCP9S5HkGC\nIQIDAQAB +6165ee59:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAutQkua2CAig4VFSJ7v54\nALyu/J1WB3oni7qwCZD3veURw7HxpNAj9hR+S5N/pNeZgubQvJWyaPuQDm7PTs1+\ntFGiYNfAsiibX6Rv0wci3M+z2XEVAeR9Vzg6v4qoofDyoTbovn2LztaNEjTkB+oK\ntlvpNhg1zhou0jDVYFniEXvzjckxswHVb8cT0OMTKHALyLPrPOJzVtM9C1ew2Nnc\n3848xLiApMu3NBk0JqfcS3Bo5Y2b1FRVBvdt+2gFoKZix1MnZdAEZ8xQzL/a0YS5\nHd0wj5+EEKHfOd3A75uPa/WQmA+o0cBFfrzm69QDcSJSwGpzWrD1ScH3AK8nWvoj\nv7e9gukK/9yl1b4fQQ00vttwJPSgm9EnfPHLAtgXkRloI27H6/PuLoNvSAMQwuCD\nhQRlyGLPBETKkHeodfLoULjhDi1K2gKJTMhtbnUcAA7nEphkMhPWkBpgFdrH+5z4\nLxy+3ek0cqcI7K68EtrffU8jtUj9LFTUC8dERaIBs7NgQ/LfDbDfGh9g6qVj1hZl\nk9aaIPTm/xsi8v3u+0qaq7KzIBc9s59JOoA8TlpOaYdVgSQhHHLBaahOuAigH+VI\nisbC9vmqsThF2QdDtQt37keuqoda2E6sL7PUvIyVXDRfwX7uMDjlzTxHTymvq2Ck\nhtBqojBnThmjJQFgZXocHG8CAwEAAQ== +61666e3f:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAlEyxkHggKCXC2Wf5Mzx4\nnZLFZvU2bgcA3exfNPO/g1YunKfQY+Jg4fr6tJUUTZ3XZUrhmLNWvpvSwDS19ZmC\nIXOu0+V94aNgnhMsk9rr59I8qcbsQGIBoHzuAl8NzZCgdbEXkiY90w1skUw8J57z\nqCsMBydAueMXuWqF5nGtYbi5vHwK42PffpiZ7G5Kjwn8nYMW5IZdL6ZnMEVJUWC9\nI4waeKg0yskczYDmZUEAtrn3laX9677ToCpiKrvmZYjlGl0BaGp3cxggP2xaDbUq\nqfFxWNgvUAb3pXD09JM6Mt6HSIJaFc9vQbrKB9KT515y763j5CC2KUsilszKi3mB\nHYe5PoebdjS7D1Oh+tRqfegU2IImzSwW3iwA7PJvefFuc/kNIijfS/gH/cAqAK6z\nbhdOtE/zc7TtqW2Wn5Y03jIZdtm12CxSxwgtCF1NPyEWyIxAQUX9ACb3M0FAZ61n\nfpPrvwTaIIxxZ01L3IzPLpbc44x/DhJIEU+iDt6IMTrHOphD9MCG4631eIdB0H1b\n6zbNX1CXTsafqHRFV9XmYYIeOMggmd90s3xIbEujA6HKNP/gwzO6CDJ+nHFDEqoF\nSkxRdTkEqjTjVKieURW7Swv7zpfu5PrsrrkyGnsRrBJJzXlm2FOOxnbI2iSL1B5F\nrO5kbUxFeZUIDq+7Yv4kLWcCAwEAAQ== +616a9724:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnC+bR4bHf/L6QdU4puhQ\ngl1MHePszRC38bzvVFDUJsmCaMCL2suCs2A2yxAgGb9pu9AJYLAmxQC4mM3jNqhg\n/E7yuaBbek3O02zN/ctvflJ250wZCy+z0ZGIp1ak6pu1j14IwHokl9j36zNfGtfv\nADVOcdpWITFFlPqwq1qt/H3UsKVmtiF3BNWWTeUEQwKvlU8ymxgS99yn0+4OPyNT\nL3EUeS+NQJtDS01unau0t7LnjUXn+XIneWny8bIYOQCuVR6s/gpIGuhBaUqwaJOw\n7jkJZYF2Ij7uPb4b5/R3vX2FfxxqEHqssFSg8FFUNTZz3qNZs0CRVyfA972g9WkJ\nhPfn31pQYil4QGRibCMIeU27YAEjXoqfJKEPh4UWMQsQLrEfdGfb8VgwrPbniGfU\nL3jKJR3VAafL9330iawzVQDlIlwGl6u77gEXMl9K0pfazunYhAp+BMP+9ot5ckK+\nosmrqj11qMESsAj083GeFdfV3pXEIwUytaB0AKEht9DbqUfiE/oeZ/LAXgySMtVC\nsbC4ESmgVeY2xSBIJdDyUap7FR49GGrw0W49NUv9gRgQtGGaNVQQO9oGL2PBC41P\niWF9GLoX30HIz1P8PF/cZvicSSPkQf2Z6TV+t0ebdGNS5DjapdnCrq8m9Z0pyKsQ\nuxAL2a7zX8l5i1CZh1ycUGsCAwEAAQ== +616abc23:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0MfCDrhODRCIxR9Dep1s\neXafh5CE5BrF4WbCgCsevyPIdvTeyIaW4vmO3bbG4VzhogDZju+R3IQYFuhoXP5v\nY+zYJGnwrgz3r5wYAvPnLEs1+dtDKYOgJXQj+wLJBW1mzRDL8FoRXOe5iRmn1EFS\nwZ1DoUvyu7/J5r0itKicZp3QKED6YoilXed+1vnS4Sk0mzN4smuMR9eO1mMCqNp9\n9KTfRDHTbakIHwasECCXCp50uXdoW6ig/xUAFanpm9LtK6jctNDbXDhQmgvAaLXZ\nLvFqoaYJ/CvWkyYCgL6qxvMvVmPoRv7OPcyni4xR/WgWa0MSaEWjgPx3+yj9fiMA\n1S02pFWFDOr5OUF/O4YhFJvUCOtVsUPPfA/Lj6faL0h5QI9mQhy5Zb9TTaS9jB6p\nLw7u0dJlrjFedk8KTJdFCcaGYHP6kNPnOxMylcB/5WcztXZVQD5WpCicGNBxCGMm\nW64SgrV7M07gQfL/32QLsdqPUf0i8hoVD8wfQ3EpbQzv6Fk1Cn90bZqZafg8XWGY\nwddhkXk7egrr23Djv37V2okjzdqoyLBYBxMz63qQzFoAVv5VoY2NDTbXYUYytOvG\nGJ1afYDRVWrExCech1mX5ZVUB1br6WM+psFLJFoBFl6mDmiYt0vMYBddKISsvwLl\nIJQkzDwtXzT2cSjoj3T5QekCAwEAAQ== +616ac3bc:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvaaoSLab+IluixwKV5Od\n0gib2YurjPatGIbn5Ov2DLUFYiebj2oJINXJSwUOO+4WcuHFEqiL/1rya+k5hLZt\nhnPL1tn6QD4rESznvGSasRCQNT2vS/oyZbTYJRyAtFkEYLlq0t3S3xBxxHWuvIf0\nqVxVNYpQWyM3N9RIeYBR/euXKJXileSHk/uq1I5wTC0XBIHWcthczGN0m9wBEiWS\n0m3cnPk4q0Ea8mUJ91Rqob19qETz6VbSPYYpZk3qOycjKosuwcuzoMpwU8KRiMFd\n5LHtX0Hx85ghGsWDVtS0c0+aJa4lOMGvJCAOvDfqvODv7gKlCXUpgumGpLdTmaZ8\n1RwqspAe3IqBcdKTqRD4m2mSg23nVx2FAY3cjFvZQtfooT7q1ItRV5RgH6FhQSl7\n+6YIMJ1Bf8AAlLdRLpg+doOUGcEn+pkDiHFgI8ylH1LKyFKw+eXaAml/7DaWZk1d\ndqggwhXOhc/UUZFQuQQ8A8zpA13PcbC05XxN2hyP93tCEtyynMLVPtrRwDnHxFKa\nqKzs3rMDXPSXRn3ZZTdKH3069ApkEjQdpcwUh+EmJ1Ve/5cdtzT6kKWCjKBFZP/s\n91MlRrX2BTRdHaU5QJkUheUtakwxuHrdah2F94lRmsnQlpPr2YseJu6sIE+Dnx4M\nCfhdVbQL2w54R645nlnohu8CAwEAAQ== +616adfeb:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq0BFD1D4lIxQcsqEpQzU\npNCYM3aP1V/fxxVdT4DWvSI53JHTwHQamKdMWtEXetWVbP5zSROniYKFXd/xrD9X\n0jiGHey3lEtylXRIPxe5s+wXoCmNLcJVnvTcDtwx/ne2NLHxp76lyc25At+6RgE6\nADjLVuoD7M4IFDkAsd8UQ8zM0Dww9SylIk/wgV3ZkifecvgUQRagrNUdUjR56EBZ\nraQrev4hhzOgwelT0kXCu3snbUuNY/lU53CoTzfBJ5UfEJ5pMw1ij6X0r5S9IVsy\nKLWH1hiO0NzU2c8ViUYCly4Fe9xMTFc6u2dy/dxf6FwERfGzETQxqZvSfrRX+GLj\n/QZAXiPg5178hT/m0Y3z5IGenIC/80Z9NCi+byF1WuJlzKjDcF/TU72zk0+PNM/H\nKuppf3JT4DyjiVzNC5YoWJT2QRMS9KLP5iKCSThwVceEEg5HfhQBRT9M6KIcFLSs\nmFjx9kNEEmc1E8hl5IR3+3Ry8G5/bTIIruz14jgeY9u5jhL8Vyyvo41jgt9sLHR1\n/J1TxKfkgksYev7PoX6/ZzJ1ksWKZY5NFoDXTNYUgzFUTOoEaOg3BAQKadb3Qbbq\nXIrxmPBdgrn9QI7NCgfnAY3Tb4EEjs3ON/BNyEhUENcXOH6I1NbcuBQ7g9P73kE4\nVORdoc8MdJ5eoKBpO8Ww8HECAwEAAQ== +616ae350:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyduVzi1mWm+lYo2Tqt/0\nXkCIWrDNP1QBMVPrE0/ZlU2bCGSoo2Z9FHQKz/mTyMRlhNqTfhJ5qU3U9XlyGOPJ\npiM+b91g26pnpXJ2Q2kOypSgOMOPA4cQ42PkHBEqhuzssfj9t7x47ppS94bboh46\nxLSDRff/NAbtwTpvhStV3URYkxFG++cKGGa5MPXBrxIp+iZf9GnuxVdST5PGiVGP\nODL/b69sPJQNbJHVquqUTOh5Ry8uuD2WZuXfKf7/C0jC/ie9m2+0CttNu9tMciGM\nEyKG1/Xhk5iIWO43m4SrrT2WkFlcZ1z2JSf9Pjm4C2+HovYpihwwdM/OdP8Xmsnr\nDzVB4YvQiW+IHBjStHVuyiZWc+JsgEPJzisNY0Wyc/kNyNtqVKpX6dRhMLanLmy+\nf53cCSI05KPQAcGj6tdL+D60uKDkt+FsDa0BTAobZ31OsFVid0vCXtsbplNhW1IF\nHwsGXBTVcfXg44RLyL8Lk/2dQxDHNHzAUslJXzPxaHBLmt++2COa2EI1iWlvtznk\nOk9WP8SOAIj+xdqoiHcC4j72BOVVgiITIJNHrbppZCq6qPR+fgXmXa+sDcGh30m6\n9Wpbr28kLMSHiENCWTdsFij+NQTd5S47H7XTROHnalYDuF1RpS+DpQidT5tUimaT\nJZDr++FjKrnnijbyNF8b98UCAwEAAQ== +616db30d:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnpUpyWDWjlUk3smlWeA0\nlIMW+oJ38t92CRLHH3IqRhyECBRW0d0aRGtq7TY8PmxjjvBZrxTNDpJT6KUk4LRm\na6A6IuAI7QnNK8SJqM0DLzlpygd7GJf8ZL9SoHSH+gFsYF67Cpooz/YDqWrlN7Vw\ntO00s0B+eXy+PCXYU7VSfuWFGK8TGEv6HfGMALLjhqMManyvfp8hz3ubN1rK3c8C\nUS/ilRh1qckdbtPvoDPhSbTDmfU1g/EfRSIEXBrIMLg9ka/XB9PvWRrekrppnQzP\nhP9YE3x/wbFc5QqQWiRCYyQl/rgIMOXvIxhkfe8H5n1Et4VAorkpEAXdsfN8KSVv\nLSMazVlLp9GYq5SUpqYX3KnxdWBgN7BJoZ4sltsTpHQ/34SXWfu3UmyUveWj7wp0\nx9hwsPirVI00EEea9AbP7NM2rAyu6ukcm4m6ATd2DZJIViq2es6m60AE6SMCmrQF\nwmk4H/kdQgeAELVfGOm2VyJ3z69fQuywz7xu27S6zTKi05Qlnohxol4wVb6OB7qG\nLPRtK9ObgzRo/OPumyXqlzAi/Yvyd1ZQk8labZps3e16bQp8+pVPiumWioMFJDWV\nGZjCmyMSU8V6MB6njbgLHoyg2LCukCAeSjbPGGGYhnKLm1AKSoJh3IpZuqcKCk5C\n8CM1S15HxV78s9dFntEqIokCAwEAAQ== +' +__Keyring= +__SkipSigCheck=0 +__UseMirror=0 + +__UnprocessedBuildArgs= +while :; do + if [[ "$#" -le 0 ]]; then + break + fi + + lowerI="$(echo "$1" | tr "[:upper:]" "[:lower:]")" + case $lowerI in + -\?|-h|--help) + usage + exit 1 + ;; + arm) + __BuildArch=arm + __UbuntuArch=armhf + __AlpineArch=armv7 + __QEMUArch=arm + ;; + arm64) + __BuildArch=arm64 + __UbuntuArch=arm64 + __AlpineArch=aarch64 + __QEMUArch=aarch64 + __FreeBSDArch=arm64 + __FreeBSDMachineArch=aarch64 + ;; + armel) + __BuildArch=armel + __UbuntuArch=armel + __UbuntuRepo="http://ftp.debian.org/debian/" + __CodeName=jessie + ;; + armv6) + __BuildArch=armv6 + __UbuntuArch=armhf + __QEMUArch=arm + __UbuntuRepo="http://raspbian.raspberrypi.org/raspbian/" + __CodeName=buster + __LLDB_Package="liblldb-6.0-dev" + + if [[ -e "/usr/share/keyrings/raspbian-archive-keyring.gpg" ]]; then + __Keyring="--keyring /usr/share/keyrings/raspbian-archive-keyring.gpg" + fi + ;; + riscv64) + __BuildArch=riscv64 + __AlpineArch=riscv64 + __AlpinePackages="${__AlpinePackages// lldb-dev/}" + __QEMUArch=riscv64 + __UbuntuArch=riscv64 + __UbuntuRepo="http://deb.debian.org/debian-ports" + __UbuntuPackages="${__UbuntuPackages// libunwind8-dev/}" + unset __LLDB_Package + + if [[ -e "/usr/share/keyrings/debian-ports-archive-keyring.gpg" ]]; then + __Keyring="--keyring /usr/share/keyrings/debian-ports-archive-keyring.gpg --include=debian-ports-archive-keyring" + fi + ;; + ppc64le) + __BuildArch=ppc64le + __AlpineArch=ppc64le + __QEMUArch=ppc64le + __UbuntuArch=ppc64el + __UbuntuRepo="http://ports.ubuntu.com/ubuntu-ports/" + __UbuntuPackages="${__UbuntuPackages// libunwind8-dev/}" + __UbuntuPackages="${__UbuntuPackages// libomp-dev/}" + __UbuntuPackages="${__UbuntuPackages// libomp5/}" + unset __LLDB_Package + ;; + s390x) + __BuildArch=s390x + __AlpineArch=s390x + __QEMUArch=s390x + __UbuntuArch=s390x + __UbuntuRepo="http://ports.ubuntu.com/ubuntu-ports/" + __UbuntuPackages="${__UbuntuPackages// libunwind8-dev/}" + __UbuntuPackages="${__UbuntuPackages// libomp-dev/}" + __UbuntuPackages="${__UbuntuPackages// libomp5/}" + unset __LLDB_Package + ;; + x64) + __BuildArch=x64 + __AlpineArch=x86_64 + __UbuntuArch=amd64 + __FreeBSDArch=amd64 + __FreeBSDMachineArch=amd64 + __illumosArch=x86_64 + __HaikuArch=x86_64 + __UbuntuRepo="http://archive.ubuntu.com/ubuntu/" + ;; + x86) + __BuildArch=x86 + __UbuntuArch=i386 + __AlpineArch=x86 + __UbuntuRepo="http://archive.ubuntu.com/ubuntu/" + ;; + lldb*) + version="${lowerI/lldb/}" + parts=(${version//./ }) + + # for versions > 6.0, lldb has dropped the minor version + if [[ "${parts[0]}" -gt 6 ]]; then + version="${parts[0]}" + fi + + __LLDB_Package="liblldb-${version}-dev" + ;; + no-lldb) + unset __LLDB_Package + ;; + llvm*) + version="${lowerI/llvm/}" + parts=(${version//./ }) + __LLVM_MajorVersion="${parts[0]}" + __LLVM_MinorVersion="${parts[1]}" + + # for versions > 6.0, llvm has dropped the minor version + if [[ -z "$__LLVM_MinorVersion" && "$__LLVM_MajorVersion" -le 6 ]]; then + __LLVM_MinorVersion=0; + fi + ;; + xenial) # Ubuntu 16.04 + if [[ "$__CodeName" != "jessie" ]]; then + __CodeName=xenial + fi + ;; + zesty) # Ubuntu 17.04 + if [[ "$__CodeName" != "jessie" ]]; then + __CodeName=zesty + fi + ;; + bionic) # Ubuntu 18.04 + if [[ "$__CodeName" != "jessie" ]]; then + __CodeName=bionic + fi + ;; + focal) # Ubuntu 20.04 + if [[ "$__CodeName" != "jessie" ]]; then + __CodeName=focal + fi + ;; + jammy) # Ubuntu 22.04 + if [[ "$__CodeName" != "jessie" ]]; then + __CodeName=jammy + fi + ;; + jessie) # Debian 8 + __CodeName=jessie + + if [[ -z "$__UbuntuRepo" ]]; then + __UbuntuRepo="http://ftp.debian.org/debian/" + fi + ;; + stretch) # Debian 9 + __CodeName=stretch + __LLDB_Package="liblldb-6.0-dev" + + if [[ -z "$__UbuntuRepo" ]]; then + __UbuntuRepo="http://ftp.debian.org/debian/" + fi + ;; + buster) # Debian 10 + __CodeName=buster + __LLDB_Package="liblldb-6.0-dev" + + if [[ -z "$__UbuntuRepo" ]]; then + __UbuntuRepo="http://ftp.debian.org/debian/" + fi + ;; + bullseye) # Debian 11 + __CodeName=bullseye + + if [[ -z "$__UbuntuRepo" ]]; then + __UbuntuRepo="http://ftp.debian.org/debian/" + fi + ;; + sid) # Debian sid + __CodeName=sid + + if [[ -z "$__UbuntuRepo" ]]; then + __UbuntuRepo="http://ftp.debian.org/debian/" + fi + ;; + tizen) + __CodeName= + __UbuntuRepo= + __Tizen=tizen + ;; + alpine*) + __CodeName=alpine + __UbuntuRepo= + version="${lowerI/alpine/}" + + if [[ "$version" == "edge" ]]; then + __AlpineVersion=edge + else + parts=(${version//./ }) + __AlpineMajorVersion="${parts[0]}" + __AlpineMinoVersion="${parts[1]}" + __AlpineVersion="$__AlpineMajorVersion.$__AlpineMinoVersion" + fi + ;; + freebsd12) + __CodeName=freebsd + __SkipUnmount=1 + ;; + freebsd13) + __CodeName=freebsd + __FreeBSDBase="13.2-RELEASE" + __FreeBSDABI="13" + __SkipUnmount=1 + ;; + illumos) + __CodeName=illumos + __SkipUnmount=1 + ;; + haiku) + __CodeName=haiku + __SkipUnmount=1 + ;; + --skipunmount) + __SkipUnmount=1 + ;; + --skipsigcheck) + __SkipSigCheck=1 + ;; + --rootfsdir|-rootfsdir) + shift + __RootfsDir="$1" + ;; + --use-mirror) + __UseMirror=1 + ;; + --use-jobs) + shift + MAXJOBS=$1 + ;; + *) + __UnprocessedBuildArgs="$__UnprocessedBuildArgs $1" + ;; + esac + + shift +done + +case "$__AlpineVersion" in + 3.14) __AlpinePackages+=" llvm11-libs" ;; + 3.15) __AlpinePackages+=" llvm12-libs" ;; + 3.16) __AlpinePackages+=" llvm13-libs" ;; + 3.17) __AlpinePackages+=" llvm15-libs" ;; + edge) __AlpineLlvmLibsLookup=1 ;; + *) + if [[ "$__AlpineArch" =~ s390x|ppc64le ]]; then + __AlpineVersion=3.15 # minimum version that supports lldb-dev + __AlpinePackages+=" llvm12-libs" + elif [[ "$__AlpineArch" == "x86" ]]; then + __AlpineVersion=3.17 # minimum version that supports lldb-dev + __AlpinePackages+=" llvm15-libs" + elif [[ "$__AlpineArch" == "riscv64" ]]; then + __AlpineLlvmLibsLookup=1 + __AlpineVersion=edge # minimum version with APKINDEX.tar.gz (packages archive) + else + __AlpineVersion=3.13 # 3.13 to maximize compatibility + __AlpinePackages+=" llvm10-libs" + + if [[ "$__AlpineArch" == "armv7" ]]; then + __AlpinePackages="${__AlpinePackages//numactl-dev/}" + fi + fi +esac + +if [[ "$__AlpineVersion" =~ 3\.1[345] ]]; then + # compiler-rt--static was merged in compiler-rt package in alpine 3.16 + # for older versions, we need compiler-rt--static, so replace the name + __AlpinePackages="${__AlpinePackages/compiler-rt/compiler-rt-static}" +fi + +if [[ "$__BuildArch" == "armel" ]]; then + __LLDB_Package="lldb-3.5-dev" +fi + +if [[ "$__CodeName" == "xenial" && "$__UbuntuArch" == "armhf" ]]; then + # libnuma-dev is not available on armhf for xenial + __UbuntuPackages="${__UbuntuPackages//libnuma-dev/}" +fi + +__UbuntuPackages+=" ${__LLDB_Package:-}" + +if [[ -n "$__LLVM_MajorVersion" ]]; then + __UbuntuPackages+=" libclang-common-${__LLVM_MajorVersion}${__LLVM_MinorVersion:+.$__LLVM_MinorVersion}-dev" +fi + +if [[ -z "$__RootfsDir" && -n "$ROOTFS_DIR" ]]; then + __RootfsDir="$ROOTFS_DIR" +fi + +if [[ -z "$__RootfsDir" ]]; then + __RootfsDir="$__CrossDir/../../../.tools/rootfs/$__BuildArch" +fi + +if [[ -d "$__RootfsDir" ]]; then + if [[ "$__SkipUnmount" == "0" ]]; then + umount "$__RootfsDir"/* || true + fi + rm -rf "$__RootfsDir" +fi + +mkdir -p "$__RootfsDir" +__RootfsDir="$( cd "$__RootfsDir" && pwd )" + +if [[ "$__CodeName" == "alpine" ]]; then + __ApkToolsVersion=2.12.11 + __ApkToolsSHA512SUM=53e57b49230da07ef44ee0765b9592580308c407a8d4da7125550957bb72cb59638e04f8892a18b584451c8d841d1c7cb0f0ab680cc323a3015776affaa3be33 + __ApkToolsDir="$(mktemp -d)" + __ApkKeysDir="$(mktemp -d)" + + wget "https://gitlab.alpinelinux.org/api/v4/projects/5/packages/generic//v$__ApkToolsVersion/x86_64/apk.static" -P "$__ApkToolsDir" + echo "$__ApkToolsSHA512SUM $__ApkToolsDir/apk.static" | sha512sum -c + chmod +x "$__ApkToolsDir/apk.static" + + if [[ -f "/usr/bin/qemu-$__QEMUArch-static" ]]; then + mkdir -p "$__RootfsDir"/usr/bin + cp -v "/usr/bin/qemu-$__QEMUArch-static" "$__RootfsDir/usr/bin" + fi + + if [[ "$__AlpineVersion" == "edge" ]]; then + version=edge + else + version="v$__AlpineVersion" + fi + + for line in $__AlpineKeys; do + id="${line%%:*}" + content="${line#*:}" + + echo -e "-----BEGIN PUBLIC KEY-----\n$content\n-----END PUBLIC KEY-----" > "$__ApkKeysDir/alpine-devel@lists.alpinelinux.org-$id.rsa.pub" + done + + if [[ "$__SkipSigCheck" == "1" ]]; then + __ApkSignatureArg="--allow-untrusted" + else + __ApkSignatureArg="--keys-dir $__ApkKeysDir" + fi + + # initialize DB + "$__ApkToolsDir/apk.static" \ + -X "http://dl-cdn.alpinelinux.org/alpine/$version/main" \ + -X "http://dl-cdn.alpinelinux.org/alpine/$version/community" \ + -U $__ApkSignatureArg --root "$__RootfsDir" --arch "$__AlpineArch" --initdb add + + if [[ "$__AlpineLlvmLibsLookup" == 1 ]]; then + __AlpinePackages+=" $("$__ApkToolsDir/apk.static" \ + -X "http://dl-cdn.alpinelinux.org/alpine/$version/main" \ + -X "http://dl-cdn.alpinelinux.org/alpine/$version/community" \ + -U $__ApkSignatureArg --root "$__RootfsDir" --arch "$__AlpineArch" \ + search 'llvm*-libs' | sort | tail -1 | sed 's/-[^-]*//2g')" + fi + + # install all packages in one go + "$__ApkToolsDir/apk.static" \ + -X "http://dl-cdn.alpinelinux.org/alpine/$version/main" \ + -X "http://dl-cdn.alpinelinux.org/alpine/$version/community" \ + -U $__ApkSignatureArg --root "$__RootfsDir" --arch "$__AlpineArch" \ + add $__AlpinePackages + + rm -r "$__ApkToolsDir" +elif [[ "$__CodeName" == "freebsd" ]]; then + mkdir -p "$__RootfsDir"/usr/local/etc + JOBS=${MAXJOBS:="$(getconf _NPROCESSORS_ONLN)"} + wget -O - "https://download.freebsd.org/ftp/releases/${__FreeBSDArch}/${__FreeBSDMachineArch}/${__FreeBSDBase}/base.txz" | tar -C "$__RootfsDir" -Jxf - ./lib ./usr/lib ./usr/libdata ./usr/include ./usr/share/keys ./etc ./bin/freebsd-version + echo "ABI = \"FreeBSD:${__FreeBSDABI}:${__FreeBSDMachineArch}\"; FINGERPRINTS = \"${__RootfsDir}/usr/share/keys\"; REPOS_DIR = [\"${__RootfsDir}/etc/pkg\"]; REPO_AUTOUPDATE = NO; RUN_SCRIPTS = NO;" > "${__RootfsDir}"/usr/local/etc/pkg.conf + echo "FreeBSD: { url: \"pkg+http://pkg.FreeBSD.org/\${ABI}/quarterly\", mirror_type: \"srv\", signature_type: \"fingerprints\", fingerprints: \"${__RootfsDir}/usr/share/keys/pkg\", enabled: yes }" > "${__RootfsDir}"/etc/pkg/FreeBSD.conf + mkdir -p "$__RootfsDir"/tmp + # get and build package manager + wget -O - "https://github.com/freebsd/pkg/archive/${__FreeBSDPkg}.tar.gz" | tar -C "$__RootfsDir"/tmp -zxf - + cd "$__RootfsDir/tmp/pkg-${__FreeBSDPkg}" + # needed for install to succeed + mkdir -p "$__RootfsDir"/host/etc + ./autogen.sh && ./configure --prefix="$__RootfsDir"/host && make -j "$JOBS" && make install + rm -rf "$__RootfsDir/tmp/pkg-${__FreeBSDPkg}" + # install packages we need. + INSTALL_AS_USER=$(whoami) "$__RootfsDir"/host/sbin/pkg -r "$__RootfsDir" -C "$__RootfsDir"/usr/local/etc/pkg.conf update + INSTALL_AS_USER=$(whoami) "$__RootfsDir"/host/sbin/pkg -r "$__RootfsDir" -C "$__RootfsDir"/usr/local/etc/pkg.conf install --yes $__FreeBSDPackages +elif [[ "$__CodeName" == "illumos" ]]; then + mkdir "$__RootfsDir/tmp" + pushd "$__RootfsDir/tmp" + JOBS=${MAXJOBS:="$(getconf _NPROCESSORS_ONLN)"} + echo "Downloading sysroot." + wget -O - https://github.com/illumos/sysroot/releases/download/20181213-de6af22ae73b-v1/illumos-sysroot-i386-20181213-de6af22ae73b-v1.tar.gz | tar -C "$__RootfsDir" -xzf - + echo "Building binutils. Please wait.." + wget -O - https://ftp.gnu.org/gnu/binutils/binutils-2.33.1.tar.bz2 | tar -xjf - + mkdir build-binutils && cd build-binutils + ../binutils-2.33.1/configure --prefix="$__RootfsDir" --target="${__illumosArch}-sun-solaris2.10" --program-prefix="${__illumosArch}-illumos-" --with-sysroot="$__RootfsDir" + make -j "$JOBS" && make install && cd .. + echo "Building gcc. Please wait.." + wget -O - https://ftp.gnu.org/gnu/gcc/gcc-8.4.0/gcc-8.4.0.tar.xz | tar -xJf - + CFLAGS="-fPIC" + CXXFLAGS="-fPIC" + CXXFLAGS_FOR_TARGET="-fPIC" + CFLAGS_FOR_TARGET="-fPIC" + export CFLAGS CXXFLAGS CXXFLAGS_FOR_TARGET CFLAGS_FOR_TARGET + mkdir build-gcc && cd build-gcc + ../gcc-8.4.0/configure --prefix="$__RootfsDir" --target="${__illumosArch}-sun-solaris2.10" --program-prefix="${__illumosArch}-illumos-" --with-sysroot="$__RootfsDir" --with-gnu-as \ + --with-gnu-ld --disable-nls --disable-libgomp --disable-libquadmath --disable-libssp --disable-libvtv --disable-libcilkrts --disable-libada --disable-libsanitizer \ + --disable-libquadmath-support --disable-shared --enable-tls + make -j "$JOBS" && make install && cd .. + BaseUrl=https://pkgsrc.smartos.org + if [[ "$__UseMirror" == 1 ]]; then + BaseUrl=https://pkgsrc.smartos.skylime.net + fi + BaseUrl="$BaseUrl/packages/SmartOS/trunk/${__illumosArch}/All" + echo "Downloading manifest" + wget "$BaseUrl" + echo "Downloading dependencies." + read -ra array <<<"$__IllumosPackages" + for package in "${array[@]}"; do + echo "Installing '$package'" + # find last occurrence of package in listing and extract its name + package="$(sed -En '/.*href="('"$package"'-[0-9].*).tgz".*/h;$!d;g;s//\1/p' All)" + echo "Resolved name '$package'" + wget "$BaseUrl"/"$package".tgz + ar -x "$package".tgz + tar --skip-old-files -xzf "$package".tmp.tg* -C "$__RootfsDir" 2>/dev/null + done + echo "Cleaning up temporary files." + popd + rm -rf "$__RootfsDir"/{tmp,+*} + mkdir -p "$__RootfsDir"/usr/include/net + mkdir -p "$__RootfsDir"/usr/include/netpacket + wget -P "$__RootfsDir"/usr/include/net https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/io/bpf/net/bpf.h + wget -P "$__RootfsDir"/usr/include/net https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/io/bpf/net/dlt.h + wget -P "$__RootfsDir"/usr/include/netpacket https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/inet/sockmods/netpacket/packet.h + wget -P "$__RootfsDir"/usr/include/sys https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/sys/sdt.h +elif [[ "$__CodeName" == "haiku" ]]; then + JOBS=${MAXJOBS:="$(getconf _NPROCESSORS_ONLN)"} + + echo "Building Haiku sysroot for $__HaikuArch" + mkdir -p "$__RootfsDir/tmp" + pushd "$__RootfsDir/tmp" + + mkdir "$__RootfsDir/tmp/download" + + echo "Downloading Haiku package tool" + git clone https://github.com/haiku/haiku-toolchains-ubuntu --depth 1 $__RootfsDir/tmp/script + wget -O "$__RootfsDir/tmp/download/hosttools.zip" $($__RootfsDir/tmp/script/fetch.sh --hosttools) + unzip -o "$__RootfsDir/tmp/download/hosttools.zip" -d "$__RootfsDir/tmp/bin" + + DepotBaseUrl="https://depot.haiku-os.org/__api/v2/pkg/get-pkg" + HpkgBaseUrl="https://eu.hpkg.haiku-os.org/haiku/master/$__HaikuArch/current" + + # Download Haiku packages + echo "Downloading Haiku packages" + read -ra array <<<"$__HaikuPackages" + for package in "${array[@]}"; do + echo "Downloading $package..." + # API documented here: https://github.com/haiku/haikudepotserver/blob/master/haikudepotserver-api2/src/main/resources/api2/pkg.yaml#L60 + # The schema here: https://github.com/haiku/haikudepotserver/blob/master/haikudepotserver-api2/src/main/resources/api2/pkg.yaml#L598 + hpkgDownloadUrl="$(wget -qO- --post-data='{"name":"'"$package"'","repositorySourceCode":"haikuports_'$__HaikuArch'","versionType":"LATEST","naturalLanguageCode":"en"}' \ + --header='Content-Type:application/json' "$DepotBaseUrl" | jq -r '.result.versions[].hpkgDownloadURL')" + wget -P "$__RootfsDir/tmp/download" "$hpkgDownloadUrl" + done + for package in haiku haiku_devel; do + echo "Downloading $package..." + hpkgVersion="$(wget -qO- $HpkgBaseUrl | sed -n 's/^.*version: "\([^"]*\)".*$/\1/p')" + wget -P "$__RootfsDir/tmp/download" "$HpkgBaseUrl/packages/$package-$hpkgVersion-1-$__HaikuArch.hpkg" + done + + # Set up the sysroot + echo "Setting up sysroot and extracting required packages" + mkdir -p "$__RootfsDir/boot/system" + for file in "$__RootfsDir/tmp/download/"*.hpkg; do + echo "Extracting $file..." + LD_LIBRARY_PATH="$__RootfsDir/tmp/bin" "$__RootfsDir/tmp/bin/package" extract -C "$__RootfsDir/boot/system" "$file" + done + + # Download buildtools + echo "Downloading Haiku buildtools" + wget -O "$__RootfsDir/tmp/download/buildtools.zip" $($__RootfsDir/tmp/script/fetch.sh --buildtools --arch=$__HaikuArch) + unzip -o "$__RootfsDir/tmp/download/buildtools.zip" -d "$__RootfsDir" + + # Cleaning up temporary files + echo "Cleaning up temporary files" + popd + rm -rf "$__RootfsDir/tmp" +elif [[ -n "$__CodeName" ]]; then + + if [[ "$__SkipSigCheck" == "0" ]]; then + __Keyring="$__Keyring --force-check-gpg" + fi + + debootstrap "--variant=minbase" $__Keyring --arch "$__UbuntuArch" "$__CodeName" "$__RootfsDir" "$__UbuntuRepo" + cp "$__CrossDir/$__BuildArch/sources.list.$__CodeName" "$__RootfsDir/etc/apt/sources.list" + chroot "$__RootfsDir" apt-get update + chroot "$__RootfsDir" apt-get -f -y install + chroot "$__RootfsDir" apt-get -y install $__UbuntuPackages + chroot "$__RootfsDir" symlinks -cr /usr + chroot "$__RootfsDir" apt-get clean + + if [[ "$__SkipUnmount" == "0" ]]; then + umount "$__RootfsDir"/* || true + fi + + if [[ "$__BuildArch" == "armel" && "$__CodeName" == "jessie" ]]; then + pushd "$__RootfsDir" + patch -p1 < "$__CrossDir/$__BuildArch/armel.jessie.patch" + popd + fi +elif [[ "$__Tizen" == "tizen" ]]; then + ROOTFS_DIR="$__RootfsDir" "$__CrossDir/tizen-build-rootfs.sh" "$__BuildArch" +else + echo "Unsupported target platform." + usage; + exit 1 +fi diff --git a/eng/common/cross/ppc64le/sources.list.bionic b/eng/common/cross/ppc64le/sources.list.bionic new file mode 100644 index 0000000000..2109557409 --- /dev/null +++ b/eng/common/cross/ppc64le/sources.list.bionic @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted universe multiverse diff --git a/eng/common/cross/riscv64/sources.list.sid b/eng/common/cross/riscv64/sources.list.sid new file mode 100644 index 0000000000..65f730d224 --- /dev/null +++ b/eng/common/cross/riscv64/sources.list.sid @@ -0,0 +1 @@ +deb http://deb.debian.org/debian-ports sid main diff --git a/eng/common/cross/s390x/sources.list.bionic b/eng/common/cross/s390x/sources.list.bionic new file mode 100644 index 0000000000..2109557409 --- /dev/null +++ b/eng/common/cross/s390x/sources.list.bionic @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted universe multiverse diff --git a/eng/common/cross/tizen-build-rootfs.sh b/eng/common/cross/tizen-build-rootfs.sh new file mode 100755 index 0000000000..ac84173d44 --- /dev/null +++ b/eng/common/cross/tizen-build-rootfs.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -e + +ARCH=$1 +LINK_ARCH=$ARCH + +case "$ARCH" in + arm) + TIZEN_ARCH="armv7hl" + ;; + armel) + TIZEN_ARCH="armv7l" + LINK_ARCH="arm" + ;; + arm64) + TIZEN_ARCH="aarch64" + ;; + x86) + TIZEN_ARCH="i686" + ;; + x64) + TIZEN_ARCH="x86_64" + LINK_ARCH="x86" + ;; + *) + echo "Unsupported architecture for tizen: $ARCH" + exit 1 +esac + +__CrossDir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +__TIZEN_CROSSDIR="$__CrossDir/${ARCH}/tizen" + +if [[ -z "$ROOTFS_DIR" ]]; then + echo "ROOTFS_DIR is not defined." + exit 1; +fi + +TIZEN_TMP_DIR=$ROOTFS_DIR/tizen_tmp +mkdir -p $TIZEN_TMP_DIR + +# Download files +echo ">>Start downloading files" +VERBOSE=1 $__CrossDir/tizen-fetch.sh $TIZEN_TMP_DIR $TIZEN_ARCH +echo "<>Start constructing Tizen rootfs" +TIZEN_RPM_FILES=`ls $TIZEN_TMP_DIR/*.rpm` +cd $ROOTFS_DIR +for f in $TIZEN_RPM_FILES; do + rpm2cpio $f | cpio -idm --quiet +done +echo "<>Start configuring Tizen rootfs" +ln -sfn asm-${LINK_ARCH} ./usr/include/asm +patch -p1 < $__TIZEN_CROSSDIR/tizen.patch +echo "</dev/null; then + VERBOSE=0 +fi + +Log() +{ + if [ $VERBOSE -ge $1 ]; then + echo ${@:2} + fi +} + +Inform() +{ + Log 1 -e "\x1B[0;34m$@\x1B[m" +} + +Debug() +{ + Log 2 -e "\x1B[0;32m$@\x1B[m" +} + +Error() +{ + >&2 Log 0 -e "\x1B[0;31m$@\x1B[m" +} + +Fetch() +{ + URL=$1 + FILE=$2 + PROGRESS=$3 + if [ $VERBOSE -ge 1 ] && [ $PROGRESS ]; then + CURL_OPT="--progress-bar" + else + CURL_OPT="--silent" + fi + curl $CURL_OPT $URL > $FILE +} + +hash curl 2> /dev/null || { Error "Require 'curl' Aborting."; exit 1; } +hash xmllint 2> /dev/null || { Error "Require 'xmllint' Aborting."; exit 1; } +hash sha256sum 2> /dev/null || { Error "Require 'sha256sum' Aborting."; exit 1; } + +TMPDIR=$1 +if [ ! -d $TMPDIR ]; then + TMPDIR=./tizen_tmp + Debug "Create temporary directory : $TMPDIR" + mkdir -p $TMPDIR +fi + +TIZEN_ARCH=$2 + +TIZEN_URL=http://download.tizen.org/snapshots/TIZEN/Tizen +BUILD_XML=build.xml +REPOMD_XML=repomd.xml +PRIMARY_XML=primary.xml +TARGET_URL="http://__not_initialized" + +Xpath_get() +{ + XPATH_RESULT='' + XPATH=$1 + XML_FILE=$2 + RESULT=$(xmllint --xpath $XPATH $XML_FILE) + if [[ -z ${RESULT// } ]]; then + Error "Can not find target from $XML_FILE" + Debug "Xpath = $XPATH" + exit 1 + fi + XPATH_RESULT=$RESULT +} + +fetch_tizen_pkgs_init() +{ + TARGET=$1 + PROFILE=$2 + Debug "Initialize TARGET=$TARGET, PROFILE=$PROFILE" + + TMP_PKG_DIR=$TMPDIR/tizen_${PROFILE}_pkgs + if [ -d $TMP_PKG_DIR ]; then rm -rf $TMP_PKG_DIR; fi + mkdir -p $TMP_PKG_DIR + + PKG_URL=$TIZEN_URL/$PROFILE/latest + + BUILD_XML_URL=$PKG_URL/$BUILD_XML + TMP_BUILD=$TMP_PKG_DIR/$BUILD_XML + TMP_REPOMD=$TMP_PKG_DIR/$REPOMD_XML + TMP_PRIMARY=$TMP_PKG_DIR/$PRIMARY_XML + TMP_PRIMARYGZ=${TMP_PRIMARY}.gz + + Fetch $BUILD_XML_URL $TMP_BUILD + + Debug "fetch $BUILD_XML_URL to $TMP_BUILD" + + TARGET_XPATH="//build/buildtargets/buildtarget[@name=\"$TARGET\"]/repo[@type=\"binary\"]/text()" + Xpath_get $TARGET_XPATH $TMP_BUILD + TARGET_PATH=$XPATH_RESULT + TARGET_URL=$PKG_URL/$TARGET_PATH + + REPOMD_URL=$TARGET_URL/repodata/repomd.xml + PRIMARY_XPATH='string(//*[local-name()="data"][@type="primary"]/*[local-name()="location"]/@href)' + + Fetch $REPOMD_URL $TMP_REPOMD + + Debug "fetch $REPOMD_URL to $TMP_REPOMD" + + Xpath_get $PRIMARY_XPATH $TMP_REPOMD + PRIMARY_XML_PATH=$XPATH_RESULT + PRIMARY_URL=$TARGET_URL/$PRIMARY_XML_PATH + + Fetch $PRIMARY_URL $TMP_PRIMARYGZ + + Debug "fetch $PRIMARY_URL to $TMP_PRIMARYGZ" + + gunzip $TMP_PRIMARYGZ + + Debug "unzip $TMP_PRIMARYGZ to $TMP_PRIMARY" +} + +fetch_tizen_pkgs() +{ + ARCH=$1 + PACKAGE_XPATH_TPL='string(//*[local-name()="metadata"]/*[local-name()="package"][*[local-name()="name"][text()="_PKG_"]][*[local-name()="arch"][text()="_ARCH_"]]/*[local-name()="location"]/@href)' + + PACKAGE_CHECKSUM_XPATH_TPL='string(//*[local-name()="metadata"]/*[local-name()="package"][*[local-name()="name"][text()="_PKG_"]][*[local-name()="arch"][text()="_ARCH_"]]/*[local-name()="checksum"]/text())' + + for pkg in ${@:2} + do + Inform "Fetching... $pkg" + XPATH=${PACKAGE_XPATH_TPL/_PKG_/$pkg} + XPATH=${XPATH/_ARCH_/$ARCH} + Xpath_get $XPATH $TMP_PRIMARY + PKG_PATH=$XPATH_RESULT + + XPATH=${PACKAGE_CHECKSUM_XPATH_TPL/_PKG_/$pkg} + XPATH=${XPATH/_ARCH_/$ARCH} + Xpath_get $XPATH $TMP_PRIMARY + CHECKSUM=$XPATH_RESULT + + PKG_URL=$TARGET_URL/$PKG_PATH + PKG_FILE=$(basename $PKG_PATH) + PKG_PATH=$TMPDIR/$PKG_FILE + + Debug "Download $PKG_URL to $PKG_PATH" + Fetch $PKG_URL $PKG_PATH true + + echo "$CHECKSUM $PKG_PATH" | sha256sum -c - > /dev/null + if [ $? -ne 0 ]; then + Error "Fail to fetch $PKG_URL to $PKG_PATH" + Debug "Checksum = $CHECKSUM" + exit 1 + fi + done +} + +Inform "Initialize ${TIZEN_ARCH} base" +fetch_tizen_pkgs_init standard Tizen-Base +Inform "fetch common packages" +fetch_tizen_pkgs ${TIZEN_ARCH} gcc gcc-devel-static glibc glibc-devel libicu libicu-devel libatomic linux-glibc-devel keyutils keyutils-devel libkeyutils +Inform "fetch coreclr packages" +fetch_tizen_pkgs ${TIZEN_ARCH} lldb lldb-devel libgcc libstdc++ libstdc++-devel libunwind libunwind-devel lttng-ust-devel lttng-ust userspace-rcu-devel userspace-rcu +Inform "fetch corefx packages" +fetch_tizen_pkgs ${TIZEN_ARCH} libcom_err libcom_err-devel zlib zlib-devel libopenssl11 libopenssl1.1-devel krb5 krb5-devel + +Inform "Initialize standard unified" +fetch_tizen_pkgs_init standard Tizen-Unified +Inform "fetch corefx packages" +fetch_tizen_pkgs ${TIZEN_ARCH} gssdp gssdp-devel tizen-release + diff --git a/eng/common/cross/toolchain.cmake b/eng/common/cross/toolchain.cmake new file mode 100644 index 0000000000..a88d643c8a --- /dev/null +++ b/eng/common/cross/toolchain.cmake @@ -0,0 +1,377 @@ +set(CROSS_ROOTFS $ENV{ROOTFS_DIR}) + +# reset platform variables (e.g. cmake 3.25 sets LINUX=1) +unset(LINUX) +unset(FREEBSD) +unset(ILLUMOS) +unset(ANDROID) +unset(TIZEN) +unset(HAIKU) + +set(TARGET_ARCH_NAME $ENV{TARGET_BUILD_ARCH}) +if(EXISTS ${CROSS_ROOTFS}/bin/freebsd-version) + set(CMAKE_SYSTEM_NAME FreeBSD) + set(FREEBSD 1) +elseif(EXISTS ${CROSS_ROOTFS}/usr/platform/i86pc) + set(CMAKE_SYSTEM_NAME SunOS) + set(ILLUMOS 1) +elseif(EXISTS ${CROSS_ROOTFS}/boot/system/develop/headers/config/HaikuConfig.h) + set(CMAKE_SYSTEM_NAME Haiku) + set(HAIKU 1) +else() + set(CMAKE_SYSTEM_NAME Linux) + set(LINUX 1) +endif() +set(CMAKE_SYSTEM_VERSION 1) + +if(EXISTS ${CROSS_ROOTFS}/etc/tizen-release) + set(TIZEN 1) +elseif(EXISTS ${CROSS_ROOTFS}/android_platform) + set(ANDROID 1) +endif() + +if(TARGET_ARCH_NAME STREQUAL "arm") + set(CMAKE_SYSTEM_PROCESSOR armv7l) + if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/armv7-alpine-linux-musleabihf) + set(TOOLCHAIN "armv7-alpine-linux-musleabihf") + elseif(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/armv6-alpine-linux-musleabihf) + set(TOOLCHAIN "armv6-alpine-linux-musleabihf") + else() + set(TOOLCHAIN "arm-linux-gnueabihf") + endif() + if(TIZEN) + set(TIZEN_TOOLCHAIN "armv7hl-tizen-linux-gnueabihf/9.2.0") + endif() +elseif(TARGET_ARCH_NAME STREQUAL "arm64") + set(CMAKE_SYSTEM_PROCESSOR aarch64) + if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/aarch64-alpine-linux-musl) + set(TOOLCHAIN "aarch64-alpine-linux-musl") + elseif(LINUX) + set(TOOLCHAIN "aarch64-linux-gnu") + if(TIZEN) + set(TIZEN_TOOLCHAIN "aarch64-tizen-linux-gnu/9.2.0") + endif() + elseif(FREEBSD) + set(triple "aarch64-unknown-freebsd12") + endif() +elseif(TARGET_ARCH_NAME STREQUAL "armel") + set(CMAKE_SYSTEM_PROCESSOR armv7l) + set(TOOLCHAIN "arm-linux-gnueabi") + if(TIZEN) + set(TIZEN_TOOLCHAIN "armv7l-tizen-linux-gnueabi/9.2.0") + endif() +elseif(TARGET_ARCH_NAME STREQUAL "armv6") + set(CMAKE_SYSTEM_PROCESSOR armv6l) + if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/armv6-alpine-linux-musleabihf) + set(TOOLCHAIN "armv6-alpine-linux-musleabihf") + else() + set(TOOLCHAIN "arm-linux-gnueabihf") + endif() +elseif(TARGET_ARCH_NAME STREQUAL "ppc64le") + set(CMAKE_SYSTEM_PROCESSOR ppc64le) + if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/powerpc64le-alpine-linux-musl) + set(TOOLCHAIN "powerpc64le-alpine-linux-musl") + else() + set(TOOLCHAIN "powerpc64le-linux-gnu") + endif() +elseif(TARGET_ARCH_NAME STREQUAL "riscv64") + set(CMAKE_SYSTEM_PROCESSOR riscv64) + if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/riscv64-alpine-linux-musl) + set(TOOLCHAIN "riscv64-alpine-linux-musl") + else() + set(TOOLCHAIN "riscv64-linux-gnu") + endif() +elseif(TARGET_ARCH_NAME STREQUAL "s390x") + set(CMAKE_SYSTEM_PROCESSOR s390x) + if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/s390x-alpine-linux-musl) + set(TOOLCHAIN "s390x-alpine-linux-musl") + else() + set(TOOLCHAIN "s390x-linux-gnu") + endif() +elseif(TARGET_ARCH_NAME STREQUAL "x64") + set(CMAKE_SYSTEM_PROCESSOR x86_64) + if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/x86_64-alpine-linux-musl) + set(TOOLCHAIN "x86_64-alpine-linux-musl") + elseif(LINUX) + set(TOOLCHAIN "x86_64-linux-gnu") + if(TIZEN) + set(TIZEN_TOOLCHAIN "x86_64-tizen-linux-gnu/9.2.0") + endif() + elseif(FREEBSD) + set(triple "x86_64-unknown-freebsd12") + elseif(ILLUMOS) + set(TOOLCHAIN "x86_64-illumos") + elseif(HAIKU) + set(TOOLCHAIN "x86_64-unknown-haiku") + endif() +elseif(TARGET_ARCH_NAME STREQUAL "x86") + set(CMAKE_SYSTEM_PROCESSOR i686) + if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/i586-alpine-linux-musl) + set(TOOLCHAIN "i586-alpine-linux-musl") + else() + set(TOOLCHAIN "i686-linux-gnu") + endif() + if(TIZEN) + set(TIZEN_TOOLCHAIN "i586-tizen-linux-gnu/9.2.0") + endif() +else() + message(FATAL_ERROR "Arch is ${TARGET_ARCH_NAME}. Only arm, arm64, armel, armv6, ppc64le, riscv64, s390x, x64 and x86 are supported!") +endif() + +if(DEFINED ENV{TOOLCHAIN}) + set(TOOLCHAIN $ENV{TOOLCHAIN}) +endif() + +# Specify include paths +if(TIZEN) + if(TARGET_ARCH_NAME STREQUAL "arm") + include_directories(SYSTEM ${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}/include/c++/) + include_directories(SYSTEM ${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}/include/c++/armv7hl-tizen-linux-gnueabihf) + endif() + if(TARGET_ARCH_NAME STREQUAL "armel") + include_directories(SYSTEM ${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}/include/c++/) + include_directories(SYSTEM ${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}/include/c++/armv7l-tizen-linux-gnueabi) + endif() + if(TARGET_ARCH_NAME STREQUAL "arm64") + include_directories(SYSTEM ${CROSS_ROOTFS}/usr/lib64/gcc/${TIZEN_TOOLCHAIN}/include/c++/) + include_directories(SYSTEM ${CROSS_ROOTFS}/usr/lib64/gcc/${TIZEN_TOOLCHAIN}/include/c++/aarch64-tizen-linux-gnu) + endif() + if(TARGET_ARCH_NAME STREQUAL "x86") + include_directories(SYSTEM ${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}/include/c++/) + include_directories(SYSTEM ${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}/include/c++/i586-tizen-linux-gnu) + endif() + if(TARGET_ARCH_NAME STREQUAL "x64") + include_directories(SYSTEM ${CROSS_ROOTFS}/usr/lib64/gcc/${TIZEN_TOOLCHAIN}/include/c++/) + include_directories(SYSTEM ${CROSS_ROOTFS}/usr/lib64/gcc/${TIZEN_TOOLCHAIN}/include/c++/x86_64-tizen-linux-gnu) + endif() +endif() + +if(ANDROID) + if(TARGET_ARCH_NAME STREQUAL "arm") + set(ANDROID_ABI armeabi-v7a) + elseif(TARGET_ARCH_NAME STREQUAL "arm64") + set(ANDROID_ABI arm64-v8a) + endif() + + # extract platform number required by the NDK's toolchain + file(READ "${CROSS_ROOTFS}/android_platform" RID_FILE_CONTENTS) + string(REPLACE "RID=" "" ANDROID_RID "${RID_FILE_CONTENTS}") + string(REGEX REPLACE ".*\\.([0-9]+)-.*" "\\1" ANDROID_PLATFORM "${ANDROID_RID}") + + set(ANDROID_TOOLCHAIN clang) + set(FEATURE_EVENT_TRACE 0) # disable event trace as there is no lttng-ust package in termux repository + set(CMAKE_SYSTEM_LIBRARY_PATH "${CROSS_ROOTFS}/usr/lib") + set(CMAKE_SYSTEM_INCLUDE_PATH "${CROSS_ROOTFS}/usr/include") + + # include official NDK toolchain script + include(${CROSS_ROOTFS}/../build/cmake/android.toolchain.cmake) +elseif(FREEBSD) + # we cross-compile by instructing clang + set(CMAKE_C_COMPILER_TARGET ${triple}) + set(CMAKE_CXX_COMPILER_TARGET ${triple}) + set(CMAKE_ASM_COMPILER_TARGET ${triple}) + set(CMAKE_SYSROOT "${CROSS_ROOTFS}") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=lld") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fuse-ld=lld") + set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -fuse-ld=lld") +elseif(ILLUMOS) + set(CMAKE_SYSROOT "${CROSS_ROOTFS}") + + include_directories(SYSTEM ${CROSS_ROOTFS}/include) + + set(TOOLSET_PREFIX ${TOOLCHAIN}-) + function(locate_toolchain_exec exec var) + string(TOUPPER ${exec} EXEC_UPPERCASE) + if(NOT "$ENV{CLR_${EXEC_UPPERCASE}}" STREQUAL "") + set(${var} "$ENV{CLR_${EXEC_UPPERCASE}}" PARENT_SCOPE) + return() + endif() + + find_program(EXEC_LOCATION_${exec} + NAMES + "${TOOLSET_PREFIX}${exec}${CLR_CMAKE_COMPILER_FILE_NAME_VERSION}" + "${TOOLSET_PREFIX}${exec}") + + if (EXEC_LOCATION_${exec} STREQUAL "EXEC_LOCATION_${exec}-NOTFOUND") + message(FATAL_ERROR "Unable to find toolchain executable. Name: ${exec}, Prefix: ${TOOLSET_PREFIX}.") + endif() + set(${var} ${EXEC_LOCATION_${exec}} PARENT_SCOPE) + endfunction() + + set(CMAKE_SYSTEM_PREFIX_PATH "${CROSS_ROOTFS}") + + locate_toolchain_exec(gcc CMAKE_C_COMPILER) + locate_toolchain_exec(g++ CMAKE_CXX_COMPILER) + + set(CMAKE_C_STANDARD_LIBRARIES "${CMAKE_C_STANDARD_LIBRARIES} -lssp") + set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -lssp") +elseif(HAIKU) + set(CMAKE_SYSROOT "${CROSS_ROOTFS}") + + set(TOOLSET_PREFIX ${TOOLCHAIN}-) + function(locate_toolchain_exec exec var) + string(TOUPPER ${exec} EXEC_UPPERCASE) + if(NOT "$ENV{CLR_${EXEC_UPPERCASE}}" STREQUAL "") + set(${var} "$ENV{CLR_${EXEC_UPPERCASE}}" PARENT_SCOPE) + return() + endif() + + find_program(EXEC_LOCATION_${exec} + PATHS "${CROSS_ROOTFS}/cross-tools-x86_64/bin" + NAMES + "${TOOLSET_PREFIX}${exec}${CLR_CMAKE_COMPILER_FILE_NAME_VERSION}" + "${TOOLSET_PREFIX}${exec}") + + if (EXEC_LOCATION_${exec} STREQUAL "EXEC_LOCATION_${exec}-NOTFOUND") + message(FATAL_ERROR "Unable to find toolchain executable. Name: ${exec}, Prefix: ${TOOLSET_PREFIX}.") + endif() + set(${var} ${EXEC_LOCATION_${exec}} PARENT_SCOPE) + endfunction() + + set(CMAKE_SYSTEM_PREFIX_PATH "${CROSS_ROOTFS}") + + locate_toolchain_exec(gcc CMAKE_C_COMPILER) + locate_toolchain_exec(g++ CMAKE_CXX_COMPILER) + + set(CMAKE_C_STANDARD_LIBRARIES "${CMAKE_C_STANDARD_LIBRARIES} -lssp") + set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -lssp") + + # let CMake set up the correct search paths + include(Platform/Haiku) +else() + set(CMAKE_SYSROOT "${CROSS_ROOTFS}") + + set(CMAKE_C_COMPILER_EXTERNAL_TOOLCHAIN "${CROSS_ROOTFS}/usr") + set(CMAKE_CXX_COMPILER_EXTERNAL_TOOLCHAIN "${CROSS_ROOTFS}/usr") + set(CMAKE_ASM_COMPILER_EXTERNAL_TOOLCHAIN "${CROSS_ROOTFS}/usr") +endif() + +# Specify link flags + +function(add_toolchain_linker_flag Flag) + set(Config "${ARGV1}") + set(CONFIG_SUFFIX "") + if (NOT Config STREQUAL "") + set(CONFIG_SUFFIX "_${Config}") + endif() + set("CMAKE_EXE_LINKER_FLAGS${CONFIG_SUFFIX}_INIT" "${CMAKE_EXE_LINKER_FLAGS${CONFIG_SUFFIX}_INIT} ${Flag}" PARENT_SCOPE) + set("CMAKE_SHARED_LINKER_FLAGS${CONFIG_SUFFIX}_INIT" "${CMAKE_SHARED_LINKER_FLAGS${CONFIG_SUFFIX}_INIT} ${Flag}" PARENT_SCOPE) +endfunction() + +if(LINUX) + add_toolchain_linker_flag("-Wl,--rpath-link=${CROSS_ROOTFS}/lib/${TOOLCHAIN}") + add_toolchain_linker_flag("-Wl,--rpath-link=${CROSS_ROOTFS}/usr/lib/${TOOLCHAIN}") +endif() + +if(TARGET_ARCH_NAME MATCHES "^(arm|armel)$") + if(TIZEN) + add_toolchain_linker_flag("-B${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}") + add_toolchain_linker_flag("-L${CROSS_ROOTFS}/lib") + add_toolchain_linker_flag("-L${CROSS_ROOTFS}/usr/lib") + add_toolchain_linker_flag("-L${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}") + endif() +elseif(TARGET_ARCH_NAME MATCHES "^(arm64|x64)$") + if(TIZEN) + add_toolchain_linker_flag("-B${CROSS_ROOTFS}/usr/lib64/gcc/${TIZEN_TOOLCHAIN}") + add_toolchain_linker_flag("-L${CROSS_ROOTFS}/lib64") + add_toolchain_linker_flag("-L${CROSS_ROOTFS}/usr/lib64") + add_toolchain_linker_flag("-L${CROSS_ROOTFS}/usr/lib64/gcc/${TIZEN_TOOLCHAIN}") + + add_toolchain_linker_flag("-Wl,--rpath-link=${CROSS_ROOTFS}/lib64") + add_toolchain_linker_flag("-Wl,--rpath-link=${CROSS_ROOTFS}/usr/lib64") + add_toolchain_linker_flag("-Wl,--rpath-link=${CROSS_ROOTFS}/usr/lib64/gcc/${TIZEN_TOOLCHAIN}") + endif() +elseif(TARGET_ARCH_NAME STREQUAL "x86") + if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/i586-alpine-linux-musl) + add_toolchain_linker_flag("--target=${TOOLCHAIN}") + add_toolchain_linker_flag("-Wl,--rpath-link=${CROSS_ROOTFS}/usr/lib/gcc/${TOOLCHAIN}") + endif() + add_toolchain_linker_flag(-m32) + if(TIZEN) + add_toolchain_linker_flag("-B${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}") + add_toolchain_linker_flag("-L${CROSS_ROOTFS}/lib") + add_toolchain_linker_flag("-L${CROSS_ROOTFS}/usr/lib") + add_toolchain_linker_flag("-L${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}") + endif() +elseif(ILLUMOS) + add_toolchain_linker_flag("-L${CROSS_ROOTFS}/lib/amd64") + add_toolchain_linker_flag("-L${CROSS_ROOTFS}/usr/amd64/lib") +elseif(HAIKU) + add_toolchain_linker_flag("-lnetwork") + add_toolchain_linker_flag("-lroot") +endif() + +# Specify compile options + +if((TARGET_ARCH_NAME MATCHES "^(arm|arm64|armel|armv6|ppc64le|riscv64|s390x|x64|x86)$" AND NOT ANDROID AND NOT FREEBSD) OR ILLUMOS OR HAIKU) + set(CMAKE_C_COMPILER_TARGET ${TOOLCHAIN}) + set(CMAKE_CXX_COMPILER_TARGET ${TOOLCHAIN}) + set(CMAKE_ASM_COMPILER_TARGET ${TOOLCHAIN}) +endif() + +if(TARGET_ARCH_NAME MATCHES "^(arm|armel)$") + add_compile_options(-mthumb) + if (NOT DEFINED CLR_ARM_FPU_TYPE) + set (CLR_ARM_FPU_TYPE vfpv3) + endif (NOT DEFINED CLR_ARM_FPU_TYPE) + + add_compile_options (-mfpu=${CLR_ARM_FPU_TYPE}) + if (NOT DEFINED CLR_ARM_FPU_CAPABILITY) + set (CLR_ARM_FPU_CAPABILITY 0x7) + endif (NOT DEFINED CLR_ARM_FPU_CAPABILITY) + + add_definitions (-DCLR_ARM_FPU_CAPABILITY=${CLR_ARM_FPU_CAPABILITY}) + + # persist variables across multiple try_compile passes + list(APPEND CMAKE_TRY_COMPILE_PLATFORM_VARIABLES CLR_ARM_FPU_TYPE CLR_ARM_FPU_CAPABILITY) + + if(TARGET_ARCH_NAME STREQUAL "armel") + add_compile_options(-mfloat-abi=softfp) + endif() +elseif(TARGET_ARCH_NAME STREQUAL "x86") + if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/i586-alpine-linux-musl) + add_compile_options(--target=${TOOLCHAIN}) + endif() + add_compile_options(-m32) + add_compile_options(-Wno-error=unused-command-line-argument) +endif() + +if(TIZEN) + if(TARGET_ARCH_NAME MATCHES "^(arm|armel|arm64|x86)$") + add_compile_options(-Wno-deprecated-declarations) # compile-time option + add_compile_options(-D__extern_always_inline=inline) # compile-time option + endif() +endif() + +# Set LLDB include and library paths for builds that need lldb. +if(TARGET_ARCH_NAME MATCHES "^(arm|armel|x86)$") + if(TARGET_ARCH_NAME STREQUAL "x86") + set(LLVM_CROSS_DIR "$ENV{LLVM_CROSS_HOME}") + else() # arm/armel case + set(LLVM_CROSS_DIR "$ENV{LLVM_ARM_HOME}") + endif() + if(LLVM_CROSS_DIR) + set(WITH_LLDB_LIBS "${LLVM_CROSS_DIR}/lib/" CACHE STRING "") + set(WITH_LLDB_INCLUDES "${LLVM_CROSS_DIR}/include" CACHE STRING "") + set(LLDB_H "${WITH_LLDB_INCLUDES}" CACHE STRING "") + set(LLDB "${LLVM_CROSS_DIR}/lib/liblldb.so" CACHE STRING "") + else() + if(TARGET_ARCH_NAME STREQUAL "x86") + set(WITH_LLDB_LIBS "${CROSS_ROOTFS}/usr/lib/i386-linux-gnu" CACHE STRING "") + set(CHECK_LLVM_DIR "${CROSS_ROOTFS}/usr/lib/llvm-3.8/include") + if(EXISTS "${CHECK_LLVM_DIR}" AND IS_DIRECTORY "${CHECK_LLVM_DIR}") + set(WITH_LLDB_INCLUDES "${CHECK_LLVM_DIR}") + else() + set(WITH_LLDB_INCLUDES "${CROSS_ROOTFS}/usr/lib/llvm-3.6/include") + endif() + else() # arm/armel case + set(WITH_LLDB_LIBS "${CROSS_ROOTFS}/usr/lib/${TOOLCHAIN}" CACHE STRING "") + set(WITH_LLDB_INCLUDES "${CROSS_ROOTFS}/usr/lib/llvm-3.6/include" CACHE STRING "") + endif() + endif() +endif() + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/eng/common/darc-init.ps1 b/eng/common/darc-init.ps1 new file mode 100644 index 0000000000..435e764134 --- /dev/null +++ b/eng/common/darc-init.ps1 @@ -0,0 +1,47 @@ +param ( + $darcVersion = $null, + $versionEndpoint = 'https://maestro-prod.westus2.cloudapp.azure.com/api/assets/darc-version?api-version=2019-01-16', + $verbosity = 'minimal', + $toolpath = $null +) + +. $PSScriptRoot\tools.ps1 + +function InstallDarcCli ($darcVersion, $toolpath) { + $darcCliPackageName = 'microsoft.dotnet.darc' + + $dotnetRoot = InitializeDotNetCli -install:$true + $dotnet = "$dotnetRoot\dotnet.exe" + $toolList = & "$dotnet" tool list -g + + if ($toolList -like "*$darcCliPackageName*") { + & "$dotnet" tool uninstall $darcCliPackageName -g + } + + # If the user didn't explicitly specify the darc version, + # query the Maestro API for the correct version of darc to install. + if (-not $darcVersion) { + $darcVersion = $(Invoke-WebRequest -Uri $versionEndpoint -UseBasicParsing).Content + } + + $arcadeServicesSource = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json' + + Write-Host "Installing Darc CLI version $darcVersion..." + Write-Host 'You may need to restart your command window if this is the first dotnet tool you have installed.' + if (-not $toolpath) { + Write-Host "'$dotnet' tool install $darcCliPackageName --version $darcVersion --add-source '$arcadeServicesSource' -v $verbosity -g" + & "$dotnet" tool install $darcCliPackageName --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity -g + }else { + Write-Host "'$dotnet' tool install $darcCliPackageName --version $darcVersion --add-source '$arcadeServicesSource' -v $verbosity --tool-path '$toolpath'" + & "$dotnet" tool install $darcCliPackageName --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity --tool-path "$toolpath" + } +} + +try { + InstallDarcCli $darcVersion $toolpath +} +catch { + Write-Host $_.ScriptStackTrace + Write-PipelineTelemetryError -Category 'Darc' -Message $_ + ExitWithExitCode 1 +} \ No newline at end of file diff --git a/eng/common/darc-init.sh b/eng/common/darc-init.sh new file mode 100755 index 0000000000..84c1d0cc2e --- /dev/null +++ b/eng/common/darc-init.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" +darcVersion='' +versionEndpoint='https://maestro-prod.westus2.cloudapp.azure.com/api/assets/darc-version?api-version=2019-01-16' +verbosity='minimal' + +while [[ $# > 0 ]]; do + opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" + case "$opt" in + --darcversion) + darcVersion=$2 + shift + ;; + --versionendpoint) + versionEndpoint=$2 + shift + ;; + --verbosity) + verbosity=$2 + shift + ;; + --toolpath) + toolpath=$2 + shift + ;; + *) + echo "Invalid argument: $1" + usage + exit 1 + ;; + esac + + shift +done + +# resolve $source until the file is no longer a symlink +while [[ -h "$source" ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +. "$scriptroot/tools.sh" + +if [ -z "$darcVersion" ]; then + darcVersion=$(curl -X GET "$versionEndpoint" -H "accept: text/plain") +fi + +function InstallDarcCli { + local darc_cli_package_name="microsoft.dotnet.darc" + + InitializeDotNetCli true + local dotnet_root=$_InitializeDotNetCli + + if [ -z "$toolpath" ]; then + local tool_list=$($dotnet_root/dotnet tool list -g) + if [[ $tool_list = *$darc_cli_package_name* ]]; then + echo $($dotnet_root/dotnet tool uninstall $darc_cli_package_name -g) + fi + else + local tool_list=$($dotnet_root/dotnet tool list --tool-path "$toolpath") + if [[ $tool_list = *$darc_cli_package_name* ]]; then + echo $($dotnet_root/dotnet tool uninstall $darc_cli_package_name --tool-path "$toolpath") + fi + fi + + local arcadeServicesSource="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" + + echo "Installing Darc CLI version $darcVersion..." + echo "You may need to restart your command shell if this is the first dotnet tool you have installed." + if [ -z "$toolpath" ]; then + echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity -g) + else + echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity --tool-path "$toolpath") + fi +} + +InstallDarcCli diff --git a/eng/common/dotnet-install.cmd b/eng/common/dotnet-install.cmd new file mode 100644 index 0000000000..b1c2642e76 --- /dev/null +++ b/eng/common/dotnet-install.cmd @@ -0,0 +1,2 @@ +@echo off +powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0dotnet-install.ps1""" %*" \ No newline at end of file diff --git a/eng/common/dotnet-install.ps1 b/eng/common/dotnet-install.ps1 new file mode 100644 index 0000000000..811f0f717f --- /dev/null +++ b/eng/common/dotnet-install.ps1 @@ -0,0 +1,28 @@ +[CmdletBinding(PositionalBinding=$false)] +Param( + [string] $verbosity = 'minimal', + [string] $architecture = '', + [string] $version = 'Latest', + [string] $runtime = 'dotnet', + [string] $RuntimeSourceFeed = '', + [string] $RuntimeSourceFeedKey = '' +) + +. $PSScriptRoot\tools.ps1 + +$dotnetRoot = Join-Path $RepoRoot '.dotnet' + +$installdir = $dotnetRoot +try { + if ($architecture -and $architecture.Trim() -eq 'x86') { + $installdir = Join-Path $installdir 'x86' + } + InstallDotNet $installdir $version $architecture $runtime $true -RuntimeSourceFeed $RuntimeSourceFeed -RuntimeSourceFeedKey $RuntimeSourceFeedKey +} +catch { + Write-Host $_.ScriptStackTrace + Write-PipelineTelemetryError -Category 'InitializeToolset' -Message $_ + ExitWithExitCode 1 +} + +ExitWithExitCode 0 diff --git a/eng/common/dotnet-install.sh b/eng/common/dotnet-install.sh new file mode 100755 index 0000000000..abd045a324 --- /dev/null +++ b/eng/common/dotnet-install.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" +# resolve $source until the file is no longer a symlink +while [[ -h "$source" ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +. "$scriptroot/tools.sh" + +version='Latest' +architecture='' +runtime='dotnet' +runtimeSourceFeed='' +runtimeSourceFeedKey='' +while [[ $# > 0 ]]; do + opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" + case "$opt" in + -version|-v) + shift + version="$1" + ;; + -architecture|-a) + shift + architecture="$1" + ;; + -runtime|-r) + shift + runtime="$1" + ;; + -runtimesourcefeed) + shift + runtimeSourceFeed="$1" + ;; + -runtimesourcefeedkey) + shift + runtimeSourceFeedKey="$1" + ;; + *) + Write-PipelineTelemetryError -Category 'Build' -Message "Invalid argument: $1" + exit 1 + ;; + esac + shift +done + +# Use uname to determine what the CPU is, see https://en.wikipedia.org/wiki/Uname#Examples +cpuname=$(uname -m) +case $cpuname in + arm64|aarch64) + buildarch=arm64 + ;; + loongarch64) + buildarch=loongarch64 + ;; + amd64|x86_64) + buildarch=x64 + ;; + armv*l) + buildarch=arm + ;; + i[3-6]86) + buildarch=x86 + ;; + *) + echo "Unknown CPU $cpuname detected, treating it as x64" + buildarch=x64 + ;; +esac + +dotnetRoot="${repo_root}.dotnet" +if [[ $architecture != "" ]] && [[ $architecture != $buildarch ]]; then + dotnetRoot="$dotnetRoot/$architecture" +fi + +InstallDotNet $dotnetRoot $version "$architecture" $runtime true $runtimeSourceFeed $runtimeSourceFeedKey || { + local exit_code=$? + Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "dotnet-install.sh failed (exit code '$exit_code')." >&2 + ExitWithExitCode $exit_code +} + +ExitWithExitCode 0 diff --git a/eng/common/enable-cross-org-publishing.ps1 b/eng/common/enable-cross-org-publishing.ps1 new file mode 100644 index 0000000000..da09da4f1f --- /dev/null +++ b/eng/common/enable-cross-org-publishing.ps1 @@ -0,0 +1,13 @@ +param( + [string] $token +) + + +. $PSScriptRoot\pipeline-logging-functions.ps1 + +# Write-PipelineSetVariable will no-op if a variable named $ci is not defined +# Since this script is only ever called in AzDO builds, just universally set it +$ci = $true + +Write-PipelineSetVariable -Name 'VSS_NUGET_ACCESSTOKEN' -Value $token -IsMultiJobVariable $false +Write-PipelineSetVariable -Name 'VSS_NUGET_URI_PREFIXES' -Value 'https://dnceng.pkgs.visualstudio.com/;https://pkgs.dev.azure.com/dnceng/;https://devdiv.pkgs.visualstudio.com/;https://pkgs.dev.azure.com/devdiv/' -IsMultiJobVariable $false diff --git a/eng/common/generate-locproject.ps1 b/eng/common/generate-locproject.ps1 new file mode 100644 index 0000000000..524aaa57f2 --- /dev/null +++ b/eng/common/generate-locproject.ps1 @@ -0,0 +1,189 @@ +Param( + [Parameter(Mandatory=$true)][string] $SourcesDirectory, # Directory where source files live; if using a Localize directory it should live in here + [string] $LanguageSet = 'VS_Main_Languages', # Language set to be used in the LocProject.json + [switch] $UseCheckedInLocProjectJson, # When set, generates a LocProject.json and compares it to one that already exists in the repo; otherwise just generates one + [switch] $CreateNeutralXlfs # Creates neutral xlf files. Only set to false when running locally +) + +# Generates LocProject.json files for the OneLocBuild task. OneLocBuildTask is described here: +# https://ceapex.visualstudio.com/CEINTL/_wiki/wikis/CEINTL.wiki/107/Localization-with-OneLocBuild-Task + +Set-StrictMode -Version 2.0 +$ErrorActionPreference = "Stop" +. $PSScriptRoot\pipeline-logging-functions.ps1 + +$exclusionsFilePath = "$SourcesDirectory\eng\Localize\LocExclusions.json" +$exclusions = @{ Exclusions = @() } +if (Test-Path -Path $exclusionsFilePath) +{ + $exclusions = Get-Content "$exclusionsFilePath" | ConvertFrom-Json +} + +Push-Location "$SourcesDirectory" # push location for Resolve-Path -Relative to work + +# Template files +$jsonFiles = @() +$jsonTemplateFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory" | Where-Object { $_.FullName -Match "\.template\.config\\localize\\.+\.en\.json" } # .NET templating pattern +$jsonTemplateFiles | ForEach-Object { + $null = $_.Name -Match "(.+)\.[\w-]+\.json" # matches '[filename].[langcode].json + + $destinationFile = "$($_.Directory.FullName)\$($Matches.1).json" + $jsonFiles += Copy-Item "$($_.FullName)" -Destination $destinationFile -PassThru +} + +$jsonWinformsTemplateFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory" | Where-Object { $_.FullName -Match "en\\strings\.json" } # current winforms pattern + +$wxlFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory" | Where-Object { $_.FullName -Match "\\.+\.wxl" -And -Not( $_.Directory.Name -Match "\d{4}" ) } # localized files live in four digit lang ID directories; this excludes them +if (-not $wxlFiles) { + $wxlEnFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory" | Where-Object { $_.FullName -Match "\\1033\\.+\.wxl" } # pick up en files (1033 = en) specifically so we can copy them to use as the neutral xlf files + if ($wxlEnFiles) { + $wxlFiles = @() + $wxlEnFiles | ForEach-Object { + $destinationFile = "$($_.Directory.Parent.FullName)\$($_.Name)" + $wxlFiles += Copy-Item "$($_.FullName)" -Destination $destinationFile -PassThru + } + } +} + +$macosHtmlEnFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory" | Where-Object { $_.FullName -Match "en\.lproj\\.+\.html$" } # add installer HTML files +$macosHtmlFiles = @() +if ($macosHtmlEnFiles) { + $macosHtmlEnFiles | ForEach-Object { + $destinationFile = "$($_.Directory.Parent.FullName)\$($_.Name)" + $macosHtmlFiles += Copy-Item "$($_.FullName)" -Destination $destinationFile -PassThru + } +} + +$xlfFiles = @() + +$allXlfFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory\*\*.xlf" +$langXlfFiles = @() +if ($allXlfFiles) { + $null = $allXlfFiles[0].FullName -Match "\.([\w-]+)\.xlf" # matches '[langcode].xlf' + $firstLangCode = $Matches.1 + $langXlfFiles = Get-ChildItem -Recurse -Path "$SourcesDirectory\*\*.$firstLangCode.xlf" +} +$langXlfFiles | ForEach-Object { + $null = $_.Name -Match "(.+)\.[\w-]+\.xlf" # matches '[filename].[langcode].xlf + + $destinationFile = "$($_.Directory.FullName)\$($Matches.1).xlf" + $xlfFiles += Copy-Item "$($_.FullName)" -Destination $destinationFile -PassThru +} + +$locFiles = $jsonFiles + $jsonWinformsTemplateFiles + $xlfFiles + +$locJson = @{ + Projects = @( + @{ + LanguageSet = $LanguageSet + LocItems = @( + $locFiles | ForEach-Object { + $outputPath = "$(($_.DirectoryName | Resolve-Path -Relative) + "\")" + $continue = $true + foreach ($exclusion in $exclusions.Exclusions) { + if ($_.FullName.Contains($exclusion)) + { + $continue = $false + } + } + $sourceFile = ($_.FullName | Resolve-Path -Relative) + if (!$CreateNeutralXlfs -and $_.Extension -eq '.xlf') { + Remove-Item -Path $sourceFile + } + if ($continue) + { + if ($_.Directory.Name -eq 'en' -and $_.Extension -eq '.json') { + return @{ + SourceFile = $sourceFile + CopyOption = "LangIDOnPath" + OutputPath = "$($_.Directory.Parent.FullName | Resolve-Path -Relative)\" + } + } else { + return @{ + SourceFile = $sourceFile + CopyOption = "LangIDOnName" + OutputPath = $outputPath + } + } + } + } + ) + }, + @{ + LanguageSet = $LanguageSet + CloneLanguageSet = "WiX_CloneLanguages" + LssFiles = @( "wxl_loc.lss" ) + LocItems = @( + $wxlFiles | ForEach-Object { + $outputPath = "$($_.Directory.FullName | Resolve-Path -Relative)\" + $continue = $true + foreach ($exclusion in $exclusions.Exclusions) { + if ($_.FullName.Contains($exclusion)) { + $continue = $false + } + } + $sourceFile = ($_.FullName | Resolve-Path -Relative) + if ($continue) + { + return @{ + SourceFile = $sourceFile + CopyOption = "LangIDOnPath" + OutputPath = $outputPath + } + } + } + ) + }, + @{ + LanguageSet = $LanguageSet + CloneLanguageSet = "VS_macOS_CloneLanguages" + LssFiles = @( ".\eng\common\loc\P22DotNetHtmlLocalization.lss" ) + LocItems = @( + $macosHtmlFiles | ForEach-Object { + $outputPath = "$($_.Directory.FullName | Resolve-Path -Relative)\" + $continue = $true + foreach ($exclusion in $exclusions.Exclusions) { + if ($_.FullName.Contains($exclusion)) { + $continue = $false + } + } + $sourceFile = ($_.FullName | Resolve-Path -Relative) + $lciFile = $sourceFile + ".lci" + if ($continue) { + $result = @{ + SourceFile = $sourceFile + CopyOption = "LangIDOnPath" + OutputPath = $outputPath + } + if (Test-Path $lciFile -PathType Leaf) { + $result["LciFile"] = $lciFile + } + return $result + } + } + ) + } + ) +} + +$json = ConvertTo-Json $locJson -Depth 5 +Write-Host "LocProject.json generated:`n`n$json`n`n" +Pop-Location + +if (!$UseCheckedInLocProjectJson) { + New-Item "$SourcesDirectory\eng\Localize\LocProject.json" -Force # Need this to make sure the Localize directory is created + Set-Content "$SourcesDirectory\eng\Localize\LocProject.json" $json +} +else { + New-Item "$SourcesDirectory\eng\Localize\LocProject-generated.json" -Force # Need this to make sure the Localize directory is created + Set-Content "$SourcesDirectory\eng\Localize\LocProject-generated.json" $json + + if ((Get-FileHash "$SourcesDirectory\eng\Localize\LocProject-generated.json").Hash -ne (Get-FileHash "$SourcesDirectory\eng\Localize\LocProject.json").Hash) { + Write-PipelineTelemetryError -Category "OneLocBuild" -Message "Existing LocProject.json differs from generated LocProject.json. Download LocProject-generated.json and compare them." + + exit 1 + } + else { + Write-Host "Generated LocProject.json and current LocProject.json are identical." + } +} diff --git a/eng/common/generate-sbom-prep.ps1 b/eng/common/generate-sbom-prep.ps1 new file mode 100644 index 0000000000..3e5c1c74a1 --- /dev/null +++ b/eng/common/generate-sbom-prep.ps1 @@ -0,0 +1,21 @@ +Param( + [Parameter(Mandatory=$true)][string] $ManifestDirPath # Manifest directory where sbom will be placed +) + +. $PSScriptRoot\pipeline-logging-functions.ps1 + +Write-Host "Creating dir $ManifestDirPath" +# create directory for sbom manifest to be placed +if (!(Test-Path -path $ManifestDirPath)) +{ + New-Item -ItemType Directory -path $ManifestDirPath + Write-Host "Successfully created directory $ManifestDirPath" +} +else{ + Write-PipelineTelemetryError -category 'Build' "Unable to create sbom folder." +} + +Write-Host "Updating artifact name" +$artifact_name = "${env:SYSTEM_STAGENAME}_${env:AGENT_JOBNAME}_SBOM" -replace '["/:<>\\|?@*"() ]', '_' +Write-Host "Artifact name $artifact_name" +Write-Host "##vso[task.setvariable variable=ARTIFACT_NAME]$artifact_name" diff --git a/eng/common/generate-sbom-prep.sh b/eng/common/generate-sbom-prep.sh new file mode 100755 index 0000000000..d5c76dc827 --- /dev/null +++ b/eng/common/generate-sbom-prep.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" + +# resolve $SOURCE until the file is no longer a symlink +while [[ -h $source ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" +. $scriptroot/pipeline-logging-functions.sh + +manifest_dir=$1 + +if [ ! -d "$manifest_dir" ] ; then + mkdir -p "$manifest_dir" + echo "Sbom directory created." $manifest_dir +else + Write-PipelineTelemetryError -category 'Build' "Unable to create sbom folder." +fi + +artifact_name=$SYSTEM_STAGENAME"_"$AGENT_JOBNAME"_SBOM" +echo "Artifact name before : "$artifact_name +# replace all special characters with _, some builds use special characters like : in Agent.Jobname, that is not a permissible name while uploading artifacts. +safe_artifact_name="${artifact_name//["/:<>\\|?@*$" ]/_}" +echo "Artifact name after : "$safe_artifact_name +export ARTIFACT_NAME=$safe_artifact_name +echo "##vso[task.setvariable variable=ARTIFACT_NAME]$safe_artifact_name" + +exit 0 diff --git a/eng/common/helixpublish.proj b/eng/common/helixpublish.proj new file mode 100644 index 0000000000..d7f185856e --- /dev/null +++ b/eng/common/helixpublish.proj @@ -0,0 +1,26 @@ + + + + msbuild + + + + + %(Identity) + + + + + + $(WorkItemDirectory) + $(WorkItemCommand) + $(WorkItemTimeout) + + + + + + + + + diff --git a/eng/common/init-tools-native.cmd b/eng/common/init-tools-native.cmd new file mode 100644 index 0000000000..438cd548c4 --- /dev/null +++ b/eng/common/init-tools-native.cmd @@ -0,0 +1,3 @@ +@echo off +powershell -NoProfile -NoLogo -ExecutionPolicy ByPass -command "& """%~dp0init-tools-native.ps1""" %*" +exit /b %ErrorLevel% \ No newline at end of file diff --git a/eng/common/init-tools-native.ps1 b/eng/common/init-tools-native.ps1 new file mode 100644 index 0000000000..27ccdb9ecc --- /dev/null +++ b/eng/common/init-tools-native.ps1 @@ -0,0 +1,203 @@ +<# +.SYNOPSIS +Entry point script for installing native tools + +.DESCRIPTION +Reads $RepoRoot\global.json file to determine native assets to install +and executes installers for those tools + +.PARAMETER BaseUri +Base file directory or Url from which to acquire tool archives + +.PARAMETER InstallDirectory +Directory to install native toolset. This is a command-line override for the default +Install directory precedence order: +- InstallDirectory command-line override +- NETCOREENG_INSTALL_DIRECTORY environment variable +- (default) %USERPROFILE%/.netcoreeng/native + +.PARAMETER Clean +Switch specifying to not install anything, but cleanup native asset folders + +.PARAMETER Force +Clean and then install tools + +.PARAMETER DownloadRetries +Total number of retry attempts + +.PARAMETER RetryWaitTimeInSeconds +Wait time between retry attempts in seconds + +.PARAMETER GlobalJsonFile +File path to global.json file + +.PARAMETER PathPromotion +Optional switch to enable either promote native tools specified in the global.json to the path (in Azure Pipelines) +or break the build if a native tool is not found on the path (on a local dev machine) + +.NOTES +#> +[CmdletBinding(PositionalBinding=$false)] +Param ( + [string] $BaseUri = 'https://netcorenativeassets.blob.core.windows.net/resource-packages/external', + [string] $InstallDirectory, + [switch] $Clean = $False, + [switch] $Force = $False, + [int] $DownloadRetries = 5, + [int] $RetryWaitTimeInSeconds = 30, + [string] $GlobalJsonFile, + [switch] $PathPromotion +) + +if (!$GlobalJsonFile) { + $GlobalJsonFile = Join-Path (Get-Item $PSScriptRoot).Parent.Parent.FullName 'global.json' +} + +Set-StrictMode -version 2.0 +$ErrorActionPreference='Stop' + +. $PSScriptRoot\pipeline-logging-functions.ps1 +Import-Module -Name (Join-Path $PSScriptRoot 'native\CommonLibrary.psm1') + +try { + # Define verbose switch if undefined + $Verbose = $VerbosePreference -Eq 'Continue' + + $EngCommonBaseDir = Join-Path $PSScriptRoot 'native\' + $NativeBaseDir = $InstallDirectory + if (!$NativeBaseDir) { + $NativeBaseDir = CommonLibrary\Get-NativeInstallDirectory + } + $Env:CommonLibrary_NativeInstallDir = $NativeBaseDir + $InstallBin = Join-Path $NativeBaseDir 'bin' + $InstallerPath = Join-Path $EngCommonBaseDir 'install-tool.ps1' + + # Process tools list + Write-Host "Processing $GlobalJsonFile" + If (-Not (Test-Path $GlobalJsonFile)) { + Write-Host "Unable to find '$GlobalJsonFile'" + exit 0 + } + $NativeTools = Get-Content($GlobalJsonFile) -Raw | + ConvertFrom-Json | + Select-Object -Expand 'native-tools' -ErrorAction SilentlyContinue + if ($NativeTools) { + if ($PathPromotion -eq $True) { + $ArcadeToolsDirectory = "$env:SYSTEMDRIVE\arcade-tools" + if (Test-Path $ArcadeToolsDirectory) { # if this directory exists, we should use native tools on machine + $NativeTools.PSObject.Properties | ForEach-Object { + $ToolName = $_.Name + $ToolVersion = $_.Value + $InstalledTools = @{} + + if ((Get-Command "$ToolName" -ErrorAction SilentlyContinue) -eq $null) { + if ($ToolVersion -eq "latest") { + $ToolVersion = "" + } + $ToolDirectories = (Get-ChildItem -Path "$ArcadeToolsDirectory" -Filter "$ToolName-$ToolVersion*" | Sort-Object -Descending) + if ($ToolDirectories -eq $null) { + Write-Error "Unable to find directory for $ToolName $ToolVersion; please make sure the tool is installed on this image." + exit 1 + } + $ToolDirectory = $ToolDirectories[0] + $BinPathFile = "$($ToolDirectory.FullName)\binpath.txt" + if (-not (Test-Path -Path "$BinPathFile")) { + Write-Error "Unable to find binpath.txt in '$($ToolDirectory.FullName)' ($ToolName $ToolVersion); artifact is either installed incorrectly or is not a bootstrappable tool." + exit 1 + } + $BinPath = Get-Content "$BinPathFile" + $ToolPath = Convert-Path -Path $BinPath + Write-Host "Adding $ToolName to the path ($ToolPath)..." + Write-Host "##vso[task.prependpath]$ToolPath" + $env:PATH = "$ToolPath;$env:PATH" + $InstalledTools += @{ $ToolName = $ToolDirectory.FullName } + } + } + return $InstalledTools + } else { + $NativeTools.PSObject.Properties | ForEach-Object { + $ToolName = $_.Name + $ToolVersion = $_.Value + + if ((Get-Command "$ToolName" -ErrorAction SilentlyContinue) -eq $null) { + Write-PipelineTelemetryError -Category 'NativeToolsBootstrap' -Message "$ToolName not found on path. Please install $ToolName $ToolVersion before proceeding." + Write-PipelineTelemetryError -Category 'NativeToolsBootstrap' -Message "If this is running on a build machine, the arcade-tools directory was not found, which means there's an error with the image." + } + } + exit 0 + } + } else { + $NativeTools.PSObject.Properties | ForEach-Object { + $ToolName = $_.Name + $ToolVersion = $_.Value + $LocalInstallerArguments = @{ ToolName = "$ToolName" } + $LocalInstallerArguments += @{ InstallPath = "$InstallBin" } + $LocalInstallerArguments += @{ BaseUri = "$BaseUri" } + $LocalInstallerArguments += @{ CommonLibraryDirectory = "$EngCommonBaseDir" } + $LocalInstallerArguments += @{ Version = "$ToolVersion" } + + if ($Verbose) { + $LocalInstallerArguments += @{ Verbose = $True } + } + if (Get-Variable 'Force' -ErrorAction 'SilentlyContinue') { + if($Force) { + $LocalInstallerArguments += @{ Force = $True } + } + } + if ($Clean) { + $LocalInstallerArguments += @{ Clean = $True } + } + + Write-Verbose "Installing $ToolName version $ToolVersion" + Write-Verbose "Executing '$InstallerPath $($LocalInstallerArguments.Keys.ForEach({"-$_ '$($LocalInstallerArguments.$_)'"}) -join ' ')'" + & $InstallerPath @LocalInstallerArguments + if ($LASTEXITCODE -Ne "0") { + $errMsg = "$ToolName installation failed" + if ((Get-Variable 'DoNotAbortNativeToolsInstallationOnFailure' -ErrorAction 'SilentlyContinue') -and $DoNotAbortNativeToolsInstallationOnFailure) { + $showNativeToolsWarning = $true + if ((Get-Variable 'DoNotDisplayNativeToolsInstallationWarnings' -ErrorAction 'SilentlyContinue') -and $DoNotDisplayNativeToolsInstallationWarnings) { + $showNativeToolsWarning = $false + } + if ($showNativeToolsWarning) { + Write-Warning $errMsg + } + $toolInstallationFailure = $true + } else { + # We cannot change this to Write-PipelineTelemetryError because of https://github.com/dotnet/arcade/issues/4482 + Write-Host $errMsg + exit 1 + } + } + } + + if ((Get-Variable 'toolInstallationFailure' -ErrorAction 'SilentlyContinue') -and $toolInstallationFailure) { + # We cannot change this to Write-PipelineTelemetryError because of https://github.com/dotnet/arcade/issues/4482 + Write-Host 'Native tools bootstrap failed' + exit 1 + } + } + } + else { + Write-Host 'No native tools defined in global.json' + exit 0 + } + + if ($Clean) { + exit 0 + } + if (Test-Path $InstallBin) { + Write-Host 'Native tools are available from ' (Convert-Path -Path $InstallBin) + Write-Host "##vso[task.prependpath]$(Convert-Path -Path $InstallBin)" + return $InstallBin + } + elseif (-not ($PathPromotion)) { + Write-PipelineTelemetryError -Category 'NativeToolsBootstrap' -Message 'Native tools install directory does not exist, installation failed' + exit 1 + } + exit 0 +} +catch { + Write-Host $_.ScriptStackTrace + Write-PipelineTelemetryError -Category 'NativeToolsBootstrap' -Message $_ + ExitWithExitCode 1 +} diff --git a/eng/common/init-tools-native.sh b/eng/common/init-tools-native.sh new file mode 100755 index 0000000000..3e6a8d6acf --- /dev/null +++ b/eng/common/init-tools-native.sh @@ -0,0 +1,238 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +base_uri='https://netcorenativeassets.blob.core.windows.net/resource-packages/external' +install_directory='' +clean=false +force=false +download_retries=5 +retry_wait_time_seconds=30 +global_json_file="$(dirname "$(dirname "${scriptroot}")")/global.json" +declare -a native_assets + +. $scriptroot/pipeline-logging-functions.sh +. $scriptroot/native/common-library.sh + +while (($# > 0)); do + lowerI="$(echo $1 | tr "[:upper:]" "[:lower:]")" + case $lowerI in + --baseuri) + base_uri=$2 + shift 2 + ;; + --installdirectory) + install_directory=$2 + shift 2 + ;; + --clean) + clean=true + shift 1 + ;; + --force) + force=true + shift 1 + ;; + --donotabortonfailure) + donotabortonfailure=true + shift 1 + ;; + --donotdisplaywarnings) + donotdisplaywarnings=true + shift 1 + ;; + --downloadretries) + download_retries=$2 + shift 2 + ;; + --retrywaittimeseconds) + retry_wait_time_seconds=$2 + shift 2 + ;; + --help) + echo "Common settings:" + echo " --installdirectory Directory to install native toolset." + echo " This is a command-line override for the default" + echo " Install directory precedence order:" + echo " - InstallDirectory command-line override" + echo " - NETCOREENG_INSTALL_DIRECTORY environment variable" + echo " - (default) %USERPROFILE%/.netcoreeng/native" + echo "" + echo " --clean Switch specifying not to install anything, but cleanup native asset folders" + echo " --donotabortonfailure Switch specifiying whether to abort native tools installation on failure" + echo " --donotdisplaywarnings Switch specifiying whether to display warnings during native tools installation on failure" + echo " --force Clean and then install tools" + echo " --help Print help and exit" + echo "" + echo "Advanced settings:" + echo " --baseuri Base URI for where to download native tools from" + echo " --downloadretries Number of times a download should be attempted" + echo " --retrywaittimeseconds Wait time between download attempts" + echo "" + exit 0 + ;; + esac +done + +function ReadGlobalJsonNativeTools { + # happy path: we have a proper JSON parsing tool `jq(1)` in PATH! + if command -v jq &> /dev/null; then + + # jq: read each key/value pair under "native-tools" entry and emit: + # KEY="" VALUE="" + # followed by a null byte. + # + # bash: read line with null byte delimeter and push to array (for later `eval`uation). + + while IFS= read -rd '' line; do + native_assets+=("$line") + done < <(jq -r '. | + select(has("native-tools")) | + ."native-tools" | + keys[] as $k | + @sh "KEY=\($k) VALUE=\(.[$k])\u0000"' "$global_json_file") + + return + fi + + # Warning: falling back to manually parsing JSON, which is not recommended. + + # Following routine matches the output and escaping logic of jq(1)'s @sh formatter used above. + # It has been tested with several weird strings with escaped characters in entries (key and value) + # and results were compared with the output of jq(1) in binary representation using xxd(1); + # just before the assignment to 'native_assets' array (above and below). + + # try to capture the section under "native-tools". + if [[ ! "$(cat "$global_json_file")" =~ \"native-tools\"[[:space:]\:\{]*([^\}]+) ]]; then + return + fi + + section="${BASH_REMATCH[1]}" + + parseStarted=0 + possibleEnd=0 + escaping=0 + escaped=0 + isKey=1 + + for (( i=0; i<${#section}; i++ )); do + char="${section:$i:1}" + if ! ((parseStarted)) && [[ "$char" =~ [[:space:],:] ]]; then continue; fi + + if ! ((escaping)) && [[ "$char" == "\\" ]]; then + escaping=1 + elif ((escaping)) && ! ((escaped)); then + escaped=1 + fi + + if ! ((parseStarted)) && [[ "$char" == "\"" ]]; then + parseStarted=1 + possibleEnd=0 + elif [[ "$char" == "'" ]]; then + token="$token'\\\''" + possibleEnd=0 + elif ((escaping)) || [[ "$char" != "\"" ]]; then + token="$token$char" + possibleEnd=1 + fi + + if ((possibleEnd)) && ! ((escaping)) && [[ "$char" == "\"" ]]; then + # Use printf to unescape token to match jq(1)'s @sh formatting rules. + # do not use 'token="$(printf "$token")"' syntax, as $() eats the trailing linefeed. + printf -v token "'$token'" + + if ((isKey)); then + KEY="$token" + isKey=0 + else + line="KEY=$KEY VALUE=$token" + native_assets+=("$line") + isKey=1 + fi + + # reset for next token + parseStarted=0 + token= + elif ((escaping)) && ((escaped)); then + escaping=0 + escaped=0 + fi + done +} + +native_base_dir=$install_directory +if [[ -z $install_directory ]]; then + native_base_dir=$(GetNativeInstallDirectory) +fi + +install_bin="${native_base_dir}/bin" +installed_any=false + +ReadGlobalJsonNativeTools + +if [[ ${#native_assets[@]} -eq 0 ]]; then + echo "No native tools defined in global.json" + exit 0; +else + native_installer_dir="$scriptroot/native" + for index in "${!native_assets[@]}"; do + eval "${native_assets["$index"]}" + + installer_path="$native_installer_dir/install-$KEY.sh" + installer_command="$installer_path" + installer_command+=" --baseuri $base_uri" + installer_command+=" --installpath $install_bin" + installer_command+=" --version $VALUE" + echo $installer_command + + if [[ $force = true ]]; then + installer_command+=" --force" + fi + + if [[ $clean = true ]]; then + installer_command+=" --clean" + fi + + if [[ -a $installer_path ]]; then + $installer_command + if [[ $? != 0 ]]; then + if [[ $donotabortonfailure = true ]]; then + if [[ $donotdisplaywarnings != true ]]; then + Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Execution Failed" + fi + else + Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Execution Failed" + exit 1 + fi + else + $installed_any = true + fi + else + if [[ $donotabortonfailure == true ]]; then + if [[ $donotdisplaywarnings != true ]]; then + Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Execution Failed: no install script" + fi + else + Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Execution Failed: no install script" + exit 1 + fi + fi + done +fi + +if [[ $clean = true ]]; then + exit 0 +fi + +if [[ -d $install_bin ]]; then + echo "Native tools are available from $install_bin" + echo "##vso[task.prependpath]$install_bin" +else + if [[ $installed_any = true ]]; then + Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Native tools install directory does not exist, installation failed" + exit 1 + fi +fi + +exit 0 diff --git a/eng/common/internal-feed-operations.ps1 b/eng/common/internal-feed-operations.ps1 new file mode 100644 index 0000000000..92b77347d9 --- /dev/null +++ b/eng/common/internal-feed-operations.ps1 @@ -0,0 +1,132 @@ +param( + [Parameter(Mandatory=$true)][string] $Operation, + [string] $AuthToken, + [string] $CommitSha, + [string] $RepoName, + [switch] $IsFeedPrivate +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 2.0 +. $PSScriptRoot\tools.ps1 + +# Sets VSS_NUGET_EXTERNAL_FEED_ENDPOINTS based on the "darc-int-*" feeds defined in NuGet.config. This is needed +# in build agents by CredProvider to authenticate the restore requests to internal feeds as specified in +# https://github.com/microsoft/artifacts-credprovider/blob/0f53327cd12fd893d8627d7b08a2171bf5852a41/README.md#environment-variables. This should ONLY be called from identified +# internal builds +function SetupCredProvider { + param( + [string] $AuthToken + ) + + # Install the Cred Provider NuGet plugin + Write-Host 'Setting up Cred Provider NuGet plugin in the agent...' + Write-Host "Getting 'installcredprovider.ps1' from 'https://github.com/microsoft/artifacts-credprovider'..." + + $url = 'https://raw.githubusercontent.com/microsoft/artifacts-credprovider/master/helpers/installcredprovider.ps1' + + Write-Host "Writing the contents of 'installcredprovider.ps1' locally..." + Invoke-WebRequest $url -OutFile installcredprovider.ps1 + + Write-Host 'Installing plugin...' + .\installcredprovider.ps1 -Force + + Write-Host "Deleting local copy of 'installcredprovider.ps1'..." + Remove-Item .\installcredprovider.ps1 + + if (-Not("$env:USERPROFILE\.nuget\plugins\netcore")) { + Write-PipelineTelemetryError -Category 'Arcade' -Message 'CredProvider plugin was not installed correctly!' + ExitWithExitCode 1 + } + else { + Write-Host 'CredProvider plugin was installed correctly!' + } + + # Then, we set the 'VSS_NUGET_EXTERNAL_FEED_ENDPOINTS' environment variable to restore from the stable + # feeds successfully + + $nugetConfigPath = Join-Path $RepoRoot "NuGet.config" + + if (-Not (Test-Path -Path $nugetConfigPath)) { + Write-PipelineTelemetryError -Category 'Build' -Message 'NuGet.config file not found in repo root!' + ExitWithExitCode 1 + } + + $endpoints = New-Object System.Collections.ArrayList + $nugetConfigPackageSources = Select-Xml -Path $nugetConfigPath -XPath "//packageSources/add[contains(@key, 'darc-int-')]/@value" | foreach{$_.Node.Value} + + if (($nugetConfigPackageSources | Measure-Object).Count -gt 0 ) { + foreach ($stableRestoreResource in $nugetConfigPackageSources) { + $trimmedResource = ([string]$stableRestoreResource).Trim() + [void]$endpoints.Add(@{endpoint="$trimmedResource"; password="$AuthToken"}) + } + } + + if (($endpoints | Measure-Object).Count -gt 0) { + $endpointCredentials = @{endpointCredentials=$endpoints} | ConvertTo-Json -Compress + + # Create the environment variables the AzDo way + Write-LoggingCommand -Area 'task' -Event 'setvariable' -Data $endpointCredentials -Properties @{ + 'variable' = 'VSS_NUGET_EXTERNAL_FEED_ENDPOINTS' + 'issecret' = 'false' + } + + # We don't want sessions cached since we will be updating the endpoints quite frequently + Write-LoggingCommand -Area 'task' -Event 'setvariable' -Data 'False' -Properties @{ + 'variable' = 'NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED' + 'issecret' = 'false' + } + } + else + { + Write-Host 'No internal endpoints found in NuGet.config' + } +} + +#Workaround for https://github.com/microsoft/msbuild/issues/4430 +function InstallDotNetSdkAndRestoreArcade { + $dotnetTempDir = Join-Path $RepoRoot "dotnet" + $dotnetSdkVersion="2.1.507" # After experimentation we know this version works when restoring the SDK (compared to 3.0.*) + $dotnet = "$dotnetTempDir\dotnet.exe" + $restoreProjPath = "$PSScriptRoot\restore.proj" + + Write-Host "Installing dotnet SDK version $dotnetSdkVersion to restore Arcade SDK..." + InstallDotNetSdk "$dotnetTempDir" "$dotnetSdkVersion" + + '' | Out-File "$restoreProjPath" + + & $dotnet restore $restoreProjPath + + Write-Host 'Arcade SDK restored!' + + if (Test-Path -Path $restoreProjPath) { + Remove-Item $restoreProjPath + } + + if (Test-Path -Path $dotnetTempDir) { + Remove-Item $dotnetTempDir -Recurse + } +} + +try { + Push-Location $PSScriptRoot + + if ($Operation -like 'setup') { + SetupCredProvider $AuthToken + } + elseif ($Operation -like 'install-restore') { + InstallDotNetSdkAndRestoreArcade + } + else { + Write-PipelineTelemetryError -Category 'Arcade' -Message "Unknown operation '$Operation'!" + ExitWithExitCode 1 + } +} +catch { + Write-Host $_.ScriptStackTrace + Write-PipelineTelemetryError -Category 'Arcade' -Message $_ + ExitWithExitCode 1 +} +finally { + Pop-Location +} diff --git a/eng/common/internal-feed-operations.sh b/eng/common/internal-feed-operations.sh new file mode 100755 index 0000000000..9378223ba0 --- /dev/null +++ b/eng/common/internal-feed-operations.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash + +set -e + +# Sets VSS_NUGET_EXTERNAL_FEED_ENDPOINTS based on the "darc-int-*" feeds defined in NuGet.config. This is needed +# in build agents by CredProvider to authenticate the restore requests to internal feeds as specified in +# https://github.com/microsoft/artifacts-credprovider/blob/0f53327cd12fd893d8627d7b08a2171bf5852a41/README.md#environment-variables. +# This should ONLY be called from identified internal builds +function SetupCredProvider { + local authToken=$1 + + # Install the Cred Provider NuGet plugin + echo "Setting up Cred Provider NuGet plugin in the agent..."... + echo "Getting 'installcredprovider.ps1' from 'https://github.com/microsoft/artifacts-credprovider'..." + + local url="https://raw.githubusercontent.com/microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh" + + echo "Writing the contents of 'installcredprovider.ps1' locally..." + local installcredproviderPath="installcredprovider.sh" + if command -v curl > /dev/null; then + curl $url > "$installcredproviderPath" + else + wget -q -O "$installcredproviderPath" "$url" + fi + + echo "Installing plugin..." + . "$installcredproviderPath" + + echo "Deleting local copy of 'installcredprovider.sh'..." + rm installcredprovider.sh + + if [ ! -d "$HOME/.nuget/plugins" ]; then + Write-PipelineTelemetryError -category 'Build' 'CredProvider plugin was not installed correctly!' + ExitWithExitCode 1 + else + echo "CredProvider plugin was installed correctly!" + fi + + # Then, we set the 'VSS_NUGET_EXTERNAL_FEED_ENDPOINTS' environment variable to restore from the stable + # feeds successfully + + local nugetConfigPath="{$repo_root}NuGet.config" + + if [ ! "$nugetConfigPath" ]; then + Write-PipelineTelemetryError -category 'Build' "NuGet.config file not found in repo's root!" + ExitWithExitCode 1 + fi + + local endpoints='[' + local nugetConfigPackageValues=`cat "$nugetConfigPath" | grep "key=\"darc-int-"` + local pattern="value=\"(.*)\"" + + for value in $nugetConfigPackageValues + do + if [[ $value =~ $pattern ]]; then + local endpoint="${BASH_REMATCH[1]}" + endpoints+="{\"endpoint\": \"$endpoint\", \"password\": \"$authToken\"}," + fi + done + + endpoints=${endpoints%?} + endpoints+=']' + + if [ ${#endpoints} -gt 2 ]; then + local endpointCredentials="{\"endpointCredentials\": "$endpoints"}" + + echo "##vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$endpointCredentials" + echo "##vso[task.setvariable variable=NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED]False" + else + echo "No internal endpoints found in NuGet.config" + fi +} + +# Workaround for https://github.com/microsoft/msbuild/issues/4430 +function InstallDotNetSdkAndRestoreArcade { + local dotnetTempDir="$repo_root/dotnet" + local dotnetSdkVersion="2.1.507" # After experimentation we know this version works when restoring the SDK (compared to 3.0.*) + local restoreProjPath="$repo_root/eng/common/restore.proj" + + echo "Installing dotnet SDK version $dotnetSdkVersion to restore Arcade SDK..." + echo "" > "$restoreProjPath" + + InstallDotNetSdk "$dotnetTempDir" "$dotnetSdkVersion" + + local res=`$dotnetTempDir/dotnet restore $restoreProjPath` + echo "Arcade SDK restored!" + + # Cleanup + if [ "$restoreProjPath" ]; then + rm "$restoreProjPath" + fi + + if [ "$dotnetTempDir" ]; then + rm -r $dotnetTempDir + fi +} + +source="${BASH_SOURCE[0]}" +operation='' +authToken='' +repoName='' + +while [[ $# > 0 ]]; do + opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" + case "$opt" in + --operation) + operation=$2 + shift + ;; + --authtoken) + authToken=$2 + shift + ;; + *) + echo "Invalid argument: $1" + usage + exit 1 + ;; + esac + + shift +done + +while [[ -h "$source" ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +. "$scriptroot/tools.sh" + +if [ "$operation" = "setup" ]; then + SetupCredProvider $authToken +elif [ "$operation" = "install-restore" ]; then + InstallDotNetSdkAndRestoreArcade +else + echo "Unknown operation '$operation'!" +fi diff --git a/eng/common/internal/Directory.Build.props b/eng/common/internal/Directory.Build.props new file mode 100644 index 0000000000..dbf99d82a5 --- /dev/null +++ b/eng/common/internal/Directory.Build.props @@ -0,0 +1,4 @@ + + + + diff --git a/eng/common/internal/NuGet.config b/eng/common/internal/NuGet.config new file mode 100644 index 0000000000..19d3d311b1 --- /dev/null +++ b/eng/common/internal/NuGet.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/eng/common/internal/Tools.csproj b/eng/common/internal/Tools.csproj new file mode 100644 index 0000000000..7f5ce6d608 --- /dev/null +++ b/eng/common/internal/Tools.csproj @@ -0,0 +1,30 @@ + + + + net472 + false + false + + + + + + + + + + + + + + https://devdiv.pkgs.visualstudio.com/_packaging/dotnet-core-internal-tooling/nuget/v3/index.json; + + + $(RestoreSources); + https://devdiv.pkgs.visualstudio.com/_packaging/VS/nuget/v3/index.json; + + + + + + diff --git a/eng/common/loc/P22DotNetHtmlLocalization.lss b/eng/common/loc/P22DotNetHtmlLocalization.lss new file mode 100644 index 0000000000000000000000000000000000000000..6661fed566e49b0c206665bc21f135e06c9b89c4 GIT binary patch literal 3810 zcmd^CT~8BH5S?ce|HG9Bo&v?0;P_m6l= znU-wb=}VLT7UH{>{5H;0M4iLmRE8QeBRr|>&k=uV*L;heKY+dq>B$0^=0I}|x`)s{ zht4t9zaiEh5Fe>E-;zVT;c4G?v;9N0G=rWwo~*(Cs`OS6ZL`HL;vsL0J@I%$+0YvE zx{9ukK|FtFx1PlPD5M;6ZM>f;1BhCf?`8y6QH*?RT9T>XwF z#~m_N+i^UKE^j{e;KdNW`kH9Rbj{G8tDY}mafCgG+m3H`I@_PhrDmcIzxD&IX@s083kV|lLUE^0(h6wWRPN0QN1n^PU5eX8r6OZ*s^g)tt77#SZCB}znxye#U$Dtinr6lnVu z!LzA{A}0~no7p$thFGJAnI}oSW||9H=Bz}I7kD#2MLg7WfrlE5o9sQjePc>qmv+6iQCmdp(y}(Vr literal 0 HcmV?d00001 diff --git a/eng/common/msbuild.ps1 b/eng/common/msbuild.ps1 new file mode 100644 index 0000000000..f041e5ddd9 --- /dev/null +++ b/eng/common/msbuild.ps1 @@ -0,0 +1,28 @@ +[CmdletBinding(PositionalBinding=$false)] +Param( + [string] $verbosity = 'minimal', + [bool] $warnAsError = $true, + [bool] $nodeReuse = $true, + [switch] $ci, + [switch] $prepareMachine, + [switch] $excludePrereleaseVS, + [string] $msbuildEngine = $null, + [Parameter(ValueFromRemainingArguments=$true)][String[]]$extraArgs +) + +. $PSScriptRoot\tools.ps1 + +try { + if ($ci) { + $nodeReuse = $false + } + + MSBuild @extraArgs +} +catch { + Write-Host $_.ScriptStackTrace + Write-PipelineTelemetryError -Category 'Build' -Message $_ + ExitWithExitCode 1 +} + +ExitWithExitCode 0 \ No newline at end of file diff --git a/eng/common/msbuild.sh b/eng/common/msbuild.sh new file mode 100755 index 0000000000..20d3dad543 --- /dev/null +++ b/eng/common/msbuild.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" + +# resolve $source until the file is no longer a symlink +while [[ -h "$source" ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +verbosity='minimal' +warn_as_error=true +node_reuse=true +prepare_machine=false +extra_args='' + +while (($# > 0)); do + lowerI="$(echo $1 | tr "[:upper:]" "[:lower:]")" + case $lowerI in + --verbosity) + verbosity=$2 + shift 2 + ;; + --warnaserror) + warn_as_error=$2 + shift 2 + ;; + --nodereuse) + node_reuse=$2 + shift 2 + ;; + --ci) + ci=true + shift 1 + ;; + --preparemachine) + prepare_machine=true + shift 1 + ;; + *) + extra_args="$extra_args $1" + shift 1 + ;; + esac +done + +. "$scriptroot/tools.sh" + +if [[ "$ci" == true ]]; then + node_reuse=false +fi + +MSBuild $extra_args +ExitWithExitCode 0 diff --git a/eng/common/native/CommonLibrary.psm1 b/eng/common/native/CommonLibrary.psm1 new file mode 100644 index 0000000000..ca38268c44 --- /dev/null +++ b/eng/common/native/CommonLibrary.psm1 @@ -0,0 +1,400 @@ +<# +.SYNOPSIS +Helper module to install an archive to a directory + +.DESCRIPTION +Helper module to download and extract an archive to a specified directory + +.PARAMETER Uri +Uri of artifact to download + +.PARAMETER InstallDirectory +Directory to extract artifact contents to + +.PARAMETER Force +Force download / extraction if file or contents already exist. Default = False + +.PARAMETER DownloadRetries +Total number of retry attempts. Default = 5 + +.PARAMETER RetryWaitTimeInSeconds +Wait time between retry attempts in seconds. Default = 30 + +.NOTES +Returns False if download or extraction fail, True otherwise +#> +function DownloadAndExtract { + [CmdletBinding(PositionalBinding=$false)] + Param ( + [Parameter(Mandatory=$True)] + [string] $Uri, + [Parameter(Mandatory=$True)] + [string] $InstallDirectory, + [switch] $Force = $False, + [int] $DownloadRetries = 5, + [int] $RetryWaitTimeInSeconds = 30 + ) + # Define verbose switch if undefined + $Verbose = $VerbosePreference -Eq "Continue" + + $TempToolPath = CommonLibrary\Get-TempPathFilename -Path $Uri + + # Download native tool + $DownloadStatus = CommonLibrary\Get-File -Uri $Uri ` + -Path $TempToolPath ` + -DownloadRetries $DownloadRetries ` + -RetryWaitTimeInSeconds $RetryWaitTimeInSeconds ` + -Force:$Force ` + -Verbose:$Verbose + + if ($DownloadStatus -Eq $False) { + Write-Error "Download failed from $Uri" + return $False + } + + # Extract native tool + $UnzipStatus = CommonLibrary\Expand-Zip -ZipPath $TempToolPath ` + -OutputDirectory $InstallDirectory ` + -Force:$Force ` + -Verbose:$Verbose + + if ($UnzipStatus -Eq $False) { + # Retry Download one more time with Force=true + $DownloadRetryStatus = CommonLibrary\Get-File -Uri $Uri ` + -Path $TempToolPath ` + -DownloadRetries 1 ` + -RetryWaitTimeInSeconds $RetryWaitTimeInSeconds ` + -Force:$True ` + -Verbose:$Verbose + + if ($DownloadRetryStatus -Eq $False) { + Write-Error "Last attempt of download failed as well" + return $False + } + + # Retry unzip again one more time with Force=true + $UnzipRetryStatus = CommonLibrary\Expand-Zip -ZipPath $TempToolPath ` + -OutputDirectory $InstallDirectory ` + -Force:$True ` + -Verbose:$Verbose + if ($UnzipRetryStatus -Eq $False) + { + Write-Error "Last attempt of unzip failed as well" + # Clean up partial zips and extracts + if (Test-Path $TempToolPath) { + Remove-Item $TempToolPath -Force + } + if (Test-Path $InstallDirectory) { + Remove-Item $InstallDirectory -Force -Recurse + } + return $False + } + } + + return $True +} + +<# +.SYNOPSIS +Download a file, retry on failure + +.DESCRIPTION +Download specified file and retry if attempt fails + +.PARAMETER Uri +Uri of file to download. If Uri is a local path, the file will be copied instead of downloaded + +.PARAMETER Path +Path to download or copy uri file to + +.PARAMETER Force +Overwrite existing file if present. Default = False + +.PARAMETER DownloadRetries +Total number of retry attempts. Default = 5 + +.PARAMETER RetryWaitTimeInSeconds +Wait time between retry attempts in seconds Default = 30 + +#> +function Get-File { + [CmdletBinding(PositionalBinding=$false)] + Param ( + [Parameter(Mandatory=$True)] + [string] $Uri, + [Parameter(Mandatory=$True)] + [string] $Path, + [int] $DownloadRetries = 5, + [int] $RetryWaitTimeInSeconds = 30, + [switch] $Force = $False + ) + $Attempt = 0 + + if ($Force) { + if (Test-Path $Path) { + Remove-Item $Path -Force + } + } + if (Test-Path $Path) { + Write-Host "File '$Path' already exists, skipping download" + return $True + } + + $DownloadDirectory = Split-Path -ErrorAction Ignore -Path "$Path" -Parent + if (-Not (Test-Path $DownloadDirectory)) { + New-Item -path $DownloadDirectory -force -itemType "Directory" | Out-Null + } + + $TempPath = "$Path.tmp" + if (Test-Path -IsValid -Path $Uri) { + Write-Verbose "'$Uri' is a file path, copying temporarily to '$TempPath'" + Copy-Item -Path $Uri -Destination $TempPath + Write-Verbose "Moving temporary file to '$Path'" + Move-Item -Path $TempPath -Destination $Path + return $? + } + else { + Write-Verbose "Downloading $Uri" + # Don't display the console progress UI - it's a huge perf hit + $ProgressPreference = 'SilentlyContinue' + while($Attempt -Lt $DownloadRetries) + { + try { + Invoke-WebRequest -UseBasicParsing -Uri $Uri -OutFile $TempPath + Write-Verbose "Downloaded to temporary location '$TempPath'" + Move-Item -Path $TempPath -Destination $Path + Write-Verbose "Moved temporary file to '$Path'" + return $True + } + catch { + $Attempt++ + if ($Attempt -Lt $DownloadRetries) { + $AttemptsLeft = $DownloadRetries - $Attempt + Write-Warning "Download failed, $AttemptsLeft attempts remaining, will retry in $RetryWaitTimeInSeconds seconds" + Start-Sleep -Seconds $RetryWaitTimeInSeconds + } + else { + Write-Error $_ + Write-Error $_.Exception + } + } + } + } + + return $False +} + +<# +.SYNOPSIS +Generate a shim for a native tool + +.DESCRIPTION +Creates a wrapper script (shim) that passes arguments forward to native tool assembly + +.PARAMETER ShimName +The name of the shim + +.PARAMETER ShimDirectory +The directory where shims are stored + +.PARAMETER ToolFilePath +Path to file that shim forwards to + +.PARAMETER Force +Replace shim if already present. Default = False + +.NOTES +Returns $True if generating shim succeeds, $False otherwise +#> +function New-ScriptShim { + [CmdletBinding(PositionalBinding=$false)] + Param ( + [Parameter(Mandatory=$True)] + [string] $ShimName, + [Parameter(Mandatory=$True)] + [string] $ShimDirectory, + [Parameter(Mandatory=$True)] + [string] $ToolFilePath, + [Parameter(Mandatory=$True)] + [string] $BaseUri, + [switch] $Force + ) + try { + Write-Verbose "Generating '$ShimName' shim" + + if (-Not (Test-Path $ToolFilePath)){ + Write-Error "Specified tool file path '$ToolFilePath' does not exist" + return $False + } + + # WinShimmer is a small .NET Framework program that creates .exe shims to bootstrapped programs + # Many of the checks for installed programs expect a .exe extension for Windows tools, rather + # than a .bat or .cmd file. + # Source: https://github.com/dotnet/arcade/tree/master/src/WinShimmer + if (-Not (Test-Path "$ShimDirectory\WinShimmer\winshimmer.exe")) { + $InstallStatus = DownloadAndExtract -Uri "$BaseUri/windows/winshimmer/WinShimmer.zip" ` + -InstallDirectory $ShimDirectory\WinShimmer ` + -Force:$Force ` + -DownloadRetries 2 ` + -RetryWaitTimeInSeconds 5 ` + -Verbose:$Verbose + } + + if ((Test-Path (Join-Path $ShimDirectory "$ShimName.exe"))) { + Write-Host "$ShimName.exe already exists; replacing..." + Remove-Item (Join-Path $ShimDirectory "$ShimName.exe") + } + + & "$ShimDirectory\WinShimmer\winshimmer.exe" $ShimName $ToolFilePath $ShimDirectory + return $True + } + catch { + Write-Host $_ + Write-Host $_.Exception + return $False + } +} + +<# +.SYNOPSIS +Returns the machine architecture of the host machine + +.NOTES +Returns 'x64' on 64 bit machines + Returns 'x86' on 32 bit machines +#> +function Get-MachineArchitecture { + $ProcessorArchitecture = $Env:PROCESSOR_ARCHITECTURE + $ProcessorArchitectureW6432 = $Env:PROCESSOR_ARCHITEW6432 + if($ProcessorArchitecture -Eq "X86") + { + if(($ProcessorArchitectureW6432 -Eq "") -Or + ($ProcessorArchitectureW6432 -Eq "X86")) { + return "x86" + } + $ProcessorArchitecture = $ProcessorArchitectureW6432 + } + if (($ProcessorArchitecture -Eq "AMD64") -Or + ($ProcessorArchitecture -Eq "IA64") -Or + ($ProcessorArchitecture -Eq "ARM64") -Or + ($ProcessorArchitecture -Eq "LOONGARCH64")) { + return "x64" + } + return "x86" +} + +<# +.SYNOPSIS +Get the name of a temporary folder under the native install directory +#> +function Get-TempDirectory { + return Join-Path (Get-NativeInstallDirectory) "temp/" +} + +function Get-TempPathFilename { + [CmdletBinding(PositionalBinding=$false)] + Param ( + [Parameter(Mandatory=$True)] + [string] $Path + ) + $TempDir = CommonLibrary\Get-TempDirectory + $TempFilename = Split-Path $Path -leaf + $TempPath = Join-Path $TempDir $TempFilename + return $TempPath +} + +<# +.SYNOPSIS +Returns the base directory to use for native tool installation + +.NOTES +Returns the value of the NETCOREENG_INSTALL_DIRECTORY if that environment variable +is set, or otherwise returns an install directory under the %USERPROFILE% +#> +function Get-NativeInstallDirectory { + $InstallDir = $Env:NETCOREENG_INSTALL_DIRECTORY + if (!$InstallDir) { + $InstallDir = Join-Path $Env:USERPROFILE ".netcoreeng/native/" + } + return $InstallDir +} + +<# +.SYNOPSIS +Unzip an archive + +.DESCRIPTION +Powershell module to unzip an archive to a specified directory + +.PARAMETER ZipPath (Required) +Path to archive to unzip + +.PARAMETER OutputDirectory (Required) +Output directory for archive contents + +.PARAMETER Force +Overwrite output directory contents if they already exist + +.NOTES +- Returns True and does not perform an extraction if output directory already exists but Overwrite is not True. +- Returns True if unzip operation is successful +- Returns False if Overwrite is True and it is unable to remove contents of OutputDirectory +- Returns False if unable to extract zip archive +#> +function Expand-Zip { + [CmdletBinding(PositionalBinding=$false)] + Param ( + [Parameter(Mandatory=$True)] + [string] $ZipPath, + [Parameter(Mandatory=$True)] + [string] $OutputDirectory, + [switch] $Force + ) + + Write-Verbose "Extracting '$ZipPath' to '$OutputDirectory'" + try { + if ((Test-Path $OutputDirectory) -And (-Not $Force)) { + Write-Host "Directory '$OutputDirectory' already exists, skipping extract" + return $True + } + if (Test-Path $OutputDirectory) { + Write-Verbose "'Force' is 'True', but '$OutputDirectory' exists, removing directory" + Remove-Item $OutputDirectory -Force -Recurse + if ($? -Eq $False) { + Write-Error "Unable to remove '$OutputDirectory'" + return $False + } + } + + $TempOutputDirectory = Join-Path "$(Split-Path -Parent $OutputDirectory)" "$(Split-Path -Leaf $OutputDirectory).tmp" + if (Test-Path $TempOutputDirectory) { + Remove-Item $TempOutputDirectory -Force -Recurse + } + New-Item -Path $TempOutputDirectory -Force -ItemType "Directory" | Out-Null + + Add-Type -assembly "system.io.compression.filesystem" + [io.compression.zipfile]::ExtractToDirectory("$ZipPath", "$TempOutputDirectory") + if ($? -Eq $False) { + Write-Error "Unable to extract '$ZipPath'" + return $False + } + + Move-Item -Path $TempOutputDirectory -Destination $OutputDirectory + } + catch { + Write-Host $_ + Write-Host $_.Exception + + return $False + } + return $True +} + +export-modulemember -function DownloadAndExtract +export-modulemember -function Expand-Zip +export-modulemember -function Get-File +export-modulemember -function Get-MachineArchitecture +export-modulemember -function Get-NativeInstallDirectory +export-modulemember -function Get-TempDirectory +export-modulemember -function Get-TempPathFilename +export-modulemember -function New-ScriptShim diff --git a/eng/common/native/common-library.sh b/eng/common/native/common-library.sh new file mode 100755 index 0000000000..080c2c283a --- /dev/null +++ b/eng/common/native/common-library.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash + +function GetNativeInstallDirectory { + local install_dir + + if [[ -z $NETCOREENG_INSTALL_DIRECTORY ]]; then + install_dir=$HOME/.netcoreeng/native/ + else + install_dir=$NETCOREENG_INSTALL_DIRECTORY + fi + + echo $install_dir + return 0 +} + +function GetTempDirectory { + + echo $(GetNativeInstallDirectory)temp/ + return 0 +} + +function ExpandZip { + local zip_path=$1 + local output_directory=$2 + local force=${3:-false} + + echo "Extracting $zip_path to $output_directory" + if [[ -d $output_directory ]] && [[ $force = false ]]; then + echo "Directory '$output_directory' already exists, skipping extract" + return 0 + fi + + if [[ -d $output_directory ]]; then + echo "'Force flag enabled, but '$output_directory' exists. Removing directory" + rm -rf $output_directory + if [[ $? != 0 ]]; then + Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Unable to remove '$output_directory'" + return 1 + fi + fi + + echo "Creating directory: '$output_directory'" + mkdir -p $output_directory + + echo "Extracting archive" + tar -xf $zip_path -C $output_directory + if [[ $? != 0 ]]; then + Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Unable to extract '$zip_path'" + return 1 + fi + + return 0 +} + +function GetCurrentOS { + local unameOut="$(uname -s)" + case $unameOut in + Linux*) echo "Linux";; + Darwin*) echo "MacOS";; + esac + return 0 +} + +function GetFile { + local uri=$1 + local path=$2 + local force=${3:-false} + local download_retries=${4:-5} + local retry_wait_time_seconds=${5:-30} + + if [[ -f $path ]]; then + if [[ $force = false ]]; then + echo "File '$path' already exists. Skipping download" + return 0 + else + rm -rf $path + fi + fi + + if [[ -f $uri ]]; then + echo "'$uri' is a file path, copying file to '$path'" + cp $uri $path + return $? + fi + + echo "Downloading $uri" + # Use curl if available, otherwise use wget + if command -v curl > /dev/null; then + curl "$uri" -sSL --retry $download_retries --retry-delay $retry_wait_time_seconds --create-dirs -o "$path" --fail + else + wget -q -O "$path" "$uri" --tries="$download_retries" + fi + + return $? +} + +function GetTempPathFileName { + local path=$1 + + local temp_dir=$(GetTempDirectory) + local temp_file_name=$(basename $path) + echo $temp_dir$temp_file_name + return 0 +} + +function DownloadAndExtract { + local uri=$1 + local installDir=$2 + local force=${3:-false} + local download_retries=${4:-5} + local retry_wait_time_seconds=${5:-30} + + local temp_tool_path=$(GetTempPathFileName $uri) + + echo "downloading to: $temp_tool_path" + + # Download file + GetFile "$uri" "$temp_tool_path" $force $download_retries $retry_wait_time_seconds + if [[ $? != 0 ]]; then + Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Failed to download '$uri' to '$temp_tool_path'." + return 1 + fi + + # Extract File + echo "extracting from $temp_tool_path to $installDir" + ExpandZip "$temp_tool_path" "$installDir" $force $download_retries $retry_wait_time_seconds + if [[ $? != 0 ]]; then + Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Failed to extract '$temp_tool_path' to '$installDir'." + return 1 + fi + + return 0 +} + +function NewScriptShim { + local shimpath=$1 + local tool_file_path=$2 + local force=${3:-false} + + echo "Generating '$shimpath' shim" + if [[ -f $shimpath ]]; then + if [[ $force = false ]]; then + echo "File '$shimpath' already exists." >&2 + return 1 + else + rm -rf $shimpath + fi + fi + + if [[ ! -f $tool_file_path ]]; then + # try to see if the path is lower cased + tool_file_path="$(echo $tool_file_path | tr "[:upper:]" "[:lower:]")" + if [[ ! -f $tool_file_path ]]; then + Write-PipelineTelemetryError -category 'NativeToolsBootstrap' "Specified tool file path:'$tool_file_path' does not exist" + return 1 + fi + fi + + local shim_contents=$'#!/usr/bin/env bash\n' + shim_contents+="SHIMARGS="$'$1\n' + shim_contents+="$tool_file_path"$' $SHIMARGS\n' + + # Write shim file + echo "$shim_contents" > $shimpath + + chmod +x $shimpath + + echo "Finished generating shim '$shimpath'" + + return $? +} + diff --git a/eng/common/native/init-compiler.sh b/eng/common/native/init-compiler.sh new file mode 100755 index 0000000000..517401b688 --- /dev/null +++ b/eng/common/native/init-compiler.sh @@ -0,0 +1,137 @@ +#!/bin/sh +# +# This file detects the C/C++ compiler and exports it to the CC/CXX environment variables +# +# NOTE: some scripts source this file and rely on stdout being empty, make sure to not output anything here! + +if [ -z "$build_arch" ] || [ -z "$compiler" ]; then + echo "Usage..." + echo "build_arch= compiler= init-compiler.sh" + echo "Specify the target architecture." + echo "Specify the name of compiler (clang or gcc)." + exit 1 +fi + +case "$compiler" in + clang*|-clang*|--clang*) + # clangx.y or clang-x.y + version="$(echo "$compiler" | tr -d '[:alpha:]-=')" + majorVersion="${version%%.*}" + [ -z "${version##*.*}" ] && minorVersion="${version#*.}" + + if [ -z "$minorVersion" ] && [ -n "$majorVersion" ] && [ "$majorVersion" -le 6 ]; then + minorVersion=0; + fi + compiler=clang + ;; + + gcc*|-gcc*|--gcc*) + # gccx.y or gcc-x.y + version="$(echo "$compiler" | tr -d '[:alpha:]-=')" + majorVersion="${version%%.*}" + [ -z "${version##*.*}" ] && minorVersion="${version#*.}" + compiler=gcc + ;; +esac + +cxxCompiler="$compiler++" + +# clear the existing CC and CXX from environment +CC= +CXX= +LDFLAGS= + +if [ "$compiler" = "gcc" ]; then cxxCompiler="g++"; fi + +check_version_exists() { + desired_version=-1 + + # Set up the environment to be used for building with the desired compiler. + if command -v "$compiler-$1.$2" > /dev/null; then + desired_version="-$1.$2" + elif command -v "$compiler$1$2" > /dev/null; then + desired_version="$1$2" + elif command -v "$compiler-$1$2" > /dev/null; then + desired_version="-$1$2" + fi + + echo "$desired_version" +} + +if [ -z "$CLR_CC" ]; then + + # Set default versions + if [ -z "$majorVersion" ]; then + # note: gcc (all versions) and clang versions higher than 6 do not have minor version in file name, if it is zero. + if [ "$compiler" = "clang" ]; then versions="16 15 14 13 12 11 10 9 8 7 6.0 5.0 4.0 3.9 3.8 3.7 3.6 3.5" + elif [ "$compiler" = "gcc" ]; then versions="13 12 11 10 9 8 7 6 5 4.9"; fi + + for version in $versions; do + _major="${version%%.*}" + [ -z "${version##*.*}" ] && _minor="${version#*.}" + desired_version="$(check_version_exists "$_major" "$_minor")" + if [ "$desired_version" != "-1" ]; then majorVersion="$_major"; break; fi + done + + if [ -z "$majorVersion" ]; then + if command -v "$compiler" > /dev/null; then + if [ "$(uname)" != "Darwin" ]; then + echo "Warning: Specific version of $compiler not found, falling back to use the one in PATH." + fi + CC="$(command -v "$compiler")" + CXX="$(command -v "$cxxCompiler")" + else + echo "No usable version of $compiler found." + exit 1 + fi + else + if [ "$compiler" = "clang" ] && [ "$majorVersion" -lt 5 ]; then + if [ "$build_arch" = "arm" ] || [ "$build_arch" = "armel" ]; then + if command -v "$compiler" > /dev/null; then + echo "Warning: Found clang version $majorVersion which is not supported on arm/armel architectures, falling back to use clang from PATH." + CC="$(command -v "$compiler")" + CXX="$(command -v "$cxxCompiler")" + else + echo "Found clang version $majorVersion which is not supported on arm/armel architectures, and there is no clang in PATH." + exit 1 + fi + fi + fi + fi + else + desired_version="$(check_version_exists "$majorVersion" "$minorVersion")" + if [ "$desired_version" = "-1" ]; then + echo "Could not find specific version of $compiler: $majorVersion $minorVersion." + exit 1 + fi + fi + + if [ -z "$CC" ]; then + CC="$(command -v "$compiler$desired_version")" + CXX="$(command -v "$cxxCompiler$desired_version")" + if [ -z "$CXX" ]; then CXX="$(command -v "$cxxCompiler")"; fi + fi +else + if [ ! -f "$CLR_CC" ]; then + echo "CLR_CC is set but path '$CLR_CC' does not exist" + exit 1 + fi + CC="$CLR_CC" + CXX="$CLR_CXX" +fi + +if [ -z "$CC" ]; then + echo "Unable to find $compiler." + exit 1 +fi + +# Only lld version >= 9 can be considered stable. lld doesn't support s390x. +if [ "$compiler" = "clang" ] && [ -n "$majorVersion" ] && [ "$majorVersion" -ge 9 ] && [ "$build_arch" != "s390x" ]; then + if "$CC" -fuse-ld=lld -Wl,--version >/dev/null 2>&1; then + LDFLAGS="-fuse-ld=lld" + fi +fi + +SCAN_BUILD_COMMAND="$(command -v "scan-build$desired_version")" + +export CC CXX LDFLAGS SCAN_BUILD_COMMAND diff --git a/eng/common/native/install-cmake-test.sh b/eng/common/native/install-cmake-test.sh new file mode 100755 index 0000000000..8a5e7cf0db --- /dev/null +++ b/eng/common/native/install-cmake-test.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +. $scriptroot/common-library.sh + +base_uri= +install_path= +version= +clean=false +force=false +download_retries=5 +retry_wait_time_seconds=30 + +while (($# > 0)); do + lowerI="$(echo $1 | tr "[:upper:]" "[:lower:]")" + case $lowerI in + --baseuri) + base_uri=$2 + shift 2 + ;; + --installpath) + install_path=$2 + shift 2 + ;; + --version) + version=$2 + shift 2 + ;; + --clean) + clean=true + shift 1 + ;; + --force) + force=true + shift 1 + ;; + --downloadretries) + download_retries=$2 + shift 2 + ;; + --retrywaittimeseconds) + retry_wait_time_seconds=$2 + shift 2 + ;; + --help) + echo "Common settings:" + echo " --baseuri Base file directory or Url wrom which to acquire tool archives" + echo " --installpath Base directory to install native tool to" + echo " --clean Don't install the tool, just clean up the current install of the tool" + echo " --force Force install of tools even if they previously exist" + echo " --help Print help and exit" + echo "" + echo "Advanced settings:" + echo " --downloadretries Total number of retry attempts" + echo " --retrywaittimeseconds Wait time between retry attempts in seconds" + echo "" + exit 0 + ;; + esac +done + +tool_name="cmake-test" +tool_os=$(GetCurrentOS) +tool_folder="$(echo $tool_os | tr "[:upper:]" "[:lower:]")" +tool_arch="x86_64" +tool_name_moniker="$tool_name-$version-$tool_os-$tool_arch" +tool_install_directory="$install_path/$tool_name/$version" +tool_file_path="$tool_install_directory/$tool_name_moniker/bin/$tool_name" +shim_path="$install_path/$tool_name.sh" +uri="${base_uri}/$tool_folder/$tool_name/$tool_name_moniker.tar.gz" + +# Clean up tool and installers +if [[ $clean = true ]]; then + echo "Cleaning $tool_install_directory" + if [[ -d $tool_install_directory ]]; then + rm -rf $tool_install_directory + fi + + echo "Cleaning $shim_path" + if [[ -f $shim_path ]]; then + rm -rf $shim_path + fi + + tool_temp_path=$(GetTempPathFileName $uri) + echo "Cleaning $tool_temp_path" + if [[ -f $tool_temp_path ]]; then + rm -rf $tool_temp_path + fi + + exit 0 +fi + +# Install tool +if [[ -f $tool_file_path ]] && [[ $force = false ]]; then + echo "$tool_name ($version) already exists, skipping install" + exit 0 +fi + +DownloadAndExtract $uri $tool_install_directory $force $download_retries $retry_wait_time_seconds + +if [[ $? != 0 ]]; then + Write-PipelineTelemetryError -category 'NativeToolsBootstrap' 'Installation failed' + exit 1 +fi + +# Generate Shim +# Always rewrite shims so that we are referencing the expected version +NewScriptShim $shim_path $tool_file_path true + +if [[ $? != 0 ]]; then + Write-PipelineTelemetryError -category 'NativeToolsBootstrap' 'Shim generation failed' + exit 1 +fi + +exit 0 diff --git a/eng/common/native/install-cmake.sh b/eng/common/native/install-cmake.sh new file mode 100755 index 0000000000..de496beebc --- /dev/null +++ b/eng/common/native/install-cmake.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +. $scriptroot/common-library.sh + +base_uri= +install_path= +version= +clean=false +force=false +download_retries=5 +retry_wait_time_seconds=30 + +while (($# > 0)); do + lowerI="$(echo $1 | tr "[:upper:]" "[:lower:]")" + case $lowerI in + --baseuri) + base_uri=$2 + shift 2 + ;; + --installpath) + install_path=$2 + shift 2 + ;; + --version) + version=$2 + shift 2 + ;; + --clean) + clean=true + shift 1 + ;; + --force) + force=true + shift 1 + ;; + --downloadretries) + download_retries=$2 + shift 2 + ;; + --retrywaittimeseconds) + retry_wait_time_seconds=$2 + shift 2 + ;; + --help) + echo "Common settings:" + echo " --baseuri Base file directory or Url wrom which to acquire tool archives" + echo " --installpath Base directory to install native tool to" + echo " --clean Don't install the tool, just clean up the current install of the tool" + echo " --force Force install of tools even if they previously exist" + echo " --help Print help and exit" + echo "" + echo "Advanced settings:" + echo " --downloadretries Total number of retry attempts" + echo " --retrywaittimeseconds Wait time between retry attempts in seconds" + echo "" + exit 0 + ;; + esac +done + +tool_name="cmake" +tool_os=$(GetCurrentOS) +tool_folder="$(echo $tool_os | tr "[:upper:]" "[:lower:]")" +tool_arch="x86_64" +tool_name_moniker="$tool_name-$version-$tool_os-$tool_arch" +tool_install_directory="$install_path/$tool_name/$version" +tool_file_path="$tool_install_directory/$tool_name_moniker/bin/$tool_name" +shim_path="$install_path/$tool_name.sh" +uri="${base_uri}/$tool_folder/$tool_name/$tool_name_moniker.tar.gz" + +# Clean up tool and installers +if [[ $clean = true ]]; then + echo "Cleaning $tool_install_directory" + if [[ -d $tool_install_directory ]]; then + rm -rf $tool_install_directory + fi + + echo "Cleaning $shim_path" + if [[ -f $shim_path ]]; then + rm -rf $shim_path + fi + + tool_temp_path=$(GetTempPathFileName $uri) + echo "Cleaning $tool_temp_path" + if [[ -f $tool_temp_path ]]; then + rm -rf $tool_temp_path + fi + + exit 0 +fi + +# Install tool +if [[ -f $tool_file_path ]] && [[ $force = false ]]; then + echo "$tool_name ($version) already exists, skipping install" + exit 0 +fi + +DownloadAndExtract $uri $tool_install_directory $force $download_retries $retry_wait_time_seconds + +if [[ $? != 0 ]]; then + Write-PipelineTelemetryError -category 'NativeToolsBootstrap' 'Installation failed' + exit 1 +fi + +# Generate Shim +# Always rewrite shims so that we are referencing the expected version +NewScriptShim $shim_path $tool_file_path true + +if [[ $? != 0 ]]; then + Write-PipelineTelemetryError -category 'NativeToolsBootstrap' 'Shim generation failed' + exit 1 +fi + +exit 0 diff --git a/eng/common/native/install-tool.ps1 b/eng/common/native/install-tool.ps1 new file mode 100644 index 0000000000..78f2d84a4e --- /dev/null +++ b/eng/common/native/install-tool.ps1 @@ -0,0 +1,132 @@ +<# +.SYNOPSIS +Install native tool + +.DESCRIPTION +Install cmake native tool from Azure blob storage + +.PARAMETER InstallPath +Base directory to install native tool to + +.PARAMETER BaseUri +Base file directory or Url from which to acquire tool archives + +.PARAMETER CommonLibraryDirectory +Path to folder containing common library modules + +.PARAMETER Force +Force install of tools even if they previously exist + +.PARAMETER Clean +Don't install the tool, just clean up the current install of the tool + +.PARAMETER DownloadRetries +Total number of retry attempts + +.PARAMETER RetryWaitTimeInSeconds +Wait time between retry attempts in seconds + +.NOTES +Returns 0 if install succeeds, 1 otherwise +#> +[CmdletBinding(PositionalBinding=$false)] +Param ( + [Parameter(Mandatory=$True)] + [string] $ToolName, + [Parameter(Mandatory=$True)] + [string] $InstallPath, + [Parameter(Mandatory=$True)] + [string] $BaseUri, + [Parameter(Mandatory=$True)] + [string] $Version, + [string] $CommonLibraryDirectory = $PSScriptRoot, + [switch] $Force = $False, + [switch] $Clean = $False, + [int] $DownloadRetries = 5, + [int] $RetryWaitTimeInSeconds = 30 +) + +. $PSScriptRoot\..\pipeline-logging-functions.ps1 + +# Import common library modules +Import-Module -Name (Join-Path $CommonLibraryDirectory "CommonLibrary.psm1") + +try { + # Define verbose switch if undefined + $Verbose = $VerbosePreference -Eq "Continue" + + $Arch = CommonLibrary\Get-MachineArchitecture + $ToolOs = "win64" + if($Arch -Eq "x32") { + $ToolOs = "win32" + } + $ToolNameMoniker = "$ToolName-$Version-$ToolOs-$Arch" + $ToolInstallDirectory = Join-Path $InstallPath "$ToolName\$Version\" + $Uri = "$BaseUri/windows/$ToolName/$ToolNameMoniker.zip" + $ShimPath = Join-Path $InstallPath "$ToolName.exe" + + if ($Clean) { + Write-Host "Cleaning $ToolInstallDirectory" + if (Test-Path $ToolInstallDirectory) { + Remove-Item $ToolInstallDirectory -Force -Recurse + } + Write-Host "Cleaning $ShimPath" + if (Test-Path $ShimPath) { + Remove-Item $ShimPath -Force + } + $ToolTempPath = CommonLibrary\Get-TempPathFilename -Path $Uri + Write-Host "Cleaning $ToolTempPath" + if (Test-Path $ToolTempPath) { + Remove-Item $ToolTempPath -Force + } + exit 0 + } + + # Install tool + if ((Test-Path $ToolInstallDirectory) -And (-Not $Force)) { + Write-Verbose "$ToolName ($Version) already exists, skipping install" + } + else { + $InstallStatus = CommonLibrary\DownloadAndExtract -Uri $Uri ` + -InstallDirectory $ToolInstallDirectory ` + -Force:$Force ` + -DownloadRetries $DownloadRetries ` + -RetryWaitTimeInSeconds $RetryWaitTimeInSeconds ` + -Verbose:$Verbose + + if ($InstallStatus -Eq $False) { + Write-PipelineTelemetryError "Installation failed" -Category "NativeToolsetBootstrapping" + exit 1 + } + } + + $ToolFilePath = Get-ChildItem $ToolInstallDirectory -Recurse -Filter "$ToolName.exe" | % { $_.FullName } + if (@($ToolFilePath).Length -Gt 1) { + Write-Error "There are multiple copies of $ToolName in $($ToolInstallDirectory): `n$(@($ToolFilePath | out-string))" + exit 1 + } elseif (@($ToolFilePath).Length -Lt 1) { + Write-Host "$ToolName was not found in $ToolInstallDirectory." + exit 1 + } + + # Generate shim + # Always rewrite shims so that we are referencing the expected version + $GenerateShimStatus = CommonLibrary\New-ScriptShim -ShimName $ToolName ` + -ShimDirectory $InstallPath ` + -ToolFilePath "$ToolFilePath" ` + -BaseUri $BaseUri ` + -Force:$Force ` + -Verbose:$Verbose + + if ($GenerateShimStatus -Eq $False) { + Write-PipelineTelemetryError "Generate shim failed" -Category "NativeToolsetBootstrapping" + return 1 + } + + exit 0 +} +catch { + Write-Host $_.ScriptStackTrace + Write-PipelineTelemetryError -Category "NativeToolsetBootstrapping" -Message $_ + exit 1 +} diff --git a/eng/common/pipeline-logging-functions.ps1 b/eng/common/pipeline-logging-functions.ps1 new file mode 100644 index 0000000000..8e422c561e --- /dev/null +++ b/eng/common/pipeline-logging-functions.ps1 @@ -0,0 +1,260 @@ +# Source for this file was taken from https://github.com/microsoft/azure-pipelines-task-lib/blob/11c9439d4af17e6475d9fe058e6b2e03914d17e6/powershell/VstsTaskSdk/LoggingCommandFunctions.ps1 and modified. + +# NOTE: You should not be calling these method directly as they are likely to change. Instead you should be calling the Write-Pipeline* functions defined in tools.ps1 + +$script:loggingCommandPrefix = '##vso[' +$script:loggingCommandEscapeMappings = @( # TODO: WHAT ABOUT "="? WHAT ABOUT "%"? + New-Object psobject -Property @{ Token = ';' ; Replacement = '%3B' } + New-Object psobject -Property @{ Token = "`r" ; Replacement = '%0D' } + New-Object psobject -Property @{ Token = "`n" ; Replacement = '%0A' } + New-Object psobject -Property @{ Token = "]" ; Replacement = '%5D' } +) +# TODO: BUG: Escape % ??? +# TODO: Add test to verify don't need to escape "=". + +# Specify "-Force" to force pipeline formatted output even if "$ci" is false or not set +function Write-PipelineTelemetryError { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Category, + [Parameter(Mandatory = $true)] + [string]$Message, + [Parameter(Mandatory = $false)] + [string]$Type = 'error', + [string]$ErrCode, + [string]$SourcePath, + [string]$LineNumber, + [string]$ColumnNumber, + [switch]$AsOutput, + [switch]$Force) + + $PSBoundParameters.Remove('Category') | Out-Null + + if ($Force -Or ((Test-Path variable:ci) -And $ci)) { + $Message = "(NETCORE_ENGINEERING_TELEMETRY=$Category) $Message" + } + $PSBoundParameters.Remove('Message') | Out-Null + $PSBoundParameters.Add('Message', $Message) + Write-PipelineTaskError @PSBoundParameters +} + +# Specify "-Force" to force pipeline formatted output even if "$ci" is false or not set +function Write-PipelineTaskError { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Message, + [Parameter(Mandatory = $false)] + [string]$Type = 'error', + [string]$ErrCode, + [string]$SourcePath, + [string]$LineNumber, + [string]$ColumnNumber, + [switch]$AsOutput, + [switch]$Force + ) + + if (!$Force -And (-Not (Test-Path variable:ci) -Or !$ci)) { + if ($Type -eq 'error') { + Write-Host $Message -ForegroundColor Red + return + } + elseif ($Type -eq 'warning') { + Write-Host $Message -ForegroundColor Yellow + return + } + } + + if (($Type -ne 'error') -and ($Type -ne 'warning')) { + Write-Host $Message + return + } + $PSBoundParameters.Remove('Force') | Out-Null + if (-not $PSBoundParameters.ContainsKey('Type')) { + $PSBoundParameters.Add('Type', 'error') + } + Write-LogIssue @PSBoundParameters +} + +function Write-PipelineSetVariable { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Name, + [string]$Value, + [switch]$Secret, + [switch]$AsOutput, + [bool]$IsMultiJobVariable = $true) + + if ((Test-Path variable:ci) -And $ci) { + Write-LoggingCommand -Area 'task' -Event 'setvariable' -Data $Value -Properties @{ + 'variable' = $Name + 'isSecret' = $Secret + 'isOutput' = $IsMultiJobVariable + } -AsOutput:$AsOutput + } +} + +function Write-PipelinePrependPath { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Path, + [switch]$AsOutput) + + if ((Test-Path variable:ci) -And $ci) { + Write-LoggingCommand -Area 'task' -Event 'prependpath' -Data $Path -AsOutput:$AsOutput + } +} + +function Write-PipelineSetResult { + [CmdletBinding()] + param( + [ValidateSet("Succeeded", "SucceededWithIssues", "Failed", "Cancelled", "Skipped")] + [Parameter(Mandatory = $true)] + [string]$Result, + [string]$Message) + if ((Test-Path variable:ci) -And $ci) { + Write-LoggingCommand -Area 'task' -Event 'complete' -Data $Message -Properties @{ + 'result' = $Result + } + } +} + +<######################################## +# Private functions. +########################################> +function Format-LoggingCommandData { + [CmdletBinding()] + param([string]$Value, [switch]$Reverse) + + if (!$Value) { + return '' + } + + if (!$Reverse) { + foreach ($mapping in $script:loggingCommandEscapeMappings) { + $Value = $Value.Replace($mapping.Token, $mapping.Replacement) + } + } + else { + for ($i = $script:loggingCommandEscapeMappings.Length - 1 ; $i -ge 0 ; $i--) { + $mapping = $script:loggingCommandEscapeMappings[$i] + $Value = $Value.Replace($mapping.Replacement, $mapping.Token) + } + } + + return $Value +} + +function Format-LoggingCommand { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Area, + [Parameter(Mandatory = $true)] + [string]$Event, + [string]$Data, + [hashtable]$Properties) + + # Append the preamble. + [System.Text.StringBuilder]$sb = New-Object -TypeName System.Text.StringBuilder + $null = $sb.Append($script:loggingCommandPrefix).Append($Area).Append('.').Append($Event) + + # Append the properties. + if ($Properties) { + $first = $true + foreach ($key in $Properties.Keys) { + [string]$value = Format-LoggingCommandData $Properties[$key] + if ($value) { + if ($first) { + $null = $sb.Append(' ') + $first = $false + } + else { + $null = $sb.Append(';') + } + + $null = $sb.Append("$key=$value") + } + } + } + + # Append the tail and output the value. + $Data = Format-LoggingCommandData $Data + $sb.Append(']').Append($Data).ToString() +} + +function Write-LoggingCommand { + [CmdletBinding(DefaultParameterSetName = 'Parameters')] + param( + [Parameter(Mandatory = $true, ParameterSetName = 'Parameters')] + [string]$Area, + [Parameter(Mandatory = $true, ParameterSetName = 'Parameters')] + [string]$Event, + [Parameter(ParameterSetName = 'Parameters')] + [string]$Data, + [Parameter(ParameterSetName = 'Parameters')] + [hashtable]$Properties, + [Parameter(Mandatory = $true, ParameterSetName = 'Object')] + $Command, + [switch]$AsOutput) + + if ($PSCmdlet.ParameterSetName -eq 'Object') { + Write-LoggingCommand -Area $Command.Area -Event $Command.Event -Data $Command.Data -Properties $Command.Properties -AsOutput:$AsOutput + return + } + + $command = Format-LoggingCommand -Area $Area -Event $Event -Data $Data -Properties $Properties + if ($AsOutput) { + $command + } + else { + Write-Host $command + } +} + +function Write-LogIssue { + [CmdletBinding()] + param( + [ValidateSet('warning', 'error')] + [Parameter(Mandatory = $true)] + [string]$Type, + [string]$Message, + [string]$ErrCode, + [string]$SourcePath, + [string]$LineNumber, + [string]$ColumnNumber, + [switch]$AsOutput) + + $command = Format-LoggingCommand -Area 'task' -Event 'logissue' -Data $Message -Properties @{ + 'type' = $Type + 'code' = $ErrCode + 'sourcepath' = $SourcePath + 'linenumber' = $LineNumber + 'columnnumber' = $ColumnNumber + } + if ($AsOutput) { + return $command + } + + if ($Type -eq 'error') { + $foregroundColor = $host.PrivateData.ErrorForegroundColor + $backgroundColor = $host.PrivateData.ErrorBackgroundColor + if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { + $foregroundColor = [System.ConsoleColor]::Red + $backgroundColor = [System.ConsoleColor]::Black + } + } + else { + $foregroundColor = $host.PrivateData.WarningForegroundColor + $backgroundColor = $host.PrivateData.WarningBackgroundColor + if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { + $foregroundColor = [System.ConsoleColor]::Yellow + $backgroundColor = [System.ConsoleColor]::Black + } + } + + Write-Host $command -ForegroundColor $foregroundColor -BackgroundColor $backgroundColor +} diff --git a/eng/common/pipeline-logging-functions.sh b/eng/common/pipeline-logging-functions.sh new file mode 100755 index 0000000000..6a0b2255e9 --- /dev/null +++ b/eng/common/pipeline-logging-functions.sh @@ -0,0 +1,206 @@ +#!/usr/bin/env bash + +function Write-PipelineTelemetryError { + local telemetry_category='' + local force=false + local function_args=() + local message='' + while [[ $# -gt 0 ]]; do + opt="$(echo "${1/#--/-}" | tr "[:upper:]" "[:lower:]")" + case "$opt" in + -category|-c) + telemetry_category=$2 + shift + ;; + -force|-f) + force=true + ;; + -*) + function_args+=("$1 $2") + shift + ;; + *) + message=$* + ;; + esac + shift + done + + if [[ $force != true ]] && [[ "$ci" != true ]]; then + echo "$message" >&2 + return + fi + + if [[ $force == true ]]; then + function_args+=("-force") + fi + message="(NETCORE_ENGINEERING_TELEMETRY=$telemetry_category) $message" + function_args+=("$message") + Write-PipelineTaskError ${function_args[@]} +} + +function Write-PipelineTaskError { + local message_type="error" + local sourcepath='' + local linenumber='' + local columnnumber='' + local error_code='' + local force=false + + while [[ $# -gt 0 ]]; do + opt="$(echo "${1/#--/-}" | tr "[:upper:]" "[:lower:]")" + case "$opt" in + -type|-t) + message_type=$2 + shift + ;; + -sourcepath|-s) + sourcepath=$2 + shift + ;; + -linenumber|-ln) + linenumber=$2 + shift + ;; + -columnnumber|-cn) + columnnumber=$2 + shift + ;; + -errcode|-e) + error_code=$2 + shift + ;; + -force|-f) + force=true + ;; + *) + break + ;; + esac + + shift + done + + if [[ $force != true ]] && [[ "$ci" != true ]]; then + echo "$@" >&2 + return + fi + + local message="##vso[task.logissue" + + message="$message type=$message_type" + + if [ -n "$sourcepath" ]; then + message="$message;sourcepath=$sourcepath" + fi + + if [ -n "$linenumber" ]; then + message="$message;linenumber=$linenumber" + fi + + if [ -n "$columnnumber" ]; then + message="$message;columnnumber=$columnnumber" + fi + + if [ -n "$error_code" ]; then + message="$message;code=$error_code" + fi + + message="$message]$*" + echo "$message" +} + +function Write-PipelineSetVariable { + if [[ "$ci" != true ]]; then + return + fi + + local name='' + local value='' + local secret=false + local as_output=false + local is_multi_job_variable=true + + while [[ $# -gt 0 ]]; do + opt="$(echo "${1/#--/-}" | tr "[:upper:]" "[:lower:]")" + case "$opt" in + -name|-n) + name=$2 + shift + ;; + -value|-v) + value=$2 + shift + ;; + -secret|-s) + secret=true + ;; + -as_output|-a) + as_output=true + ;; + -is_multi_job_variable|-i) + is_multi_job_variable=$2 + shift + ;; + esac + shift + done + + value=${value/;/%3B} + value=${value/\\r/%0D} + value=${value/\\n/%0A} + value=${value/]/%5D} + + local message="##vso[task.setvariable variable=$name;isSecret=$secret;isOutput=$is_multi_job_variable]$value" + + if [[ "$as_output" == true ]]; then + $message + else + echo "$message" + fi +} + +function Write-PipelinePrependPath { + local prepend_path='' + + while [[ $# -gt 0 ]]; do + opt="$(echo "${1/#--/-}" | tr "[:upper:]" "[:lower:]")" + case "$opt" in + -path|-p) + prepend_path=$2 + shift + ;; + esac + shift + done + + export PATH="$prepend_path:$PATH" + + if [[ "$ci" == true ]]; then + echo "##vso[task.prependpath]$prepend_path" + fi +} + +function Write-PipelineSetResult { + local result='' + local message='' + + while [[ $# -gt 0 ]]; do + opt="$(echo "${1/#--/-}" | tr "[:upper:]" "[:lower:]")" + case "$opt" in + -result|-r) + result=$2 + shift + ;; + -message|-m) + message=$2 + shift + ;; + esac + shift + done + + if [[ "$ci" == true ]]; then + echo "##vso[task.complete result=$result;]$message" + fi +} diff --git a/eng/common/post-build/add-build-to-channel.ps1 b/eng/common/post-build/add-build-to-channel.ps1 new file mode 100644 index 0000000000..de2d957922 --- /dev/null +++ b/eng/common/post-build/add-build-to-channel.ps1 @@ -0,0 +1,48 @@ +param( + [Parameter(Mandatory=$true)][int] $BuildId, + [Parameter(Mandatory=$true)][int] $ChannelId, + [Parameter(Mandatory=$true)][string] $MaestroApiAccessToken, + [Parameter(Mandatory=$false)][string] $MaestroApiEndPoint = 'https://maestro-prod.westus2.cloudapp.azure.com', + [Parameter(Mandatory=$false)][string] $MaestroApiVersion = '2019-01-16' +) + +try { + . $PSScriptRoot\post-build-utils.ps1 + + # Check that the channel we are going to promote the build to exist + $channelInfo = Get-MaestroChannel -ChannelId $ChannelId + + if (!$channelInfo) { + Write-PipelineTelemetryCategory -Category 'PromoteBuild' -Message "Channel with BAR ID $ChannelId was not found in BAR!" + ExitWithExitCode 1 + } + + # Get info about which channel(s) the build has already been promoted to + $buildInfo = Get-MaestroBuild -BuildId $BuildId + + if (!$buildInfo) { + Write-PipelineTelemetryError -Category 'PromoteBuild' -Message "Build with BAR ID $BuildId was not found in BAR!" + ExitWithExitCode 1 + } + + # Find whether the build is already assigned to the channel or not + if ($buildInfo.channels) { + foreach ($channel in $buildInfo.channels) { + if ($channel.Id -eq $ChannelId) { + Write-Host "The build with BAR ID $BuildId is already on channel $ChannelId!" + ExitWithExitCode 0 + } + } + } + + Write-Host "Promoting build '$BuildId' to channel '$ChannelId'." + + Assign-BuildToChannel -BuildId $BuildId -ChannelId $ChannelId + + Write-Host 'done.' +} +catch { + Write-Host $_ + Write-PipelineTelemetryError -Category 'PromoteBuild' -Message "There was an error while trying to promote build '$BuildId' to channel '$ChannelId'" + ExitWithExitCode 1 +} diff --git a/eng/common/post-build/check-channel-consistency.ps1 b/eng/common/post-build/check-channel-consistency.ps1 new file mode 100644 index 0000000000..63f3464c98 --- /dev/null +++ b/eng/common/post-build/check-channel-consistency.ps1 @@ -0,0 +1,40 @@ +param( + [Parameter(Mandatory=$true)][string] $PromoteToChannels, # List of channels that the build should be promoted to + [Parameter(Mandatory=$true)][array] $AvailableChannelIds # List of channel IDs available in the YAML implementation +) + +try { + . $PSScriptRoot\post-build-utils.ps1 + + if ($PromoteToChannels -eq "") { + Write-PipelineTaskError -Type 'warning' -Message "This build won't publish assets as it's not configured to any Maestro channel. If that wasn't intended use Darc to configure a default channel using add-default-channel for this branch or to promote it to a channel using add-build-to-channel. See https://github.com/dotnet/arcade/blob/master/Documentation/Darc.md#assigning-an-individual-build-to-a-channel for more info." + ExitWithExitCode 0 + } + + # Check that every channel that Maestro told to promote the build to + # is available in YAML + $PromoteToChannelsIds = $PromoteToChannels -split "\D" | Where-Object { $_ } + + $hasErrors = $false + + foreach ($id in $PromoteToChannelsIds) { + if (($id -ne 0) -and ($id -notin $AvailableChannelIds)) { + Write-PipelineTaskError -Message "Channel $id is not present in the post-build YAML configuration! This is an error scenario. Please contact @dnceng." + $hasErrors = $true + } + } + + # The `Write-PipelineTaskError` doesn't error the script and we might report several errors + # in the previous lines. The check below makes sure that we return an error state from the + # script if we reported any validation error + if ($hasErrors) { + ExitWithExitCode 1 + } + + Write-Host 'done.' +} +catch { + Write-Host $_ + Write-PipelineTelemetryError -Category 'CheckChannelConsistency' -Message "There was an error while trying to check consistency of Maestro default channels for the build and post-build YAML configuration." + ExitWithExitCode 1 +} diff --git a/eng/common/post-build/nuget-validation.ps1 b/eng/common/post-build/nuget-validation.ps1 new file mode 100644 index 0000000000..dab3534ab5 --- /dev/null +++ b/eng/common/post-build/nuget-validation.ps1 @@ -0,0 +1,24 @@ +# This script validates NuGet package metadata information using this +# tool: https://github.com/NuGet/NuGetGallery/tree/jver-verify/src/VerifyMicrosoftPackage + +param( + [Parameter(Mandatory=$true)][string] $PackagesPath, # Path to where the packages to be validated are + [Parameter(Mandatory=$true)][string] $ToolDestinationPath # Where the validation tool should be downloaded to +) + +try { + . $PSScriptRoot\post-build-utils.ps1 + + $url = 'https://raw.githubusercontent.com/NuGet/NuGetGallery/3e25ad135146676bcab0050a516939d9958bfa5d/src/VerifyMicrosoftPackage/verify.ps1' + + New-Item -ItemType 'directory' -Path ${ToolDestinationPath} -Force + + Invoke-WebRequest $url -OutFile ${ToolDestinationPath}\verify.ps1 + + & ${ToolDestinationPath}\verify.ps1 ${PackagesPath}\*.nupkg +} +catch { + Write-Host $_.ScriptStackTrace + Write-PipelineTelemetryError -Category 'NuGetValidation' -Message $_ + ExitWithExitCode 1 +} diff --git a/eng/common/post-build/post-build-utils.ps1 b/eng/common/post-build/post-build-utils.ps1 new file mode 100644 index 0000000000..534f6988d5 --- /dev/null +++ b/eng/common/post-build/post-build-utils.ps1 @@ -0,0 +1,91 @@ +# Most of the functions in this file require the variables `MaestroApiEndPoint`, +# `MaestroApiVersion` and `MaestroApiAccessToken` to be globally available. + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 2.0 + +# `tools.ps1` checks $ci to perform some actions. Since the post-build +# scripts don't necessarily execute in the same agent that run the +# build.ps1/sh script this variable isn't automatically set. +$ci = $true +$disableConfigureToolsetImport = $true +. $PSScriptRoot\..\tools.ps1 + +function Create-MaestroApiRequestHeaders([string]$ContentType = 'application/json') { + Validate-MaestroVars + + $headers = New-Object 'System.Collections.Generic.Dictionary[[String],[String]]' + $headers.Add('Accept', $ContentType) + $headers.Add('Authorization',"Bearer $MaestroApiAccessToken") + return $headers +} + +function Get-MaestroChannel([int]$ChannelId) { + Validate-MaestroVars + + $apiHeaders = Create-MaestroApiRequestHeaders + $apiEndpoint = "$MaestroApiEndPoint/api/channels/${ChannelId}?api-version=$MaestroApiVersion" + + $result = try { Invoke-WebRequest -Method Get -Uri $apiEndpoint -Headers $apiHeaders | ConvertFrom-Json } catch { Write-Host "Error: $_" } + return $result +} + +function Get-MaestroBuild([int]$BuildId) { + Validate-MaestroVars + + $apiHeaders = Create-MaestroApiRequestHeaders -AuthToken $MaestroApiAccessToken + $apiEndpoint = "$MaestroApiEndPoint/api/builds/${BuildId}?api-version=$MaestroApiVersion" + + $result = try { return Invoke-WebRequest -Method Get -Uri $apiEndpoint -Headers $apiHeaders | ConvertFrom-Json } catch { Write-Host "Error: $_" } + return $result +} + +function Get-MaestroSubscriptions([string]$SourceRepository, [int]$ChannelId) { + Validate-MaestroVars + + $SourceRepository = [System.Web.HttpUtility]::UrlEncode($SourceRepository) + $apiHeaders = Create-MaestroApiRequestHeaders -AuthToken $MaestroApiAccessToken + $apiEndpoint = "$MaestroApiEndPoint/api/subscriptions?sourceRepository=$SourceRepository&channelId=$ChannelId&api-version=$MaestroApiVersion" + + $result = try { Invoke-WebRequest -Method Get -Uri $apiEndpoint -Headers $apiHeaders | ConvertFrom-Json } catch { Write-Host "Error: $_" } + return $result +} + +function Assign-BuildToChannel([int]$BuildId, [int]$ChannelId) { + Validate-MaestroVars + + $apiHeaders = Create-MaestroApiRequestHeaders -AuthToken $MaestroApiAccessToken + $apiEndpoint = "$MaestroApiEndPoint/api/channels/${ChannelId}/builds/${BuildId}?api-version=$MaestroApiVersion" + Invoke-WebRequest -Method Post -Uri $apiEndpoint -Headers $apiHeaders | Out-Null +} + +function Trigger-Subscription([string]$SubscriptionId) { + Validate-MaestroVars + + $apiHeaders = Create-MaestroApiRequestHeaders -AuthToken $MaestroApiAccessToken + $apiEndpoint = "$MaestroApiEndPoint/api/subscriptions/$SubscriptionId/trigger?api-version=$MaestroApiVersion" + Invoke-WebRequest -Uri $apiEndpoint -Headers $apiHeaders -Method Post | Out-Null +} + +function Validate-MaestroVars { + try { + Get-Variable MaestroApiEndPoint | Out-Null + Get-Variable MaestroApiVersion | Out-Null + Get-Variable MaestroApiAccessToken | Out-Null + + if (!($MaestroApiEndPoint -Match '^http[s]?://maestro-(int|prod).westus2.cloudapp.azure.com$')) { + Write-PipelineTelemetryError -Category 'MaestroVars' -Message "MaestroApiEndPoint is not a valid Maestro URL. '$MaestroApiEndPoint'" + ExitWithExitCode 1 + } + + if (!($MaestroApiVersion -Match '^[0-9]{4}-[0-9]{2}-[0-9]{2}$')) { + Write-PipelineTelemetryError -Category 'MaestroVars' -Message "MaestroApiVersion does not match a version string in the format yyyy-MM-DD. '$MaestroApiVersion'" + ExitWithExitCode 1 + } + } + catch { + Write-PipelineTelemetryError -Category 'MaestroVars' -Message 'Error: Variables `MaestroApiEndPoint`, `MaestroApiVersion` and `MaestroApiAccessToken` are required while using this script.' + Write-Host $_ + ExitWithExitCode 1 + } +} diff --git a/eng/common/post-build/publish-using-darc.ps1 b/eng/common/post-build/publish-using-darc.ps1 new file mode 100644 index 0000000000..8508397d77 --- /dev/null +++ b/eng/common/post-build/publish-using-darc.ps1 @@ -0,0 +1,54 @@ +param( + [Parameter(Mandatory=$true)][int] $BuildId, + [Parameter(Mandatory=$true)][int] $PublishingInfraVersion, + [Parameter(Mandatory=$true)][string] $AzdoToken, + [Parameter(Mandatory=$true)][string] $MaestroToken, + [Parameter(Mandatory=$false)][string] $MaestroApiEndPoint = 'https://maestro-prod.westus2.cloudapp.azure.com', + [Parameter(Mandatory=$true)][string] $WaitPublishingFinish, + [Parameter(Mandatory=$false)][string] $ArtifactsPublishingAdditionalParameters, + [Parameter(Mandatory=$false)][string] $SymbolPublishingAdditionalParameters +) + +try { + . $PSScriptRoot\post-build-utils.ps1 + + $darc = Get-Darc + + $optionalParams = [System.Collections.ArrayList]::new() + + if ("" -ne $ArtifactsPublishingAdditionalParameters) { + $optionalParams.Add("--artifact-publishing-parameters") | Out-Null + $optionalParams.Add($ArtifactsPublishingAdditionalParameters) | Out-Null + } + + if ("" -ne $SymbolPublishingAdditionalParameters) { + $optionalParams.Add("--symbol-publishing-parameters") | Out-Null + $optionalParams.Add($SymbolPublishingAdditionalParameters) | Out-Null + } + + if ("false" -eq $WaitPublishingFinish) { + $optionalParams.Add("--no-wait") | Out-Null + } + + & $darc add-build-to-channel ` + --id $buildId ` + --publishing-infra-version $PublishingInfraVersion ` + --default-channels ` + --source-branch main ` + --azdev-pat $AzdoToken ` + --bar-uri $MaestroApiEndPoint ` + --password $MaestroToken ` + @optionalParams + + if ($LastExitCode -ne 0) { + Write-Host "Problems using Darc to promote build ${buildId} to default channels. Stopping execution..." + exit 1 + } + + Write-Host 'done.' +} +catch { + Write-Host $_ + Write-PipelineTelemetryError -Category 'PromoteBuild' -Message "There was an error while trying to publish build '$BuildId' to default channels." + ExitWithExitCode 1 +} diff --git a/eng/common/post-build/sourcelink-validation.ps1 b/eng/common/post-build/sourcelink-validation.ps1 new file mode 100644 index 0000000000..4011d324e7 --- /dev/null +++ b/eng/common/post-build/sourcelink-validation.ps1 @@ -0,0 +1,319 @@ +param( + [Parameter(Mandatory=$true)][string] $InputPath, # Full path to directory where Symbols.NuGet packages to be checked are stored + [Parameter(Mandatory=$true)][string] $ExtractPath, # Full path to directory where the packages will be extracted during validation + [Parameter(Mandatory=$false)][string] $GHRepoName, # GitHub name of the repo including the Org. E.g., dotnet/arcade + [Parameter(Mandatory=$false)][string] $GHCommit, # GitHub commit SHA used to build the packages + [Parameter(Mandatory=$true)][string] $SourcelinkCliVersion # Version of SourceLink CLI to use +) + +. $PSScriptRoot\post-build-utils.ps1 + +# Cache/HashMap (File -> Exist flag) used to consult whether a file exist +# in the repository at a specific commit point. This is populated by inserting +# all files present in the repo at a specific commit point. +$global:RepoFiles = @{} + +# Maximum number of jobs to run in parallel +$MaxParallelJobs = 16 + +$MaxRetries = 5 +$RetryWaitTimeInSeconds = 30 + +# Wait time between check for system load +$SecondsBetweenLoadChecks = 10 + +if (!$InputPath -or !(Test-Path $InputPath)){ + Write-Host "No files to validate." + ExitWithExitCode 0 +} + +$ValidatePackage = { + param( + [string] $PackagePath # Full path to a Symbols.NuGet package + ) + + . $using:PSScriptRoot\..\tools.ps1 + + # Ensure input file exist + if (!(Test-Path $PackagePath)) { + Write-Host "Input file does not exist: $PackagePath" + return [pscustomobject]@{ + result = 1 + packagePath = $PackagePath + } + } + + # Extensions for which we'll look for SourceLink information + # For now we'll only care about Portable & Embedded PDBs + $RelevantExtensions = @('.dll', '.exe', '.pdb') + + Write-Host -NoNewLine 'Validating ' ([System.IO.Path]::GetFileName($PackagePath)) '...' + + $PackageId = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath) + $ExtractPath = Join-Path -Path $using:ExtractPath -ChildPath $PackageId + $FailedFiles = 0 + + Add-Type -AssemblyName System.IO.Compression.FileSystem + + [System.IO.Directory]::CreateDirectory($ExtractPath) | Out-Null + + try { + $zip = [System.IO.Compression.ZipFile]::OpenRead($PackagePath) + + $zip.Entries | + Where-Object {$RelevantExtensions -contains [System.IO.Path]::GetExtension($_.Name)} | + ForEach-Object { + $FileName = $_.FullName + $Extension = [System.IO.Path]::GetExtension($_.Name) + $FakeName = -Join((New-Guid), $Extension) + $TargetFile = Join-Path -Path $ExtractPath -ChildPath $FakeName + + # We ignore resource DLLs + if ($FileName.EndsWith('.resources.dll')) { + return [pscustomobject]@{ + result = 0 + packagePath = $PackagePath + } + } + + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $TargetFile, $true) + + $ValidateFile = { + param( + [string] $FullPath, # Full path to the module that has to be checked + [string] $RealPath, + [ref] $FailedFiles + ) + + $sourcelinkExe = "$env:USERPROFILE\.dotnet\tools" + $sourcelinkExe = Resolve-Path "$sourcelinkExe\sourcelink.exe" + $SourceLinkInfos = & $sourcelinkExe print-urls $FullPath | Out-String + + if ($LASTEXITCODE -eq 0 -and -not ([string]::IsNullOrEmpty($SourceLinkInfos))) { + $NumFailedLinks = 0 + + # We only care about Http addresses + $Matches = (Select-String '(http[s]?)(:\/\/)([^\s,]+)' -Input $SourceLinkInfos -AllMatches).Matches + + if ($Matches.Count -ne 0) { + $Matches.Value | + ForEach-Object { + $Link = $_ + $CommitUrl = "https://raw.githubusercontent.com/${using:GHRepoName}/${using:GHCommit}/" + + $FilePath = $Link.Replace($CommitUrl, "") + $Status = 200 + $Cache = $using:RepoFiles + + $attempts = 0 + + while ($attempts -lt $using:MaxRetries) { + if ( !($Cache.ContainsKey($FilePath)) ) { + try { + $Uri = $Link -as [System.URI] + + if ($Link -match "submodules") { + # Skip submodule links until sourcelink properly handles submodules + $Status = 200 + } + elseif ($Uri.AbsoluteURI -ne $null -and ($Uri.Host -match 'github' -or $Uri.Host -match 'githubusercontent')) { + # Only GitHub links are valid + $Status = (Invoke-WebRequest -Uri $Link -UseBasicParsing -Method HEAD -TimeoutSec 5).StatusCode + } + else { + # If it's not a github link, we want to break out of the loop and not retry. + $Status = 0 + $attempts = $using:MaxRetries + } + } + catch { + Write-Host $_ + $Status = 0 + } + } + + if ($Status -ne 200) { + $attempts++ + + if ($attempts -lt $using:MaxRetries) + { + $attemptsLeft = $using:MaxRetries - $attempts + Write-Warning "Download failed, $attemptsLeft attempts remaining, will retry in $using:RetryWaitTimeInSeconds seconds" + Start-Sleep -Seconds $using:RetryWaitTimeInSeconds + } + else { + if ($NumFailedLinks -eq 0) { + if ($FailedFiles.Value -eq 0) { + Write-Host + } + + Write-Host "`tFile $RealPath has broken links:" + } + + Write-Host "`t`tFailed to retrieve $Link" + + $NumFailedLinks++ + } + } + else { + break + } + } + } + } + + if ($NumFailedLinks -ne 0) { + $FailedFiles.value++ + $global:LASTEXITCODE = 1 + } + } + } + + &$ValidateFile $TargetFile $FileName ([ref]$FailedFiles) + } + } + catch { + Write-Host $_ + } + finally { + $zip.Dispose() + } + + if ($FailedFiles -eq 0) { + Write-Host 'Passed.' + return [pscustomobject]@{ + result = 0 + packagePath = $PackagePath + } + } + else { + Write-PipelineTelemetryError -Category 'SourceLink' -Message "$PackagePath has broken SourceLink links." + return [pscustomobject]@{ + result = 1 + packagePath = $PackagePath + } + } +} + +function CheckJobResult( + $result, + $packagePath, + [ref]$ValidationFailures, + [switch]$logErrors) { + if ($result -ne '0') { + if ($logErrors) { + Write-PipelineTelemetryError -Category 'SourceLink' -Message "$packagePath has broken SourceLink links." + } + $ValidationFailures.Value++ + } +} + +function ValidateSourceLinkLinks { + if ($GHRepoName -ne '' -and !($GHRepoName -Match '^[^\s\/]+/[^\s\/]+$')) { + if (!($GHRepoName -Match '^[^\s-]+-[^\s]+$')) { + Write-PipelineTelemetryError -Category 'SourceLink' -Message "GHRepoName should be in the format / or -. '$GHRepoName'" + ExitWithExitCode 1 + } + else { + $GHRepoName = $GHRepoName -replace '^([^\s-]+)-([^\s]+)$', '$1/$2'; + } + } + + if ($GHCommit -ne '' -and !($GHCommit -Match '^[0-9a-fA-F]{40}$')) { + Write-PipelineTelemetryError -Category 'SourceLink' -Message "GHCommit should be a 40 chars hexadecimal string. '$GHCommit'" + ExitWithExitCode 1 + } + + if ($GHRepoName -ne '' -and $GHCommit -ne '') { + $RepoTreeURL = -Join('http://api.github.com/repos/', $GHRepoName, '/git/trees/', $GHCommit, '?recursive=1') + $CodeExtensions = @('.cs', '.vb', '.fs', '.fsi', '.fsx', '.fsscript') + + try { + # Retrieve the list of files in the repo at that particular commit point and store them in the RepoFiles hash + $Data = Invoke-WebRequest $RepoTreeURL -UseBasicParsing | ConvertFrom-Json | Select-Object -ExpandProperty tree + + foreach ($file in $Data) { + $Extension = [System.IO.Path]::GetExtension($file.path) + + if ($CodeExtensions.Contains($Extension)) { + $RepoFiles[$file.path] = 1 + } + } + } + catch { + Write-Host "Problems downloading the list of files from the repo. Url used: $RepoTreeURL . Execution will proceed without caching." + } + } + elseif ($GHRepoName -ne '' -or $GHCommit -ne '') { + Write-Host 'For using the http caching mechanism both GHRepoName and GHCommit should be informed.' + } + + if (Test-Path $ExtractPath) { + Remove-Item $ExtractPath -Force -Recurse -ErrorAction SilentlyContinue + } + + $ValidationFailures = 0 + + # Process each NuGet package in parallel + Get-ChildItem "$InputPath\*.symbols.nupkg" | + ForEach-Object { + Write-Host "Starting $($_.FullName)" + Start-Job -ScriptBlock $ValidatePackage -ArgumentList $_.FullName | Out-Null + $NumJobs = @(Get-Job -State 'Running').Count + + while ($NumJobs -ge $MaxParallelJobs) { + Write-Host "There are $NumJobs validation jobs running right now. Waiting $SecondsBetweenLoadChecks seconds to check again." + sleep $SecondsBetweenLoadChecks + $NumJobs = @(Get-Job -State 'Running').Count + } + + foreach ($Job in @(Get-Job -State 'Completed')) { + $jobResult = Wait-Job -Id $Job.Id | Receive-Job + CheckJobResult $jobResult.result $jobResult.packagePath ([ref]$ValidationFailures) -LogErrors + Remove-Job -Id $Job.Id + } + } + + foreach ($Job in @(Get-Job)) { + $jobResult = Wait-Job -Id $Job.Id | Receive-Job + CheckJobResult $jobResult.result $jobResult.packagePath ([ref]$ValidationFailures) + Remove-Job -Id $Job.Id + } + if ($ValidationFailures -gt 0) { + Write-PipelineTelemetryError -Category 'SourceLink' -Message "$ValidationFailures package(s) failed validation." + ExitWithExitCode 1 + } +} + +function InstallSourcelinkCli { + $sourcelinkCliPackageName = 'sourcelink' + + $dotnetRoot = InitializeDotNetCli -install:$true + $dotnet = "$dotnetRoot\dotnet.exe" + $toolList = & "$dotnet" tool list --global + + if (($toolList -like "*$sourcelinkCliPackageName*") -and ($toolList -like "*$sourcelinkCliVersion*")) { + Write-Host "SourceLink CLI version $sourcelinkCliVersion is already installed." + } + else { + Write-Host "Installing SourceLink CLI version $sourcelinkCliVersion..." + Write-Host 'You may need to restart your command window if this is the first dotnet tool you have installed.' + & "$dotnet" tool install $sourcelinkCliPackageName --version $sourcelinkCliVersion --verbosity "minimal" --global + } +} + +try { + InstallSourcelinkCli + + foreach ($Job in @(Get-Job)) { + Remove-Job -Id $Job.Id + } + + ValidateSourceLinkLinks +} +catch { + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + Write-PipelineTelemetryError -Category 'SourceLink' -Message $_ + ExitWithExitCode 1 +} diff --git a/eng/common/post-build/symbols-validation.ps1 b/eng/common/post-build/symbols-validation.ps1 new file mode 100644 index 0000000000..cd2181bafa --- /dev/null +++ b/eng/common/post-build/symbols-validation.ps1 @@ -0,0 +1,339 @@ +param( + [Parameter(Mandatory = $true)][string] $InputPath, # Full path to directory where NuGet packages to be checked are stored + [Parameter(Mandatory = $true)][string] $ExtractPath, # Full path to directory where the packages will be extracted during validation + [Parameter(Mandatory = $true)][string] $DotnetSymbolVersion, # Version of dotnet symbol to use + [Parameter(Mandatory = $false)][switch] $CheckForWindowsPdbs, # If we should check for the existence of windows pdbs in addition to portable PDBs + [Parameter(Mandatory = $false)][switch] $ContinueOnError, # If we should keep checking symbols after an error + [Parameter(Mandatory = $false)][switch] $Clean, # Clean extracted symbols directory after checking symbols + [Parameter(Mandatory = $false)][string] $SymbolExclusionFile # Exclude the symbols in the file from publishing to symbol server +) + +. $PSScriptRoot\..\tools.ps1 +# Maximum number of jobs to run in parallel +$MaxParallelJobs = 16 + +# Max number of retries +$MaxRetry = 5 + +# Wait time between check for system load +$SecondsBetweenLoadChecks = 10 + +# Set error codes +Set-Variable -Name "ERROR_BADEXTRACT" -Option Constant -Value -1 +Set-Variable -Name "ERROR_FILEDOESNOTEXIST" -Option Constant -Value -2 + +$WindowsPdbVerificationParam = "" +if ($CheckForWindowsPdbs) { + $WindowsPdbVerificationParam = "--windows-pdbs" +} + +$ExclusionSet = New-Object System.Collections.Generic.HashSet[string]; + +if (!$InputPath -or !(Test-Path $InputPath)){ + Write-Host "No symbols to validate." + ExitWithExitCode 0 +} + +#Check if the path exists +if ($SymbolExclusionFile -and (Test-Path $SymbolExclusionFile)){ + [string[]]$Exclusions = Get-Content "$SymbolExclusionFile" + $Exclusions | foreach { if($_ -and $_.Trim()){$ExclusionSet.Add($_)} } +} +else{ + Write-Host "Symbol Exclusion file does not exists. No symbols to exclude." +} + +$CountMissingSymbols = { + param( + [string] $PackagePath, # Path to a NuGet package + [string] $WindowsPdbVerificationParam # If we should check for the existence of windows pdbs in addition to portable PDBs + ) + + Add-Type -AssemblyName System.IO.Compression.FileSystem + + Write-Host "Validating $PackagePath " + + # Ensure input file exist + if (!(Test-Path $PackagePath)) { + Write-PipelineTaskError "Input file does not exist: $PackagePath" + return [pscustomobject]@{ + result = $using:ERROR_FILEDOESNOTEXIST + packagePath = $PackagePath + } + } + + # Extensions for which we'll look for symbols + $RelevantExtensions = @('.dll', '.exe', '.so', '.dylib') + + # How many files are missing symbol information + $MissingSymbols = 0 + + $PackageId = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath) + $PackageGuid = New-Guid + $ExtractPath = Join-Path -Path $using:ExtractPath -ChildPath $PackageGuid + $SymbolsPath = Join-Path -Path $ExtractPath -ChildPath 'Symbols' + + try { + [System.IO.Compression.ZipFile]::ExtractToDirectory($PackagePath, $ExtractPath) + } + catch { + Write-Host "Something went wrong extracting $PackagePath" + Write-Host $_ + return [pscustomobject]@{ + result = $using:ERROR_BADEXTRACT + packagePath = $PackagePath + } + } + + Get-ChildItem -Recurse $ExtractPath | + Where-Object { $RelevantExtensions -contains $_.Extension } | + ForEach-Object { + $FileName = $_.FullName + if ($FileName -Match '\\ref\\') { + Write-Host "`t Ignoring reference assembly file " $FileName + return + } + + $FirstMatchingSymbolDescriptionOrDefault = { + param( + [string] $FullPath, # Full path to the module that has to be checked + [string] $TargetServerParam, # Parameter to pass to `Symbol Tool` indicating the server to lookup for symbols + [string] $WindowsPdbVerificationParam, # Parameter to pass to potential check for windows-pdbs. + [string] $SymbolsPath + ) + + $FileName = [System.IO.Path]::GetFileName($FullPath) + $Extension = [System.IO.Path]::GetExtension($FullPath) + + # Those below are potential symbol files that the `dotnet symbol` might + # return. Which one will be returned depend on the type of file we are + # checking and which type of file was uploaded. + + # The file itself is returned + $SymbolPath = $SymbolsPath + '\' + $FileName + + # PDB file for the module + $PdbPath = $SymbolPath.Replace($Extension, '.pdb') + + # PDB file for R2R module (created by crossgen) + $NGenPdb = $SymbolPath.Replace($Extension, '.ni.pdb') + + # DBG file for a .so library + $SODbg = $SymbolPath.Replace($Extension, '.so.dbg') + + # DWARF file for a .dylib + $DylibDwarf = $SymbolPath.Replace($Extension, '.dylib.dwarf') + + $dotnetSymbolExe = "$env:USERPROFILE\.dotnet\tools" + $dotnetSymbolExe = Resolve-Path "$dotnetSymbolExe\dotnet-symbol.exe" + + $totalRetries = 0 + + while ($totalRetries -lt $using:MaxRetry) { + + # Save the output and get diagnostic output + $output = & $dotnetSymbolExe --symbols --modules $WindowsPdbVerificationParam $TargetServerParam $FullPath -o $SymbolsPath --diagnostics | Out-String + + if ((Test-Path $PdbPath) -and (Test-path $SymbolPath)) { + return 'Module and PDB for Module' + } + elseif ((Test-Path $NGenPdb) -and (Test-Path $PdbPath) -and (Test-Path $SymbolPath)) { + return 'Dll, PDB and NGen PDB' + } + elseif ((Test-Path $SODbg) -and (Test-Path $SymbolPath)) { + return 'So and DBG for SO' + } + elseif ((Test-Path $DylibDwarf) -and (Test-Path $SymbolPath)) { + return 'Dylib and Dwarf for Dylib' + } + elseif (Test-Path $SymbolPath) { + return 'Module' + } + else + { + $totalRetries++ + } + } + + return $null + } + + $FileRelativePath = $FileName.Replace("$ExtractPath\", "") + if (($($using:ExclusionSet) -ne $null) -and ($($using:ExclusionSet).Contains($FileRelativePath) -or ($($using:ExclusionSet).Contains($FileRelativePath.Replace("\", "/"))))){ + Write-Host "Skipping $FileName from symbol validation" + } + + else { + $FileGuid = New-Guid + $ExpandedSymbolsPath = Join-Path -Path $SymbolsPath -ChildPath $FileGuid + + $SymbolsOnMSDL = & $FirstMatchingSymbolDescriptionOrDefault ` + -FullPath $FileName ` + -TargetServerParam '--microsoft-symbol-server' ` + -SymbolsPath "$ExpandedSymbolsPath-msdl" ` + -WindowsPdbVerificationParam $WindowsPdbVerificationParam + $SymbolsOnSymWeb = & $FirstMatchingSymbolDescriptionOrDefault ` + -FullPath $FileName ` + -TargetServerParam '--internal-server' ` + -SymbolsPath "$ExpandedSymbolsPath-symweb" ` + -WindowsPdbVerificationParam $WindowsPdbVerificationParam + + Write-Host -NoNewLine "`t Checking file " $FileName "... " + + if ($SymbolsOnMSDL -ne $null -and $SymbolsOnSymWeb -ne $null) { + Write-Host "Symbols found on MSDL ($SymbolsOnMSDL) and SymWeb ($SymbolsOnSymWeb)" + } + else { + $MissingSymbols++ + + if ($SymbolsOnMSDL -eq $null -and $SymbolsOnSymWeb -eq $null) { + Write-Host 'No symbols found on MSDL or SymWeb!' + } + else { + if ($SymbolsOnMSDL -eq $null) { + Write-Host 'No symbols found on MSDL!' + } + else { + Write-Host 'No symbols found on SymWeb!' + } + } + } + } + } + + if ($using:Clean) { + Remove-Item $ExtractPath -Recurse -Force + } + + Pop-Location + + return [pscustomobject]@{ + result = $MissingSymbols + packagePath = $PackagePath + } +} + +function CheckJobResult( + $result, + $packagePath, + [ref]$DupedSymbols, + [ref]$TotalFailures) { + if ($result -eq $ERROR_BADEXTRACT) { + Write-PipelineTelemetryError -Category 'CheckSymbols' -Message "$packagePath has duplicated symbol files" + $DupedSymbols.Value++ + } + elseif ($result -eq $ERROR_FILEDOESNOTEXIST) { + Write-PipelineTelemetryError -Category 'CheckSymbols' -Message "$packagePath does not exist" + $TotalFailures.Value++ + } + elseif ($result -gt '0') { + Write-PipelineTelemetryError -Category 'CheckSymbols' -Message "Missing symbols for $result modules in the package $packagePath" + $TotalFailures.Value++ + } + else { + Write-Host "All symbols verified for package $packagePath" + } +} + +function CheckSymbolsAvailable { + if (Test-Path $ExtractPath) { + Remove-Item $ExtractPath -Force -Recurse -ErrorAction SilentlyContinue + } + + $TotalPackages = 0 + $TotalFailures = 0 + $DupedSymbols = 0 + + Get-ChildItem "$InputPath\*.nupkg" | + ForEach-Object { + $FileName = $_.Name + $FullName = $_.FullName + + # These packages from Arcade-Services include some native libraries that + # our current symbol uploader can't handle. Below is a workaround until + # we get issue: https://github.com/dotnet/arcade/issues/2457 sorted. + if ($FileName -Match 'Microsoft\.DotNet\.Darc\.') { + Write-Host "Ignoring Arcade-services file: $FileName" + Write-Host + return + } + elseif ($FileName -Match 'Microsoft\.DotNet\.Maestro\.Tasks\.') { + Write-Host "Ignoring Arcade-services file: $FileName" + Write-Host + return + } + + $TotalPackages++ + + Start-Job -ScriptBlock $CountMissingSymbols -ArgumentList @($FullName,$WindowsPdbVerificationParam) | Out-Null + + $NumJobs = @(Get-Job -State 'Running').Count + + while ($NumJobs -ge $MaxParallelJobs) { + Write-Host "There are $NumJobs validation jobs running right now. Waiting $SecondsBetweenLoadChecks seconds to check again." + sleep $SecondsBetweenLoadChecks + $NumJobs = @(Get-Job -State 'Running').Count + } + + foreach ($Job in @(Get-Job -State 'Completed')) { + $jobResult = Wait-Job -Id $Job.Id | Receive-Job + CheckJobResult $jobResult.result $jobResult.packagePath ([ref]$DupedSymbols) ([ref]$TotalFailures) + Remove-Job -Id $Job.Id + } + Write-Host + } + + foreach ($Job in @(Get-Job)) { + $jobResult = Wait-Job -Id $Job.Id | Receive-Job + CheckJobResult $jobResult.result $jobResult.packagePath ([ref]$DupedSymbols) ([ref]$TotalFailures) + } + + if ($TotalFailures -gt 0 -or $DupedSymbols -gt 0) { + if ($TotalFailures -gt 0) { + Write-PipelineTelemetryError -Category 'CheckSymbols' -Message "Symbols missing for $TotalFailures/$TotalPackages packages" + } + + if ($DupedSymbols -gt 0) { + Write-PipelineTelemetryError -Category 'CheckSymbols' -Message "$DupedSymbols/$TotalPackages packages had duplicated symbol files and could not be extracted" + } + + ExitWithExitCode 1 + } + else { + Write-Host "All symbols validated!" + } +} + +function InstallDotnetSymbol { + $dotnetSymbolPackageName = 'dotnet-symbol' + + $dotnetRoot = InitializeDotNetCli -install:$true + $dotnet = "$dotnetRoot\dotnet.exe" + $toolList = & "$dotnet" tool list --global + + if (($toolList -like "*$dotnetSymbolPackageName*") -and ($toolList -like "*$dotnetSymbolVersion*")) { + Write-Host "dotnet-symbol version $dotnetSymbolVersion is already installed." + } + else { + Write-Host "Installing dotnet-symbol version $dotnetSymbolVersion..." + Write-Host 'You may need to restart your command window if this is the first dotnet tool you have installed.' + & "$dotnet" tool install $dotnetSymbolPackageName --version $dotnetSymbolVersion --verbosity "minimal" --global + } +} + +try { + . $PSScriptRoot\post-build-utils.ps1 + + InstallDotnetSymbol + + foreach ($Job in @(Get-Job)) { + Remove-Job -Id $Job.Id + } + + CheckSymbolsAvailable +} +catch { + Write-Host $_.ScriptStackTrace + Write-PipelineTelemetryError -Category 'CheckSymbols' -Message $_ + ExitWithExitCode 1 +} diff --git a/eng/common/post-build/trigger-subscriptions.ps1 b/eng/common/post-build/trigger-subscriptions.ps1 new file mode 100644 index 0000000000..55dea518ac --- /dev/null +++ b/eng/common/post-build/trigger-subscriptions.ps1 @@ -0,0 +1,64 @@ +param( + [Parameter(Mandatory=$true)][string] $SourceRepo, + [Parameter(Mandatory=$true)][int] $ChannelId, + [Parameter(Mandatory=$true)][string] $MaestroApiAccessToken, + [Parameter(Mandatory=$false)][string] $MaestroApiEndPoint = 'https://maestro-prod.westus2.cloudapp.azure.com', + [Parameter(Mandatory=$false)][string] $MaestroApiVersion = '2019-01-16' +) + +try { + . $PSScriptRoot\post-build-utils.ps1 + + # Get all the $SourceRepo subscriptions + $normalizedSourceRepo = $SourceRepo.Replace('dnceng@', '') + $subscriptions = Get-MaestroSubscriptions -SourceRepository $normalizedSourceRepo -ChannelId $ChannelId + + if (!$subscriptions) { + Write-PipelineTelemetryError -Category 'TriggerSubscriptions' -Message "No subscriptions found for source repo '$normalizedSourceRepo' in channel '$ChannelId'" + ExitWithExitCode 0 + } + + $subscriptionsToTrigger = New-Object System.Collections.Generic.List[string] + $failedTriggeredSubscription = $false + + # Get all enabled subscriptions that need dependency flow on 'everyBuild' + foreach ($subscription in $subscriptions) { + if ($subscription.enabled -and $subscription.policy.updateFrequency -like 'everyBuild' -and $subscription.channel.id -eq $ChannelId) { + Write-Host "Should trigger this subscription: ${$subscription.id}" + [void]$subscriptionsToTrigger.Add($subscription.id) + } + } + + foreach ($subscriptionToTrigger in $subscriptionsToTrigger) { + try { + Write-Host "Triggering subscription '$subscriptionToTrigger'." + + Trigger-Subscription -SubscriptionId $subscriptionToTrigger + + Write-Host 'done.' + } + catch + { + Write-Host "There was an error while triggering subscription '$subscriptionToTrigger'" + Write-Host $_ + Write-Host $_.ScriptStackTrace + $failedTriggeredSubscription = $true + } + } + + if ($subscriptionsToTrigger.Count -eq 0) { + Write-Host "No subscription matched source repo '$normalizedSourceRepo' and channel ID '$ChannelId'." + } + elseif ($failedTriggeredSubscription) { + Write-PipelineTelemetryError -Category 'TriggerSubscriptions' -Message 'At least one subscription failed to be triggered...' + ExitWithExitCode 1 + } + else { + Write-Host 'All subscriptions were triggered successfully!' + } +} +catch { + Write-Host $_.ScriptStackTrace + Write-PipelineTelemetryError -Category 'TriggerSubscriptions' -Message $_ + ExitWithExitCode 1 +} diff --git a/eng/common/retain-build.ps1 b/eng/common/retain-build.ps1 new file mode 100644 index 0000000000..e7ba975ade --- /dev/null +++ b/eng/common/retain-build.ps1 @@ -0,0 +1,45 @@ + +Param( +[Parameter(Mandatory=$true)][int] $buildId, +[Parameter(Mandatory=$true)][string] $azdoOrgUri, +[Parameter(Mandatory=$true)][string] $azdoProject, +[Parameter(Mandatory=$true)][string] $token +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 2.0 + +function Get-AzDOHeaders( + [string] $token) +{ + $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":${token}")) + $headers = @{"Authorization"="Basic $base64AuthInfo"} + return $headers +} + +function Update-BuildRetention( + [string] $azdoOrgUri, + [string] $azdoProject, + [int] $buildId, + [string] $token) +{ + $headers = Get-AzDOHeaders -token $token + $requestBody = "{ + `"keepForever`": `"true`" + }" + + $requestUri = "${azdoOrgUri}/${azdoProject}/_apis/build/builds/${buildId}?api-version=6.0" + write-Host "Attempting to retain build using the following URI: ${requestUri} ..." + + try { + Invoke-RestMethod -Uri $requestUri -Method Patch -Body $requestBody -Header $headers -contentType "application/json" + Write-Host "Updated retention settings for build ${buildId}." + } + catch { + Write-Error "Failed to update retention settings for build: $_.Exception.Response.StatusDescription" + exit 1 + } +} + +Update-BuildRetention -azdoOrgUri $azdoOrgUri -azdoProject $azdoProject -buildId $buildId -token $token +exit 0 diff --git a/eng/common/sdk-task.ps1 b/eng/common/sdk-task.ps1 new file mode 100644 index 0000000000..e10a596879 --- /dev/null +++ b/eng/common/sdk-task.ps1 @@ -0,0 +1,97 @@ +[CmdletBinding(PositionalBinding=$false)] +Param( + [string] $configuration = 'Debug', + [string] $task, + [string] $verbosity = 'minimal', + [string] $msbuildEngine = $null, + [switch] $restore, + [switch] $prepareMachine, + [switch] $help, + [Parameter(ValueFromRemainingArguments=$true)][String[]]$properties +) + +$ci = $true +$binaryLog = $true +$warnAsError = $true + +. $PSScriptRoot\tools.ps1 + +function Print-Usage() { + Write-Host "Common settings:" + Write-Host " -task Name of Arcade task (name of a project in SdkTasks directory of the Arcade SDK package)" + Write-Host " -restore Restore dependencies" + Write-Host " -verbosity Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]" + Write-Host " -help Print help and exit" + Write-Host "" + + Write-Host "Advanced settings:" + Write-Host " -prepareMachine Prepare machine for CI run" + Write-Host " -msbuildEngine Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)." + Write-Host "" + Write-Host "Command line arguments not listed above are passed thru to msbuild." +} + +function Build([string]$target) { + $logSuffix = if ($target -eq 'Execute') { '' } else { ".$target" } + $log = Join-Path $LogDir "$task$logSuffix.binlog" + $outputPath = Join-Path $ToolsetDir "$task\" + + MSBuild $taskProject ` + /bl:$log ` + /t:$target ` + /p:Configuration=$configuration ` + /p:RepoRoot=$RepoRoot ` + /p:BaseIntermediateOutputPath=$outputPath ` + /v:$verbosity ` + @properties +} + +try { + if ($help -or (($null -ne $properties) -and ($properties.Contains('/help') -or $properties.Contains('/?')))) { + Print-Usage + exit 0 + } + + if ($task -eq "") { + Write-PipelineTelemetryError -Category 'Build' -Message "Missing required parameter '-task '" + Print-Usage + ExitWithExitCode 1 + } + + if( $msbuildEngine -eq "vs") { + # Ensure desktop MSBuild is available for sdk tasks. + if( -not ($GlobalJson.tools.PSObject.Properties.Name -contains "vs" )) { + $GlobalJson.tools | Add-Member -Name "vs" -Value (ConvertFrom-Json "{ `"version`": `"16.5`" }") -MemberType NoteProperty + } + if( -not ($GlobalJson.tools.PSObject.Properties.Name -match "xcopy-msbuild" )) { + $GlobalJson.tools | Add-Member -Name "xcopy-msbuild" -Value "17.4.1" -MemberType NoteProperty + } + if ($GlobalJson.tools."xcopy-msbuild".Trim() -ine "none") { + $xcopyMSBuildToolsFolder = InitializeXCopyMSBuild $GlobalJson.tools."xcopy-msbuild" -install $true + } + if ($xcopyMSBuildToolsFolder -eq $null) { + throw 'Unable to get xcopy downloadable version of msbuild' + } + + $global:_MSBuildExe = "$($xcopyMSBuildToolsFolder)\MSBuild\Current\Bin\MSBuild.exe" + } + + $taskProject = GetSdkTaskProject $task + if (!(Test-Path $taskProject)) { + Write-PipelineTelemetryError -Category 'Build' -Message "Unknown task: $task" + ExitWithExitCode 1 + } + + if ($restore) { + Build 'Restore' + } + + Build 'Execute' +} +catch { + Write-Host $_.ScriptStackTrace + Write-PipelineTelemetryError -Category 'Build' -Message $_ + ExitWithExitCode 1 +} + +ExitWithExitCode 0 diff --git a/eng/common/sdl/NuGet.config b/eng/common/sdl/NuGet.config new file mode 100644 index 0000000000..3849bdb3cf --- /dev/null +++ b/eng/common/sdl/NuGet.config @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/eng/common/sdl/configure-sdl-tool.ps1 b/eng/common/sdl/configure-sdl-tool.ps1 new file mode 100644 index 0000000000..bdbf49e6c7 --- /dev/null +++ b/eng/common/sdl/configure-sdl-tool.ps1 @@ -0,0 +1,116 @@ +Param( + [string] $GuardianCliLocation, + [string] $WorkingDirectory, + [string] $TargetDirectory, + [string] $GdnFolder, + # The list of Guardian tools to configure. For each object in the array: + # - If the item is a [hashtable], it must contain these entries: + # - Name = The tool name as Guardian knows it. + # - Scenario = (Optional) Scenario-specific name for this configuration entry. It must be unique + # among all tool entries with the same Name. + # - Args = (Optional) Array of Guardian tool configuration args, like '@("Target > C:\temp")' + # - If the item is a [string] $v, it is treated as '@{ Name="$v" }' + [object[]] $ToolsList, + [string] $GuardianLoggerLevel='Standard', + # Optional: Additional params to add to any tool using CredScan. + [string[]] $CrScanAdditionalRunConfigParams, + # Optional: Additional params to add to any tool using PoliCheck. + [string[]] $PoliCheckAdditionalRunConfigParams, + # Optional: Additional params to add to any tool using CodeQL/Semmle. + [string[]] $CodeQLAdditionalRunConfigParams +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 2.0 +$disableConfigureToolsetImport = $true +$global:LASTEXITCODE = 0 + +try { + # `tools.ps1` checks $ci to perform some actions. Since the SDL + # scripts don't necessarily execute in the same agent that run the + # build.ps1/sh script this variable isn't automatically set. + $ci = $true + . $PSScriptRoot\..\tools.ps1 + + # Normalize tools list: all in [hashtable] form with defined values for each key. + $ToolsList = $ToolsList | + ForEach-Object { + if ($_ -is [string]) { + $_ = @{ Name = $_ } + } + + if (-not ($_['Scenario'])) { $_.Scenario = "" } + if (-not ($_['Args'])) { $_.Args = @() } + $_ + } + + Write-Host "List of tools to configure:" + $ToolsList | ForEach-Object { $_ | Out-String | Write-Host } + + # We store config files in the r directory of .gdn + $gdnConfigPath = Join-Path $GdnFolder 'r' + $ValidPath = Test-Path $GuardianCliLocation + + if ($ValidPath -eq $False) + { + Write-PipelineTelemetryError -Force -Category 'Sdl' -Message "Invalid Guardian CLI Location." + ExitWithExitCode 1 + } + + foreach ($tool in $ToolsList) { + # Put together the name and scenario to make a unique key. + $toolConfigName = $tool.Name + if ($tool.Scenario) { + $toolConfigName += "_" + $tool.Scenario + } + + Write-Host "=== Configuring $toolConfigName..." + + $gdnConfigFile = Join-Path $gdnConfigPath "$toolConfigName-configure.gdnconfig" + + # For some tools, add default and automatic args. + if ($tool.Name -eq 'credscan') { + if ($targetDirectory) { + $tool.Args += "`"TargetDirectory < $TargetDirectory`"" + } + $tool.Args += "`"OutputType < pre`"" + $tool.Args += $CrScanAdditionalRunConfigParams + } elseif ($tool.Name -eq 'policheck') { + if ($targetDirectory) { + $tool.Args += "`"Target < $TargetDirectory`"" + } + $tool.Args += $PoliCheckAdditionalRunConfigParams + } elseif ($tool.Name -eq 'semmle' -or $tool.Name -eq 'codeql') { + if ($targetDirectory) { + $tool.Args += "`"SourceCodeDirectory < $TargetDirectory`"" + } + $tool.Args += $CodeQLAdditionalRunConfigParams + } + + # Create variable pointing to the args array directly so we can use splat syntax later. + $toolArgs = $tool.Args + + # Configure the tool. If args array is provided or the current tool has some default arguments + # defined, add "--args" and splat each element on the end. Arg format is "{Arg id} < {Value}", + # one per parameter. Doc page for "guardian configure": + # https://dev.azure.com/securitytools/SecurityIntegration/_wiki/wikis/Guardian/1395/configure + Exec-BlockVerbosely { + & $GuardianCliLocation configure ` + --working-directory $WorkingDirectory ` + --tool $tool.Name ` + --output-path $gdnConfigFile ` + --logger-level $GuardianLoggerLevel ` + --noninteractive ` + --force ` + $(if ($toolArgs) { "--args" }) @toolArgs + Exit-IfNZEC "Sdl" + } + + Write-Host "Created '$toolConfigName' configuration file: $gdnConfigFile" + } +} +catch { + Write-Host $_.ScriptStackTrace + Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_ + ExitWithExitCode 1 +} diff --git a/eng/common/sdl/execute-all-sdl-tools.ps1 b/eng/common/sdl/execute-all-sdl-tools.ps1 new file mode 100644 index 0000000000..4797e012c7 --- /dev/null +++ b/eng/common/sdl/execute-all-sdl-tools.ps1 @@ -0,0 +1,165 @@ +Param( + [string] $GuardianPackageName, # Required: the name of guardian CLI package (not needed if GuardianCliLocation is specified) + [string] $NugetPackageDirectory, # Required: directory where NuGet packages are installed (not needed if GuardianCliLocation is specified) + [string] $GuardianCliLocation, # Optional: Direct location of Guardian CLI executable if GuardianPackageName & NugetPackageDirectory are not specified + [string] $Repository=$env:BUILD_REPOSITORY_NAME, # Required: the name of the repository (e.g. dotnet/arcade) + [string] $BranchName=$env:BUILD_SOURCEBRANCH, # Optional: name of branch or version of gdn settings; defaults to master + [string] $SourceDirectory=$env:BUILD_SOURCESDIRECTORY, # Required: the directory where source files are located + [string] $ArtifactsDirectory = (Join-Path $env:BUILD_ARTIFACTSTAGINGDIRECTORY ('artifacts')), # Required: the directory where build artifacts are located + [string] $AzureDevOpsAccessToken, # Required: access token for dnceng; should be provided via KeyVault + + # Optional: list of SDL tools to run on source code. See 'configure-sdl-tool.ps1' for tools list + # format. + [object[]] $SourceToolsList, + # Optional: list of SDL tools to run on built artifacts. See 'configure-sdl-tool.ps1' for tools + # list format. + [object[]] $ArtifactToolsList, + # Optional: list of SDL tools to run without automatically specifying a target directory. See + # 'configure-sdl-tool.ps1' for tools list format. + [object[]] $CustomToolsList, + + [bool] $TsaPublish=$False, # Optional: true will publish results to TSA; only set to true after onboarding to TSA; TSA is the automated framework used to upload test results as bugs. + [string] $TsaBranchName=$env:BUILD_SOURCEBRANCH, # Optional: required for TSA publish; defaults to $(Build.SourceBranchName); TSA is the automated framework used to upload test results as bugs. + [string] $TsaRepositoryName=$env:BUILD_REPOSITORY_NAME, # Optional: TSA repository name; will be generated automatically if not submitted; TSA is the automated framework used to upload test results as bugs. + [string] $BuildNumber=$env:BUILD_BUILDNUMBER, # Optional: required for TSA publish; defaults to $(Build.BuildNumber) + [bool] $UpdateBaseline=$False, # Optional: if true, will update the baseline in the repository; should only be run after fixing any issues which need to be fixed + [bool] $TsaOnboard=$False, # Optional: if true, will onboard the repository to TSA; should only be run once; TSA is the automated framework used to upload test results as bugs. + [string] $TsaInstanceUrl, # Optional: only needed if TsaOnboard or TsaPublish is true; the instance-url registered with TSA; TSA is the automated framework used to upload test results as bugs. + [string] $TsaCodebaseName, # Optional: only needed if TsaOnboard or TsaPublish is true; the name of the codebase registered with TSA; TSA is the automated framework used to upload test results as bugs. + [string] $TsaProjectName, # Optional: only needed if TsaOnboard or TsaPublish is true; the name of the project registered with TSA; TSA is the automated framework used to upload test results as bugs. + [string] $TsaNotificationEmail, # Optional: only needed if TsaOnboard is true; the email(s) which will receive notifications of TSA bug filings (e.g. alias@microsoft.com); TSA is the automated framework used to upload test results as bugs. + [string] $TsaCodebaseAdmin, # Optional: only needed if TsaOnboard is true; the aliases which are admins of the TSA codebase (e.g. DOMAIN\alias); TSA is the automated framework used to upload test results as bugs. + [string] $TsaBugAreaPath, # Optional: only needed if TsaOnboard is true; the area path where TSA will file bugs in AzDO; TSA is the automated framework used to upload test results as bugs. + [string] $TsaIterationPath, # Optional: only needed if TsaOnboard is true; the iteration path where TSA will file bugs in AzDO; TSA is the automated framework used to upload test results as bugs. + [string] $GuardianLoggerLevel='Standard', # Optional: the logger level for the Guardian CLI; options are Trace, Verbose, Standard, Warning, and Error + [string[]] $CrScanAdditionalRunConfigParams, # Optional: Additional Params to custom build a CredScan run config in the format @("xyz:abc","sdf:1") + [string[]] $PoliCheckAdditionalRunConfigParams, # Optional: Additional Params to custom build a Policheck run config in the format @("xyz:abc","sdf:1") + [string[]] $CodeQLAdditionalRunConfigParams, # Optional: Additional Params to custom build a Semmle/CodeQL run config in the format @("xyz < abc","sdf < 1") + [bool] $BreakOnFailure=$False # Optional: Fail the build if there were errors during the run +) + +try { + $ErrorActionPreference = 'Stop' + Set-StrictMode -Version 2.0 + $disableConfigureToolsetImport = $true + $global:LASTEXITCODE = 0 + + # `tools.ps1` checks $ci to perform some actions. Since the SDL + # scripts don't necessarily execute in the same agent that run the + # build.ps1/sh script this variable isn't automatically set. + $ci = $true + . $PSScriptRoot\..\tools.ps1 + + #Replace repo names to the format of org/repo + if (!($Repository.contains('/'))) { + $RepoName = $Repository -replace '(.*?)-(.*)', '$1/$2'; + } + else{ + $RepoName = $Repository; + } + + if ($GuardianPackageName) { + $guardianCliLocation = Join-Path $NugetPackageDirectory (Join-Path $GuardianPackageName (Join-Path 'tools' 'guardian.cmd')) + } else { + $guardianCliLocation = $GuardianCliLocation + } + + $workingDirectory = (Split-Path $SourceDirectory -Parent) + $ValidPath = Test-Path $guardianCliLocation + + if ($ValidPath -eq $False) + { + Write-PipelineTelemetryError -Force -Category 'Sdl' -Message 'Invalid Guardian CLI Location.' + ExitWithExitCode 1 + } + + Exec-BlockVerbosely { + & $(Join-Path $PSScriptRoot 'init-sdl.ps1') -GuardianCliLocation $guardianCliLocation -Repository $RepoName -BranchName $BranchName -WorkingDirectory $workingDirectory -AzureDevOpsAccessToken $AzureDevOpsAccessToken -GuardianLoggerLevel $GuardianLoggerLevel + } + $gdnFolder = Join-Path $workingDirectory '.gdn' + + if ($TsaOnboard) { + if ($TsaCodebaseName -and $TsaNotificationEmail -and $TsaCodebaseAdmin -and $TsaBugAreaPath) { + Exec-BlockVerbosely { + & $guardianCliLocation tsa-onboard --codebase-name "$TsaCodebaseName" --notification-alias "$TsaNotificationEmail" --codebase-admin "$TsaCodebaseAdmin" --instance-url "$TsaInstanceUrl" --project-name "$TsaProjectName" --area-path "$TsaBugAreaPath" --iteration-path "$TsaIterationPath" --working-directory $workingDirectory --logger-level $GuardianLoggerLevel + } + if ($LASTEXITCODE -ne 0) { + Write-PipelineTelemetryError -Force -Category 'Sdl' -Message "Guardian tsa-onboard failed with exit code $LASTEXITCODE." + ExitWithExitCode $LASTEXITCODE + } + } else { + Write-PipelineTelemetryError -Force -Category 'Sdl' -Message 'Could not onboard to TSA -- not all required values ($TsaCodebaseName, $TsaNotificationEmail, $TsaCodebaseAdmin, $TsaBugAreaPath) were specified.' + ExitWithExitCode 1 + } + } + + # Configure a list of tools with a default target directory. Populates the ".gdn/r" directory. + function Configure-ToolsList([object[]] $tools, [string] $targetDirectory) { + if ($tools -and $tools.Count -gt 0) { + Exec-BlockVerbosely { + & $(Join-Path $PSScriptRoot 'configure-sdl-tool.ps1') ` + -GuardianCliLocation $guardianCliLocation ` + -WorkingDirectory $workingDirectory ` + -TargetDirectory $targetDirectory ` + -GdnFolder $gdnFolder ` + -ToolsList $tools ` + -AzureDevOpsAccessToken $AzureDevOpsAccessToken ` + -GuardianLoggerLevel $GuardianLoggerLevel ` + -CrScanAdditionalRunConfigParams $CrScanAdditionalRunConfigParams ` + -PoliCheckAdditionalRunConfigParams $PoliCheckAdditionalRunConfigParams ` + -CodeQLAdditionalRunConfigParams $CodeQLAdditionalRunConfigParams + if ($BreakOnFailure) { + Exit-IfNZEC "Sdl" + } + } + } + } + + # Configure Artifact and Source tools with default Target directories. + Configure-ToolsList $ArtifactToolsList $ArtifactsDirectory + Configure-ToolsList $SourceToolsList $SourceDirectory + # Configure custom tools with no default Target directory. + Configure-ToolsList $CustomToolsList $null + + # At this point, all tools are configured in the ".gdn" directory. Run them all in a single call. + # (If we used "run" multiple times, each run would overwrite data from earlier runs.) + Exec-BlockVerbosely { + & $(Join-Path $PSScriptRoot 'run-sdl.ps1') ` + -GuardianCliLocation $guardianCliLocation ` + -WorkingDirectory $SourceDirectory ` + -UpdateBaseline $UpdateBaseline ` + -GdnFolder $gdnFolder + } + + if ($TsaPublish) { + if ($TsaBranchName -and $BuildNumber) { + if (-not $TsaRepositoryName) { + $TsaRepositoryName = "$($Repository)-$($BranchName)" + } + Exec-BlockVerbosely { + & $guardianCliLocation tsa-publish --all-tools --repository-name "$TsaRepositoryName" --branch-name "$TsaBranchName" --build-number "$BuildNumber" --onboard $True --codebase-name "$TsaCodebaseName" --notification-alias "$TsaNotificationEmail" --codebase-admin "$TsaCodebaseAdmin" --instance-url "$TsaInstanceUrl" --project-name "$TsaProjectName" --area-path "$TsaBugAreaPath" --iteration-path "$TsaIterationPath" --working-directory $workingDirectory --logger-level $GuardianLoggerLevel + } + if ($LASTEXITCODE -ne 0) { + Write-PipelineTelemetryError -Force -Category 'Sdl' -Message "Guardian tsa-publish failed with exit code $LASTEXITCODE." + ExitWithExitCode $LASTEXITCODE + } + } else { + Write-PipelineTelemetryError -Force -Category 'Sdl' -Message 'Could not publish to TSA -- not all required values ($TsaBranchName, $BuildNumber) were specified.' + ExitWithExitCode 1 + } + } + + if ($BreakOnFailure) { + Write-Host "Failing the build in case of breaking results..." + Exec-BlockVerbosely { + & $guardianCliLocation break --working-directory $workingDirectory --logger-level $GuardianLoggerLevel + } + } else { + Write-Host "Letting the build pass even if there were breaking results..." + } +} +catch { + Write-Host $_.ScriptStackTrace + Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_ + exit 1 +} diff --git a/eng/common/sdl/extract-artifact-archives.ps1 b/eng/common/sdl/extract-artifact-archives.ps1 new file mode 100644 index 0000000000..68da4fbf25 --- /dev/null +++ b/eng/common/sdl/extract-artifact-archives.ps1 @@ -0,0 +1,63 @@ +# This script looks for each archive file in a directory and extracts it into the target directory. +# For example, the file "$InputPath/bin.tar.gz" extracts to "$ExtractPath/bin.tar.gz.extracted/**". +# Uses the "tar" utility added to Windows 10 / Windows 2019 that supports tar.gz and zip. +param( + # Full path to directory where archives are stored. + [Parameter(Mandatory=$true)][string] $InputPath, + # Full path to directory to extract archives into. May be the same as $InputPath. + [Parameter(Mandatory=$true)][string] $ExtractPath +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 2.0 + +$disableConfigureToolsetImport = $true + +try { + # `tools.ps1` checks $ci to perform some actions. Since the SDL + # scripts don't necessarily execute in the same agent that run the + # build.ps1/sh script this variable isn't automatically set. + $ci = $true + . $PSScriptRoot\..\tools.ps1 + + Measure-Command { + $jobs = @() + + # Find archive files for non-Windows and Windows builds. + $archiveFiles = @( + Get-ChildItem (Join-Path $InputPath "*.tar.gz") + Get-ChildItem (Join-Path $InputPath "*.zip") + ) + + foreach ($targzFile in $archiveFiles) { + $jobs += Start-Job -ScriptBlock { + $file = $using:targzFile + $fileName = [System.IO.Path]::GetFileName($file) + $extractDir = Join-Path $using:ExtractPath "$fileName.extracted" + + New-Item $extractDir -ItemType Directory -Force | Out-Null + + Write-Host "Extracting '$file' to '$extractDir'..." + + # Pipe errors to stdout to prevent PowerShell detecting them and quitting the job early. + # This type of quit skips the catch, so we wouldn't be able to tell which file triggered the + # error. Save output so it can be stored in the exception string along with context. + $output = tar -xf $file -C $extractDir 2>&1 + # Handle NZEC manually rather than using Exit-IfNZEC: we are in a background job, so we + # don't have access to the outer scope. + if ($LASTEXITCODE -ne 0) { + throw "Error extracting '$file': non-zero exit code ($LASTEXITCODE). Output: '$output'" + } + + Write-Host "Extracted to $extractDir" + } + } + + Receive-Job $jobs -Wait + } +} +catch { + Write-Host $_ + Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_ + ExitWithExitCode 1 +} diff --git a/eng/common/sdl/extract-artifact-packages.ps1 b/eng/common/sdl/extract-artifact-packages.ps1 new file mode 100644 index 0000000000..7f28d9c59e --- /dev/null +++ b/eng/common/sdl/extract-artifact-packages.ps1 @@ -0,0 +1,80 @@ +param( + [Parameter(Mandatory=$true)][string] $InputPath, # Full path to directory where artifact packages are stored + [Parameter(Mandatory=$true)][string] $ExtractPath # Full path to directory where the packages will be extracted +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 2.0 + +$disableConfigureToolsetImport = $true + +function ExtractArtifacts { + if (!(Test-Path $InputPath)) { + Write-Host "Input Path does not exist: $InputPath" + ExitWithExitCode 0 + } + $Jobs = @() + Get-ChildItem "$InputPath\*.nupkg" | + ForEach-Object { + $Jobs += Start-Job -ScriptBlock $ExtractPackage -ArgumentList $_.FullName + } + + foreach ($Job in $Jobs) { + Wait-Job -Id $Job.Id | Receive-Job + } +} + +try { + # `tools.ps1` checks $ci to perform some actions. Since the SDL + # scripts don't necessarily execute in the same agent that run the + # build.ps1/sh script this variable isn't automatically set. + $ci = $true + . $PSScriptRoot\..\tools.ps1 + + $ExtractPackage = { + param( + [string] $PackagePath # Full path to a NuGet package + ) + + if (!(Test-Path $PackagePath)) { + Write-PipelineTelemetryError -Category 'Build' -Message "Input file does not exist: $PackagePath" + ExitWithExitCode 1 + } + + $RelevantExtensions = @('.dll', '.exe', '.pdb') + Write-Host -NoNewLine 'Extracting ' ([System.IO.Path]::GetFileName($PackagePath)) '...' + + $PackageId = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath) + $ExtractPath = Join-Path -Path $using:ExtractPath -ChildPath $PackageId + + Add-Type -AssemblyName System.IO.Compression.FileSystem + + [System.IO.Directory]::CreateDirectory($ExtractPath); + + try { + $zip = [System.IO.Compression.ZipFile]::OpenRead($PackagePath) + + $zip.Entries | + Where-Object {$RelevantExtensions -contains [System.IO.Path]::GetExtension($_.Name)} | + ForEach-Object { + $TargetFile = Join-Path -Path $ExtractPath -ChildPath $_.Name + + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $TargetFile, $true) + } + } + catch { + Write-Host $_ + Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_ + ExitWithExitCode 1 + } + finally { + $zip.Dispose() + } + } + Measure-Command { ExtractArtifacts } +} +catch { + Write-Host $_ + Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_ + ExitWithExitCode 1 +} diff --git a/eng/common/sdl/init-sdl.ps1 b/eng/common/sdl/init-sdl.ps1 new file mode 100644 index 0000000000..3ac1d92b37 --- /dev/null +++ b/eng/common/sdl/init-sdl.ps1 @@ -0,0 +1,55 @@ +Param( + [string] $GuardianCliLocation, + [string] $Repository, + [string] $BranchName='master', + [string] $WorkingDirectory, + [string] $AzureDevOpsAccessToken, + [string] $GuardianLoggerLevel='Standard' +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 2.0 +$disableConfigureToolsetImport = $true +$global:LASTEXITCODE = 0 + +# `tools.ps1` checks $ci to perform some actions. Since the SDL +# scripts don't necessarily execute in the same agent that run the +# build.ps1/sh script this variable isn't automatically set. +$ci = $true +. $PSScriptRoot\..\tools.ps1 + +# Don't display the console progress UI - it's a huge perf hit +$ProgressPreference = 'SilentlyContinue' + +# Construct basic auth from AzDO access token; construct URI to the repository's gdn folder stored in that repository; construct location of zip file +$encodedPat = [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$AzureDevOpsAccessToken")) +$escapedRepository = [Uri]::EscapeDataString("/$Repository/$BranchName/.gdn") +$uri = "https://dev.azure.com/dnceng/internal/_apis/git/repositories/sdl-tool-cfg/Items?path=$escapedRepository&versionDescriptor[versionOptions]=0&`$format=zip&api-version=5.0" +$zipFile = "$WorkingDirectory/gdn.zip" + +Add-Type -AssemblyName System.IO.Compression.FileSystem +$gdnFolder = (Join-Path $WorkingDirectory '.gdn') + +try { + # if the folder does not exist, we'll do a guardian init and push it to the remote repository + Write-Host 'Initializing Guardian...' + Write-Host "$GuardianCliLocation init --working-directory $WorkingDirectory --logger-level $GuardianLoggerLevel" + & $GuardianCliLocation init --working-directory $WorkingDirectory --logger-level $GuardianLoggerLevel + if ($LASTEXITCODE -ne 0) { + Write-PipelineTelemetryError -Force -Category 'Build' -Message "Guardian init failed with exit code $LASTEXITCODE." + ExitWithExitCode $LASTEXITCODE + } + # We create the mainbaseline so it can be edited later + Write-Host "$GuardianCliLocation baseline --working-directory $WorkingDirectory --name mainbaseline" + & $GuardianCliLocation baseline --working-directory $WorkingDirectory --name mainbaseline + if ($LASTEXITCODE -ne 0) { + Write-PipelineTelemetryError -Force -Category 'Build' -Message "Guardian baseline failed with exit code $LASTEXITCODE." + ExitWithExitCode $LASTEXITCODE + } + ExitWithExitCode 0 +} +catch { + Write-Host $_.ScriptStackTrace + Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_ + ExitWithExitCode 1 +} diff --git a/eng/common/sdl/packages.config b/eng/common/sdl/packages.config new file mode 100644 index 0000000000..4585cfd6bb --- /dev/null +++ b/eng/common/sdl/packages.config @@ -0,0 +1,4 @@ + + + + diff --git a/eng/common/sdl/run-sdl.ps1 b/eng/common/sdl/run-sdl.ps1 new file mode 100644 index 0000000000..2eac8c78f1 --- /dev/null +++ b/eng/common/sdl/run-sdl.ps1 @@ -0,0 +1,49 @@ +Param( + [string] $GuardianCliLocation, + [string] $WorkingDirectory, + [string] $GdnFolder, + [string] $UpdateBaseline, + [string] $GuardianLoggerLevel='Standard' +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 2.0 +$disableConfigureToolsetImport = $true +$global:LASTEXITCODE = 0 + +try { + # `tools.ps1` checks $ci to perform some actions. Since the SDL + # scripts don't necessarily execute in the same agent that run the + # build.ps1/sh script this variable isn't automatically set. + $ci = $true + . $PSScriptRoot\..\tools.ps1 + + # We store config files in the r directory of .gdn + $gdnConfigPath = Join-Path $GdnFolder 'r' + $ValidPath = Test-Path $GuardianCliLocation + + if ($ValidPath -eq $False) + { + Write-PipelineTelemetryError -Force -Category 'Sdl' -Message "Invalid Guardian CLI Location." + ExitWithExitCode 1 + } + + $gdnConfigFiles = Get-ChildItem $gdnConfigPath -Recurse -Include '*.gdnconfig' + Write-Host "Discovered Guardian config files:" + $gdnConfigFiles | Out-String | Write-Host + + Exec-BlockVerbosely { + & $GuardianCliLocation run ` + --working-directory $WorkingDirectory ` + --baseline mainbaseline ` + --update-baseline $UpdateBaseline ` + --logger-level $GuardianLoggerLevel ` + --config @gdnConfigFiles + Exit-IfNZEC "Sdl" + } +} +catch { + Write-Host $_.ScriptStackTrace + Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_ + ExitWithExitCode 1 +} diff --git a/eng/common/sdl/sdl.ps1 b/eng/common/sdl/sdl.ps1 new file mode 100644 index 0000000000..648c5068d7 --- /dev/null +++ b/eng/common/sdl/sdl.ps1 @@ -0,0 +1,38 @@ + +function Install-Gdn { + param( + [Parameter(Mandatory=$true)] + [string]$Path, + + # If omitted, install the latest version of Guardian, otherwise install that specific version. + [string]$Version + ) + + $ErrorActionPreference = 'Stop' + Set-StrictMode -Version 2.0 + $disableConfigureToolsetImport = $true + $global:LASTEXITCODE = 0 + + # `tools.ps1` checks $ci to perform some actions. Since the SDL + # scripts don't necessarily execute in the same agent that run the + # build.ps1/sh script this variable isn't automatically set. + $ci = $true + . $PSScriptRoot\..\tools.ps1 + + $argumentList = @("install", "Microsoft.Guardian.Cli", "-Source https://securitytools.pkgs.visualstudio.com/_packaging/Guardian/nuget/v3/index.json", "-OutputDirectory $Path", "-NonInteractive", "-NoCache") + + if ($Version) { + $argumentList += "-Version $Version" + } + + Start-Process nuget -Verbose -ArgumentList $argumentList -NoNewWindow -Wait + + $gdnCliPath = Get-ChildItem -Filter guardian.cmd -Recurse -Path $Path + + if (!$gdnCliPath) + { + Write-PipelineTelemetryError -Category 'Sdl' -Message 'Failure installing Guardian' + } + + return $gdnCliPath.FullName +} \ No newline at end of file diff --git a/eng/common/templates/job/execute-sdl.yml b/eng/common/templates/job/execute-sdl.yml new file mode 100644 index 0000000000..7aabaa1801 --- /dev/null +++ b/eng/common/templates/job/execute-sdl.yml @@ -0,0 +1,134 @@ +parameters: + enable: 'false' # Whether the SDL validation job should execute or not + overrideParameters: '' # Optional: to override values for parameters. + additionalParameters: '' # Optional: parameters that need user specific values eg: '-SourceToolsList @("abc","def") -ArtifactToolsList @("ghi","jkl")' + # Optional: if specified, restore and use this version of Guardian instead of the default. + overrideGuardianVersion: '' + # Optional: if true, publish the '.gdn' folder as a pipeline artifact. This can help with in-depth + # diagnosis of problems with specific tool configurations. + publishGuardianDirectoryToPipeline: false + # The script to run to execute all SDL tools. Use this if you want to use a script to define SDL + # parameters rather than relying on YAML. It may be better to use a local script, because you can + # reproduce results locally without piecing together a command based on the YAML. + executeAllSdlToolsScript: 'eng/common/sdl/execute-all-sdl-tools.ps1' + # There is some sort of bug (has been reported) in Azure DevOps where if this parameter is named + # 'continueOnError', the parameter value is not correctly picked up. + # This can also be remedied by the caller (post-build.yml) if it does not use a nested parameter + sdlContinueOnError: false # optional: determines whether to continue the build if the step errors; + # optional: determines if build artifacts should be downloaded. + downloadArtifacts: true + # optional: determines if this job should search the directory of downloaded artifacts for + # 'tar.gz' and 'zip' archive files and extract them before running SDL validation tasks. + extractArchiveArtifacts: false + dependsOn: '' # Optional: dependencies of the job + artifactNames: '' # Optional: patterns supplied to DownloadBuildArtifacts + # Usage: + # artifactNames: + # - 'BlobArtifacts' + # - 'Artifacts_Windows_NT_Release' + # Optional: download a list of pipeline artifacts. 'downloadArtifacts' controls build artifacts, + # not pipeline artifacts, so doesn't affect the use of this parameter. + pipelineArtifactNames: [] + +jobs: +- job: Run_SDL + dependsOn: ${{ parameters.dependsOn }} + displayName: Run SDL tool + condition: and(succeededOrFailed(), eq( ${{ parameters.enable }}, 'true')) + variables: + - group: DotNet-VSTS-Bot + - name: AzDOProjectName + value: ${{ parameters.AzDOProjectName }} + - name: AzDOPipelineId + value: ${{ parameters.AzDOPipelineId }} + - name: AzDOBuildId + value: ${{ parameters.AzDOBuildId }} + - template: /eng/common/templates/variables/sdl-variables.yml + - name: GuardianVersion + value: ${{ coalesce(parameters.overrideGuardianVersion, '$(DefaultGuardianVersion)') }} + - template: /eng/common/templates/variables/pool-providers.yml + pool: + # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + name: VSEngSS-MicroBuild2022-1ES + demands: Cmd + # If it's not devdiv, it's dnceng + ${{ if ne(variables['System.TeamProject'], 'DevDiv') }}: + name: $(DncEngInternalBuildPool) + demands: ImageOverride -equals windows.vs2019.amd64 + steps: + - checkout: self + clean: true + + # If the template caller didn't provide an AzDO parameter, set them all up as Maestro vars. + - ${{ if not(and(parameters.AzDOProjectName, parameters.AzDOPipelineId, parameters.AzDOBuildId)) }}: + - template: /eng/common/templates/post-build/setup-maestro-vars.yml + + - ${{ if ne(parameters.downloadArtifacts, 'false')}}: + - ${{ if ne(parameters.artifactNames, '') }}: + - ${{ each artifactName in parameters.artifactNames }}: + - task: DownloadBuildArtifacts@0 + displayName: Download Build Artifacts + inputs: + buildType: specific + buildVersionToDownload: specific + project: $(AzDOProjectName) + pipeline: $(AzDOPipelineId) + buildId: $(AzDOBuildId) + artifactName: ${{ artifactName }} + downloadPath: $(Build.ArtifactStagingDirectory)\artifacts + checkDownloadedFiles: true + - ${{ if eq(parameters.artifactNames, '') }}: + - task: DownloadBuildArtifacts@0 + displayName: Download Build Artifacts + inputs: + buildType: specific + buildVersionToDownload: specific + project: $(AzDOProjectName) + pipeline: $(AzDOPipelineId) + buildId: $(AzDOBuildId) + downloadType: specific files + itemPattern: "**" + downloadPath: $(Build.ArtifactStagingDirectory)\artifacts + checkDownloadedFiles: true + + - ${{ each artifactName in parameters.pipelineArtifactNames }}: + - task: DownloadPipelineArtifact@2 + displayName: Download Pipeline Artifacts + inputs: + buildType: specific + buildVersionToDownload: specific + project: $(AzDOProjectName) + pipeline: $(AzDOPipelineId) + buildId: $(AzDOBuildId) + artifactName: ${{ artifactName }} + downloadPath: $(Build.ArtifactStagingDirectory)\artifacts + checkDownloadedFiles: true + + - powershell: eng/common/sdl/extract-artifact-packages.ps1 + -InputPath $(Build.ArtifactStagingDirectory)\artifacts\BlobArtifacts + -ExtractPath $(Build.ArtifactStagingDirectory)\artifacts\BlobArtifacts + displayName: Extract Blob Artifacts + continueOnError: ${{ parameters.sdlContinueOnError }} + + - powershell: eng/common/sdl/extract-artifact-packages.ps1 + -InputPath $(Build.ArtifactStagingDirectory)\artifacts\PackageArtifacts + -ExtractPath $(Build.ArtifactStagingDirectory)\artifacts\PackageArtifacts + displayName: Extract Package Artifacts + continueOnError: ${{ parameters.sdlContinueOnError }} + + - ${{ if ne(parameters.extractArchiveArtifacts, 'false') }}: + - powershell: eng/common/sdl/extract-artifact-archives.ps1 + -InputPath $(Build.ArtifactStagingDirectory)\artifacts + -ExtractPath $(Build.ArtifactStagingDirectory)\artifacts + displayName: Extract Archive Artifacts + continueOnError: ${{ parameters.sdlContinueOnError }} + + - template: /eng/common/templates/steps/execute-sdl.yml + parameters: + overrideGuardianVersion: ${{ parameters.overrideGuardianVersion }} + executeAllSdlToolsScript: ${{ parameters.executeAllSdlToolsScript }} + overrideParameters: ${{ parameters.overrideParameters }} + additionalParameters: ${{ parameters.additionalParameters }} + publishGuardianDirectoryToPipeline: ${{ parameters.publishGuardianDirectoryToPipeline }} + sdlContinueOnError: ${{ parameters.sdlContinueOnError }} diff --git a/eng/common/templates/job/job.yml b/eng/common/templates/job/job.yml new file mode 100644 index 0000000000..44ad26abf5 --- /dev/null +++ b/eng/common/templates/job/job.yml @@ -0,0 +1,251 @@ +# Internal resources (telemetry, microbuild) can only be accessed from non-public projects, +# and some (Microbuild) should only be applied to non-PR cases for internal builds. + +parameters: +# Job schema parameters - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#job + cancelTimeoutInMinutes: '' + condition: '' + container: '' + continueOnError: false + dependsOn: '' + displayName: '' + pool: '' + steps: [] + strategy: '' + timeoutInMinutes: '' + variables: [] + workspace: '' + +# Job base template specific parameters + # See schema documentation - https://github.com/dotnet/arcade/blob/master/Documentation/AzureDevOps/TemplateSchema.md + artifacts: '' + enableMicrobuild: false + enablePublishBuildArtifacts: false + enablePublishBuildAssets: false + enablePublishTestResults: false + enablePublishUsingPipelines: false + enableBuildRetry: false + disableComponentGovernance: '' + componentGovernanceIgnoreDirectories: '' + mergeTestResults: false + testRunTitle: '' + testResultsFormat: '' + name: '' + preSteps: [] + runAsPublic: false +# Sbom related params + enableSbom: true + PackageVersion: 7.0.0 + BuildDropPath: '$(Build.SourcesDirectory)/artifacts' + +jobs: +- job: ${{ parameters.name }} + + ${{ if ne(parameters.cancelTimeoutInMinutes, '') }}: + cancelTimeoutInMinutes: ${{ parameters.cancelTimeoutInMinutes }} + + ${{ if ne(parameters.condition, '') }}: + condition: ${{ parameters.condition }} + + ${{ if ne(parameters.container, '') }}: + container: ${{ parameters.container }} + + ${{ if ne(parameters.continueOnError, '') }}: + continueOnError: ${{ parameters.continueOnError }} + + ${{ if ne(parameters.dependsOn, '') }}: + dependsOn: ${{ parameters.dependsOn }} + + ${{ if ne(parameters.displayName, '') }}: + displayName: ${{ parameters.displayName }} + + ${{ if ne(parameters.pool, '') }}: + pool: ${{ parameters.pool }} + + ${{ if ne(parameters.strategy, '') }}: + strategy: ${{ parameters.strategy }} + + ${{ if ne(parameters.timeoutInMinutes, '') }}: + timeoutInMinutes: ${{ parameters.timeoutInMinutes }} + + variables: + - ${{ if ne(parameters.enableTelemetry, 'false') }}: + - name: DOTNET_CLI_TELEMETRY_PROFILE + value: '$(Build.Repository.Uri)' + - ${{ if eq(parameters.enableRichCodeNavigation, 'true') }}: + - name: EnableRichCodeNavigation + value: 'true' + - ${{ each variable in parameters.variables }}: + # handle name-value variable syntax + # example: + # - name: [key] + # value: [value] + - ${{ if ne(variable.name, '') }}: + - name: ${{ variable.name }} + value: ${{ variable.value }} + + # handle variable groups + - ${{ if ne(variable.group, '') }}: + - group: ${{ variable.group }} + + # handle template variable syntax + # example: + # - template: path/to/template.yml + # parameters: + # [key]: [value] + - ${{ if ne(variable.template, '') }}: + - template: ${{ variable.template }} + ${{ if ne(variable.parameters, '') }}: + parameters: ${{ variable.parameters }} + + # handle key-value variable syntax. + # example: + # - [key]: [value] + - ${{ if and(eq(variable.name, ''), eq(variable.group, ''), eq(variable.template, '')) }}: + - ${{ each pair in variable }}: + - name: ${{ pair.key }} + value: ${{ pair.value }} + + # DotNet-HelixApi-Access provides 'HelixApiAccessToken' for internal builds + - ${{ if and(eq(parameters.enableTelemetry, 'true'), eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - group: DotNet-HelixApi-Access + + ${{ if ne(parameters.workspace, '') }}: + workspace: ${{ parameters.workspace }} + + steps: + - ${{ if ne(parameters.preSteps, '') }}: + - ${{ each preStep in parameters.preSteps }}: + - ${{ preStep }} + + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - ${{ if eq(parameters.enableMicrobuild, 'true') }}: + - task: MicroBuildSigningPlugin@3 + displayName: Install MicroBuild plugin + inputs: + signType: $(_SignType) + zipSources: false + feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json + env: + TeamName: $(_TeamName) + continueOnError: ${{ parameters.continueOnError }} + condition: and(succeeded(), in(variables['_SignType'], 'real', 'test'), eq(variables['Agent.Os'], 'Windows_NT')) + + - ${{ if and(eq(parameters.runAsPublic, 'false'), eq(variables['System.TeamProject'], 'internal')) }}: + - task: NuGetAuthenticate@0 + + - ${{ if and(ne(parameters.artifacts.download, 'false'), ne(parameters.artifacts.download, '')) }}: + - task: DownloadPipelineArtifact@2 + inputs: + buildType: current + artifactName: ${{ coalesce(parameters.artifacts.download.name, 'Artifacts_$(Agent.OS)_$(_BuildConfig)') }} + targetPath: ${{ coalesce(parameters.artifacts.download.path, 'artifacts') }} + itemPattern: ${{ coalesce(parameters.artifacts.download.pattern, '**') }} + + - ${{ each step in parameters.steps }}: + - ${{ step }} + + - ${{ if eq(parameters.enableRichCodeNavigation, true) }}: + - task: RichCodeNavIndexer@0 + displayName: RichCodeNav Upload + inputs: + languages: ${{ coalesce(parameters.richCodeNavigationLanguage, 'csharp') }} + environment: ${{ coalesce(parameters.richCodeNavigationEnvironment, 'production') }} + richNavLogOutputDirectory: $(Build.SourcesDirectory)/artifacts/bin + uploadRichNavArtifacts: ${{ coalesce(parameters.richCodeNavigationUploadArtifacts, false) }} + continueOnError: true + + - template: /eng/common/templates/steps/component-governance.yml + parameters: + ${{ if eq(parameters.disableComponentGovernance, '') }}: + ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest'), eq(parameters.runAsPublic, 'false'), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/dotnet/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/microsoft/'), eq(variables['Build.SourceBranch'], 'refs/heads/main'))) }}: + disableComponentGovernance: false + ${{ else }}: + disableComponentGovernance: true + ${{ else }}: + disableComponentGovernance: ${{ parameters.disableComponentGovernance }} + componentGovernanceIgnoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} + + - ${{ if eq(parameters.enableMicrobuild, 'true') }}: + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: MicroBuildCleanup@1 + displayName: Execute Microbuild cleanup tasks + condition: and(always(), in(variables['_SignType'], 'real', 'test'), eq(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} + env: + TeamName: $(_TeamName) + + - ${{ if ne(parameters.artifacts.publish, '') }}: + - ${{ if and(ne(parameters.artifacts.publish.artifacts, 'false'), ne(parameters.artifacts.publish.artifacts, '')) }}: + - task: CopyFiles@2 + displayName: Gather binaries for publish to artifacts + inputs: + SourceFolder: 'artifacts/bin' + Contents: '**' + TargetFolder: '$(Build.ArtifactStagingDirectory)/artifacts/bin' + - task: CopyFiles@2 + displayName: Gather packages for publish to artifacts + inputs: + SourceFolder: 'artifacts/packages' + Contents: '**' + TargetFolder: '$(Build.ArtifactStagingDirectory)/artifacts/packages' + - task: PublishBuildArtifacts@1 + displayName: Publish pipeline artifacts + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts' + PublishLocation: Container + ArtifactName: ${{ coalesce(parameters.artifacts.publish.artifacts.name , 'Artifacts_$(Agent.Os)_$(_BuildConfig)') }} + continueOnError: true + condition: always() + - ${{ if and(ne(parameters.artifacts.publish.logs, 'false'), ne(parameters.artifacts.publish.logs, '')) }}: + - publish: artifacts/log + artifact: ${{ coalesce(parameters.artifacts.publish.logs.name, 'Logs_Build_$(Agent.Os)_$(_BuildConfig)') }} + displayName: Publish logs + continueOnError: true + condition: always() + + - ${{ if ne(parameters.enablePublishBuildArtifacts, 'false') }}: + - task: PublishBuildArtifacts@1 + displayName: Publish Logs + inputs: + PathtoPublish: '$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)' + PublishLocation: Container + ArtifactName: ${{ coalesce(parameters.enablePublishBuildArtifacts.artifactName, '$(Agent.Os)_$(Agent.JobName)' ) }} + continueOnError: true + condition: always() + + - ${{ if or(and(eq(parameters.enablePublishTestResults, 'true'), eq(parameters.testResultsFormat, '')), eq(parameters.testResultsFormat, 'xunit')) }}: + - task: PublishTestResults@2 + displayName: Publish XUnit Test Results + inputs: + testResultsFormat: 'xUnit' + testResultsFiles: '*.xml' + searchFolder: '$(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)' + testRunTitle: ${{ coalesce(parameters.testRunTitle, parameters.name, '$(System.JobName)') }}-xunit + mergeTestResults: ${{ parameters.mergeTestResults }} + continueOnError: true + condition: always() + - ${{ if or(and(eq(parameters.enablePublishTestResults, 'true'), eq(parameters.testResultsFormat, '')), eq(parameters.testResultsFormat, 'vstest')) }}: + - task: PublishTestResults@2 + displayName: Publish TRX Test Results + inputs: + testResultsFormat: 'VSTest' + testResultsFiles: '*.trx' + searchFolder: '$(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)' + testRunTitle: ${{ coalesce(parameters.testRunTitle, parameters.name, '$(System.JobName)') }}-trx + mergeTestResults: ${{ parameters.mergeTestResults }} + continueOnError: true + condition: always() + + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest'), eq(parameters.enableSbom, 'true')) }}: + - template: /eng/common/templates/steps/generate-sbom.yml + parameters: + PackageVersion: ${{ parameters.packageVersion}} + BuildDropPath: ${{ parameters.buildDropPath }} + IgnoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} + + - ${{ if eq(parameters.enableBuildRetry, 'true') }}: + - publish: $(Build.SourcesDirectory)\eng\common\BuildConfiguration + artifact: BuildConfiguration + displayName: Publish build retry configuration + continueOnError: true diff --git a/eng/common/templates/job/onelocbuild.yml b/eng/common/templates/job/onelocbuild.yml new file mode 100644 index 0000000000..60ab00c4de --- /dev/null +++ b/eng/common/templates/job/onelocbuild.yml @@ -0,0 +1,109 @@ +parameters: + # Optional: dependencies of the job + dependsOn: '' + + # Optional: A defined YAML pool - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#pool + pool: '' + + CeapexPat: $(dn-bot-ceapex-package-r) # PAT for the loc AzDO instance https://dev.azure.com/ceapex + GithubPat: $(BotAccount-dotnet-bot-repo-PAT) + + SourcesDirectory: $(Build.SourcesDirectory) + CreatePr: true + AutoCompletePr: false + ReusePr: true + UseLfLineEndings: true + UseCheckedInLocProjectJson: false + SkipLocProjectJsonGeneration: false + LanguageSet: VS_Main_Languages + LclSource: lclFilesInRepo + LclPackageId: '' + RepoType: gitHub + GitHubOrg: dotnet + MirrorRepo: '' + MirrorBranch: main + condition: '' + JobNameSuffix: '' + +jobs: +- job: OneLocBuild${{ parameters.JobNameSuffix }} + + dependsOn: ${{ parameters.dependsOn }} + + displayName: OneLocBuild${{ parameters.JobNameSuffix }} + + variables: + - group: OneLocBuildVariables # Contains the CeapexPat and GithubPat + - name: _GenerateLocProjectArguments + value: -SourcesDirectory ${{ parameters.SourcesDirectory }} + -LanguageSet "${{ parameters.LanguageSet }}" + -CreateNeutralXlfs + - ${{ if eq(parameters.UseCheckedInLocProjectJson, 'true') }}: + - name: _GenerateLocProjectArguments + value: ${{ variables._GenerateLocProjectArguments }} -UseCheckedInLocProjectJson + - template: /eng/common/templates/variables/pool-providers.yml + + ${{ if ne(parameters.pool, '') }}: + pool: ${{ parameters.pool }} + ${{ if eq(parameters.pool, '') }}: + pool: + # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + name: VSEngSS-MicroBuild2022-1ES + demands: Cmd + # If it's not devdiv, it's dnceng + ${{ if ne(variables['System.TeamProject'], 'DevDiv') }}: + name: $(DncEngInternalBuildPool) + demands: ImageOverride -equals windows.vs2019.amd64 + + steps: + - ${{ if ne(parameters.SkipLocProjectJsonGeneration, 'true') }}: + - task: Powershell@2 + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/generate-locproject.ps1 + arguments: $(_GenerateLocProjectArguments) + displayName: Generate LocProject.json + condition: ${{ parameters.condition }} + + - task: OneLocBuild@2 + displayName: OneLocBuild + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + inputs: + locProj: eng/Localize/LocProject.json + outDir: $(Build.ArtifactStagingDirectory) + lclSource: ${{ parameters.LclSource }} + lclPackageId: ${{ parameters.LclPackageId }} + isCreatePrSelected: ${{ parameters.CreatePr }} + isAutoCompletePrSelected: ${{ parameters.AutoCompletePr }} + ${{ if eq(parameters.CreatePr, true) }}: + isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }} + ${{ if eq(parameters.RepoType, 'gitHub') }}: + isShouldReusePrSelected: ${{ parameters.ReusePr }} + packageSourceAuth: patAuth + patVariable: ${{ parameters.CeapexPat }} + ${{ if eq(parameters.RepoType, 'gitHub') }}: + repoType: ${{ parameters.RepoType }} + gitHubPatVariable: "${{ parameters.GithubPat }}" + ${{ if ne(parameters.MirrorRepo, '') }}: + isMirrorRepoSelected: true + gitHubOrganization: ${{ parameters.GitHubOrg }} + mirrorRepo: ${{ parameters.MirrorRepo }} + mirrorBranch: ${{ parameters.MirrorBranch }} + condition: ${{ parameters.condition }} + + - task: PublishBuildArtifacts@1 + displayName: Publish Localization Files + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)/loc' + PublishLocation: Container + ArtifactName: Loc + condition: ${{ parameters.condition }} + + - task: PublishBuildArtifacts@1 + displayName: Publish LocProject.json + inputs: + PathtoPublish: '$(Build.SourcesDirectory)/eng/Localize/' + PublishLocation: Container + ArtifactName: Loc + condition: ${{ parameters.condition }} \ No newline at end of file diff --git a/eng/common/templates/job/publish-build-assets.yml b/eng/common/templates/job/publish-build-assets.yml new file mode 100644 index 0000000000..42017109f3 --- /dev/null +++ b/eng/common/templates/job/publish-build-assets.yml @@ -0,0 +1,151 @@ +parameters: + configuration: 'Debug' + + # Optional: condition for the job to run + condition: '' + + # Optional: 'true' if future jobs should run even if this job fails + continueOnError: false + + # Optional: dependencies of the job + dependsOn: '' + + # Optional: Include PublishBuildArtifacts task + enablePublishBuildArtifacts: false + + # Optional: A defined YAML pool - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#pool + pool: {} + + # Optional: should run as a public build even in the internal project + # if 'true', the build won't run any of the internal only steps, even if it is running in non-public projects. + runAsPublic: false + + # Optional: whether the build's artifacts will be published using release pipelines or direct feed publishing + publishUsingPipelines: false + + # Optional: whether the build's artifacts will be published using release pipelines or direct feed publishing + publishAssetsImmediately: false + + artifactsPublishingAdditionalParameters: '' + + signingValidationAdditionalParameters: '' + +jobs: +- job: Asset_Registry_Publish + + dependsOn: ${{ parameters.dependsOn }} + timeoutInMinutes: 150 + + ${{ if eq(parameters.publishAssetsImmediately, 'true') }}: + displayName: Publish Assets + ${{ else }}: + displayName: Publish to Build Asset Registry + + variables: + - template: /eng/common/templates/variables/pool-providers.yml + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - group: Publish-Build-Assets + - group: AzureDevOps-Artifact-Feeds-Pats + - name: runCodesignValidationInjection + value: false + - ${{ if eq(parameters.publishAssetsImmediately, 'true') }}: + - template: /eng/common/templates/post-build/common-variables.yml + + pool: + # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + name: VSEngSS-MicroBuild2022-1ES + demands: Cmd + # If it's not devdiv, it's dnceng + ${{ if ne(variables['System.TeamProject'], 'DevDiv') }}: + name: $(DncEngInternalBuildPool) + demands: ImageOverride -equals windows.vs2019.amd64 + + steps: + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: DownloadBuildArtifacts@0 + displayName: Download artifact + inputs: + artifactName: AssetManifests + downloadPath: '$(Build.StagingDirectory)/Download' + checkDownloadedFiles: true + condition: ${{ parameters.condition }} + continueOnError: ${{ parameters.continueOnError }} + + - task: NuGetAuthenticate@0 + + - task: PowerShell@2 + displayName: Publish Build Assets + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishBuildAssets -restore -msbuildEngine dotnet + /p:ManifestsPath='$(Build.StagingDirectory)/Download/AssetManifests' + /p:BuildAssetRegistryToken=$(MaestroAccessToken) + /p:MaestroApiEndpoint=https://maestro-prod.westus2.cloudapp.azure.com + /p:PublishUsingPipelines=${{ parameters.publishUsingPipelines }} + /p:OfficialBuildId=$(Build.BuildNumber) + condition: ${{ parameters.condition }} + continueOnError: ${{ parameters.continueOnError }} + + - task: powershell@2 + displayName: Create ReleaseConfigs Artifact + inputs: + targetType: inline + script: | + Add-Content -Path "$(Build.StagingDirectory)/ReleaseConfigs.txt" -Value $(BARBuildId) + Add-Content -Path "$(Build.StagingDirectory)/ReleaseConfigs.txt" -Value "$(DefaultChannels)" + Add-Content -Path "$(Build.StagingDirectory)/ReleaseConfigs.txt" -Value $(IsStableBuild) + + - task: PublishBuildArtifacts@1 + displayName: Publish ReleaseConfigs Artifact + inputs: + PathtoPublish: '$(Build.StagingDirectory)/ReleaseConfigs.txt' + PublishLocation: Container + ArtifactName: ReleaseConfigs + + - task: powershell@2 + displayName: Check if SymbolPublishingExclusionsFile.txt exists + inputs: + targetType: inline + script: | + $symbolExclusionfile = "$(Build.SourcesDirectory)/eng/SymbolPublishingExclusionsFile.txt" + if(Test-Path -Path $symbolExclusionfile) + { + Write-Host "SymbolExclusionFile exists" + Write-Host "##vso[task.setvariable variable=SymbolExclusionFile]true" + } + else{ + Write-Host "Symbols Exclusion file does not exists" + Write-Host "##vso[task.setvariable variable=SymbolExclusionFile]false" + } + + - task: PublishBuildArtifacts@1 + displayName: Publish SymbolPublishingExclusionsFile Artifact + condition: eq(variables['SymbolExclusionFile'], 'true') + inputs: + PathtoPublish: '$(Build.SourcesDirectory)/eng/SymbolPublishingExclusionsFile.txt' + PublishLocation: Container + ArtifactName: ReleaseConfigs + + - ${{ if eq(parameters.publishAssetsImmediately, 'true') }}: + - template: /eng/common/templates/post-build/setup-maestro-vars.yml + parameters: + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} + + - task: PowerShell@2 + displayName: Publish Using Darc + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/publish-using-darc.ps1 + arguments: -BuildId $(BARBuildId) + -PublishingInfraVersion 3 + -AzdoToken '$(publishing-dnceng-devdiv-code-r-build-re)' + -MaestroToken '$(MaestroApiAccessToken)' + -WaitPublishingFinish true + -ArtifactsPublishingAdditionalParameters '${{ parameters.artifactsPublishingAdditionalParameters }}' + -SymbolPublishingAdditionalParameters '${{ parameters.symbolPublishingAdditionalParameters }}' + + - ${{ if eq(parameters.enablePublishBuildArtifacts, 'true') }}: + - template: /eng/common/templates/steps/publish-logs.yml + parameters: + JobLabel: 'Publish_Artifacts_Logs' diff --git a/eng/common/templates/job/source-build.yml b/eng/common/templates/job/source-build.yml new file mode 100644 index 0000000000..8a3deef2b7 --- /dev/null +++ b/eng/common/templates/job/source-build.yml @@ -0,0 +1,66 @@ +parameters: + # This template adds arcade-powered source-build to CI. The template produces a server job with a + # default ID 'Source_Build_Complete' to put in a dependency list if necessary. + + # Specifies the prefix for source-build jobs added to pipeline. Use this if disambiguation needed. + jobNamePrefix: 'Source_Build' + + # Defines the platform on which to run the job. By default, a linux-x64 machine, suitable for + # managed-only repositories. This is an object with these properties: + # + # name: '' + # The name of the job. This is included in the job ID. + # targetRID: '' + # The name of the target RID to use, instead of the one auto-detected by Arcade. + # nonPortable: false + # Enables non-portable mode. This means a more specific RID (e.g. fedora.32-x64 rather than + # linux-x64), and compiling against distro-provided packages rather than portable ones. + # skipPublishValidation: false + # Disables publishing validation. By default, a check is performed to ensure no packages are + # published by source-build. + # container: '' + # A container to use. Runs in docker. + # pool: {} + # A pool to use. Runs directly on an agent. + # buildScript: '' + # Specifies the build script to invoke to perform the build in the repo. The default + # './build.sh' should work for typical Arcade repositories, but this is customizable for + # difficult situations. + # jobProperties: {} + # A list of job properties to inject at the top level, for potential extensibility beyond + # container and pool. + platform: {} + +jobs: +- job: ${{ parameters.jobNamePrefix }}_${{ parameters.platform.name }} + displayName: Source-Build (${{ parameters.platform.name }}) + + ${{ each property in parameters.platform.jobProperties }}: + ${{ property.key }}: ${{ property.value }} + + ${{ if ne(parameters.platform.container, '') }}: + container: ${{ parameters.platform.container }} + + ${{ if eq(parameters.platform.pool, '') }}: + # The default VM host AzDO pool. This should be capable of running Docker containers: almost all + # source-build builds run in Docker, including the default managed platform. + # /eng/common/templates/variables/pool-providers.yml can't be used here (some customers declare variables already), so duplicate its logic + pool: + ${{ if eq(variables['System.TeamProject'], 'public') }}: + name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore-Svc-Public' ), False, 'NetCore-Public')] + demands: ImageOverride -equals Build.Ubuntu.1804.Amd64.Open + + ${{ if eq(variables['System.TeamProject'], 'internal') }}: + name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore1ESPool-Svc-Internal'), False, 'NetCore1ESPool-Internal')] + demands: ImageOverride -equals Build.Ubuntu.1804.Amd64 + + ${{ if ne(parameters.platform.pool, '') }}: + pool: ${{ parameters.platform.pool }} + + workspace: + clean: all + + steps: + - template: /eng/common/templates/steps/source-build.yml + parameters: + platform: ${{ parameters.platform }} diff --git a/eng/common/templates/job/source-index-stage1.yml b/eng/common/templates/job/source-index-stage1.yml new file mode 100644 index 0000000000..b98202aa02 --- /dev/null +++ b/eng/common/templates/job/source-index-stage1.yml @@ -0,0 +1,67 @@ +parameters: + runAsPublic: false + sourceIndexPackageVersion: 1.0.1-20230228.2 + sourceIndexPackageSource: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json + sourceIndexBuildCommand: powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "eng/common/build.ps1 -restore -build -binarylog -ci" + preSteps: [] + binlogPath: artifacts/log/Debug/Build.binlog + condition: '' + dependsOn: '' + pool: '' + +jobs: +- job: SourceIndexStage1 + dependsOn: ${{ parameters.dependsOn }} + condition: ${{ parameters.condition }} + variables: + - name: SourceIndexPackageVersion + value: ${{ parameters.sourceIndexPackageVersion }} + - name: SourceIndexPackageSource + value: ${{ parameters.sourceIndexPackageSource }} + - name: BinlogPath + value: ${{ parameters.binlogPath }} + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - group: source-dot-net stage1 variables + - template: /eng/common/templates/variables/pool-providers.yml + + ${{ if ne(parameters.pool, '') }}: + pool: ${{ parameters.pool }} + ${{ if eq(parameters.pool, '') }}: + pool: + ${{ if eq(variables['System.TeamProject'], 'public') }}: + name: $(DncEngPublicBuildPool) + demands: ImageOverride -equals windows.vs2019.amd64.open + ${{ if eq(variables['System.TeamProject'], 'internal') }}: + name: $(DncEngInternalBuildPool) + demands: ImageOverride -equals windows.vs2019.amd64 + + steps: + - ${{ each preStep in parameters.preSteps }}: + - ${{ preStep }} + + - task: UseDotNet@2 + displayName: Use .NET Core SDK 6 + inputs: + packageType: sdk + version: 6.0.x + installationPath: $(Agent.TempDirectory)/dotnet + workingDirectory: $(Agent.TempDirectory) + + - script: | + $(Agent.TempDirectory)/dotnet/dotnet tool install BinLogToSln --version $(SourceIndexPackageVersion) --add-source $(SourceIndexPackageSource) --tool-path $(Agent.TempDirectory)/.source-index/tools + $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version $(SourceIndexPackageVersion) --add-source $(SourceIndexPackageSource) --tool-path $(Agent.TempDirectory)/.source-index/tools + displayName: Download Tools + # Set working directory to temp directory so 'dotnet' doesn't try to use global.json and use the repo's sdk. + workingDirectory: $(Agent.TempDirectory) + + - script: ${{ parameters.sourceIndexBuildCommand }} + displayName: Build Repository + + - script: $(Agent.TempDirectory)/.source-index/tools/BinLogToSln -i $(BinlogPath) -r $(Build.SourcesDirectory) -n $(Build.Repository.Name) -o .source-index/stage1output + displayName: Process Binlog into indexable sln + + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - script: $(Agent.TempDirectory)/.source-index/tools/UploadIndexStage1 -i .source-index/stage1output -n $(Build.Repository.Name) + displayName: Upload stage1 artifacts to source index + env: + BLOB_CONTAINER_URL: $(source-dot-net-stage1-blob-container-url) diff --git a/eng/common/templates/jobs/codeql-build.yml b/eng/common/templates/jobs/codeql-build.yml new file mode 100644 index 0000000000..f7dc5ea4aa --- /dev/null +++ b/eng/common/templates/jobs/codeql-build.yml @@ -0,0 +1,31 @@ +parameters: + # See schema documentation in /Documentation/AzureDevOps/TemplateSchema.md + continueOnError: false + # Required: A collection of jobs to run - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#job + jobs: [] + # Optional: if specified, restore and use this version of Guardian instead of the default. + overrideGuardianVersion: '' + +jobs: +- template: /eng/common/templates/jobs/jobs.yml + parameters: + enableMicrobuild: false + enablePublishBuildArtifacts: false + enablePublishTestResults: false + enablePublishBuildAssets: false + enablePublishUsingPipelines: false + enableTelemetry: true + + variables: + - group: Publish-Build-Assets + # The Guardian version specified in 'eng/common/sdl/packages.config'. This value must be kept in + # sync with the packages.config file. + - name: DefaultGuardianVersion + value: 0.109.0 + - name: GuardianPackagesConfigFile + value: $(Build.SourcesDirectory)\eng\common\sdl\packages.config + - name: GuardianVersion + value: ${{ coalesce(parameters.overrideGuardianVersion, '$(DefaultGuardianVersion)') }} + + jobs: ${{ parameters.jobs }} + diff --git a/eng/common/templates/jobs/jobs.yml b/eng/common/templates/jobs/jobs.yml new file mode 100644 index 0000000000..289bb2396c --- /dev/null +++ b/eng/common/templates/jobs/jobs.yml @@ -0,0 +1,97 @@ +parameters: + # See schema documentation in /Documentation/AzureDevOps/TemplateSchema.md + continueOnError: false + + # Optional: Include PublishBuildArtifacts task + enablePublishBuildArtifacts: false + + # Optional: Enable publishing using release pipelines + enablePublishUsingPipelines: false + + # Optional: Enable running the source-build jobs to build repo from source + enableSourceBuild: false + + # Optional: Parameters for source-build template. + # See /eng/common/templates/jobs/source-build.yml for options + sourceBuildParameters: [] + + graphFileGeneration: + # Optional: Enable generating the graph files at the end of the build + enabled: false + # Optional: Include toolset dependencies in the generated graph files + includeToolset: false + + # Required: A collection of jobs to run - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#job + jobs: [] + + # Optional: Override automatically derived dependsOn value for "publish build assets" job + publishBuildAssetsDependsOn: '' + + # Optional: Publish the assets as soon as the publish to BAR stage is complete, rather doing so in a separate stage. + publishAssetsImmediately: false + + # Optional: If using publishAssetsImmediately and additional parameters are needed, can be used to send along additional parameters (normally sent to post-build.yml) + artifactsPublishingAdditionalParameters: '' + signingValidationAdditionalParameters: '' + + # Optional: should run as a public build even in the internal project + # if 'true', the build won't run any of the internal only steps, even if it is running in non-public projects. + runAsPublic: false + + enableSourceIndex: false + sourceIndexParams: {} + +# Internal resources (telemetry, microbuild) can only be accessed from non-public projects, +# and some (Microbuild) should only be applied to non-PR cases for internal builds. + +jobs: +- ${{ each job in parameters.jobs }}: + - template: ../job/job.yml + parameters: + # pass along parameters + ${{ each parameter in parameters }}: + ${{ if ne(parameter.key, 'jobs') }}: + ${{ parameter.key }}: ${{ parameter.value }} + + # pass along job properties + ${{ each property in job }}: + ${{ if ne(property.key, 'job') }}: + ${{ property.key }}: ${{ property.value }} + + name: ${{ job.job }} + +- ${{ if eq(parameters.enableSourceBuild, true) }}: + - template: /eng/common/templates/jobs/source-build.yml + parameters: + allCompletedJobId: Source_Build_Complete + ${{ each parameter in parameters.sourceBuildParameters }}: + ${{ parameter.key }}: ${{ parameter.value }} + +- ${{ if eq(parameters.enableSourceIndex, 'true') }}: + - template: ../job/source-index-stage1.yml + parameters: + runAsPublic: ${{ parameters.runAsPublic }} + ${{ each parameter in parameters.sourceIndexParams }}: + ${{ parameter.key }}: ${{ parameter.value }} + +- ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - ${{ if or(eq(parameters.enablePublishBuildAssets, true), eq(parameters.artifacts.publish.manifests, 'true'), ne(parameters.artifacts.publish.manifests, '')) }}: + - template: ../job/publish-build-assets.yml + parameters: + continueOnError: ${{ parameters.continueOnError }} + dependsOn: + - ${{ if ne(parameters.publishBuildAssetsDependsOn, '') }}: + - ${{ each job in parameters.publishBuildAssetsDependsOn }}: + - ${{ job.job }} + - ${{ if eq(parameters.publishBuildAssetsDependsOn, '') }}: + - ${{ each job in parameters.jobs }}: + - ${{ job.job }} + - ${{ if eq(parameters.enableSourceBuild, true) }}: + - Source_Build_Complete + + runAsPublic: ${{ parameters.runAsPublic }} + publishUsingPipelines: ${{ parameters.enablePublishUsingPipelines }} + publishAssetsImmediately: ${{ parameters.publishAssetsImmediately }} + enablePublishBuildArtifacts: ${{ parameters.enablePublishBuildArtifacts }} + artifactsPublishingAdditionalParameters: ${{ parameters.artifactsPublishingAdditionalParameters }} + signingValidationAdditionalParameters: ${{ parameters.signingValidationAdditionalParameters }} diff --git a/eng/common/templates/jobs/source-build.yml b/eng/common/templates/jobs/source-build.yml new file mode 100644 index 0000000000..a15b07eb51 --- /dev/null +++ b/eng/common/templates/jobs/source-build.yml @@ -0,0 +1,46 @@ +parameters: + # This template adds arcade-powered source-build to CI. A job is created for each platform, as + # well as an optional server job that completes when all platform jobs complete. + + # The name of the "join" job for all source-build platforms. If set to empty string, the job is + # not included. Existing repo pipelines can use this job depend on all source-build jobs + # completing without maintaining a separate list of every single job ID: just depend on this one + # server job. By default, not included. Recommended name if used: 'Source_Build_Complete'. + allCompletedJobId: '' + + # See /eng/common/templates/job/source-build.yml + jobNamePrefix: 'Source_Build' + + # This is the default platform provided by Arcade, intended for use by a managed-only repo. + defaultManagedPlatform: + name: 'Managed' + container: 'mcr.microsoft.com/dotnet-buildtools/prereqs:centos-stream8' + + # Defines the platforms on which to run build jobs. One job is created for each platform, and the + # object in this array is sent to the job template as 'platform'. If no platforms are specified, + # one job runs on 'defaultManagedPlatform'. + platforms: [] + +jobs: + +- ${{ if ne(parameters.allCompletedJobId, '') }}: + - job: ${{ parameters.allCompletedJobId }} + displayName: Source-Build Complete + pool: server + dependsOn: + - ${{ each platform in parameters.platforms }}: + - ${{ parameters.jobNamePrefix }}_${{ platform.name }} + - ${{ if eq(length(parameters.platforms), 0) }}: + - ${{ parameters.jobNamePrefix }}_${{ parameters.defaultManagedPlatform.name }} + +- ${{ each platform in parameters.platforms }}: + - template: /eng/common/templates/job/source-build.yml + parameters: + jobNamePrefix: ${{ parameters.jobNamePrefix }} + platform: ${{ platform }} + +- ${{ if eq(length(parameters.platforms), 0) }}: + - template: /eng/common/templates/job/source-build.yml + parameters: + jobNamePrefix: ${{ parameters.jobNamePrefix }} + platform: ${{ parameters.defaultManagedPlatform }} diff --git a/eng/common/templates/post-build/common-variables.yml b/eng/common/templates/post-build/common-variables.yml new file mode 100644 index 0000000000..c24193acfc --- /dev/null +++ b/eng/common/templates/post-build/common-variables.yml @@ -0,0 +1,22 @@ +variables: + - group: Publish-Build-Assets + + # Whether the build is internal or not + - name: IsInternalBuild + value: ${{ and(ne(variables['System.TeamProject'], 'public'), contains(variables['Build.SourceBranch'], 'internal')) }} + + # Default Maestro++ API Endpoint and API Version + - name: MaestroApiEndPoint + value: "https://maestro-prod.westus2.cloudapp.azure.com" + - name: MaestroApiAccessToken + value: $(MaestroAccessToken) + - name: MaestroApiVersion + value: "2020-02-20" + + - name: SourceLinkCLIVersion + value: 3.0.0 + - name: SymbolToolVersion + value: 1.0.1 + + - name: runCodesignValidationInjection + value: false diff --git a/eng/common/templates/post-build/post-build.yml b/eng/common/templates/post-build/post-build.yml new file mode 100644 index 0000000000..ef720f9d78 --- /dev/null +++ b/eng/common/templates/post-build/post-build.yml @@ -0,0 +1,281 @@ +parameters: + # Which publishing infra should be used. THIS SHOULD MATCH THE VERSION ON THE BUILD MANIFEST. + # Publishing V1 is no longer supported + # Publishing V2 is no longer supported + # Publishing V3 is the default + - name: publishingInfraVersion + displayName: Which version of publishing should be used to promote the build definition? + type: number + default: 3 + values: + - 3 + + - name: BARBuildId + displayName: BAR Build Id + type: number + default: 0 + + - name: PromoteToChannelIds + displayName: Channel to promote BARBuildId to + type: string + default: '' + + - name: enableSourceLinkValidation + displayName: Enable SourceLink validation + type: boolean + default: false + + - name: enableSigningValidation + displayName: Enable signing validation + type: boolean + default: true + + - name: enableSymbolValidation + displayName: Enable symbol validation + type: boolean + default: false + + - name: enableNugetValidation + displayName: Enable NuGet validation + type: boolean + default: true + + - name: publishInstallersAndChecksums + displayName: Publish installers and checksums + type: boolean + default: true + + - name: SDLValidationParameters + type: object + default: + enable: false + publishGdn: false + continueOnError: false + params: '' + artifactNames: '' + downloadArtifacts: true + + # These parameters let the user customize the call to sdk-task.ps1 for publishing + # symbols & general artifacts as well as for signing validation + - name: symbolPublishingAdditionalParameters + displayName: Symbol publishing additional parameters + type: string + default: '' + + - name: artifactsPublishingAdditionalParameters + displayName: Artifact publishing additional parameters + type: string + default: '' + + - name: signingValidationAdditionalParameters + displayName: Signing validation additional parameters + type: string + default: '' + + # Which stages should finish execution before post-build stages start + - name: validateDependsOn + type: object + default: + - build + + - name: publishDependsOn + type: object + default: + - Validate + + # Optional: Call asset publishing rather than running in a separate stage + - name: publishAssetsImmediately + type: boolean + default: false + +stages: +- ${{ if or(eq( parameters.enableNugetValidation, 'true'), eq(parameters.enableSigningValidation, 'true'), eq(parameters.enableSourceLinkValidation, 'true'), eq(parameters.SDLValidationParameters.enable, 'true')) }}: + - stage: Validate + dependsOn: ${{ parameters.validateDependsOn }} + displayName: Validate Build Assets + variables: + - template: common-variables.yml + - template: /eng/common/templates/variables/pool-providers.yml + jobs: + - job: + displayName: NuGet Validation + condition: and(succeededOrFailed(), eq( ${{ parameters.enableNugetValidation }}, 'true')) + pool: + # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + name: VSEngSS-MicroBuild2022-1ES + demands: Cmd + # If it's not devdiv, it's dnceng + ${{ else }}: + name: $(DncEngInternalBuildPool) + demands: ImageOverride -equals windows.vs2019.amd64 + + steps: + - template: setup-maestro-vars.yml + parameters: + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} + + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: specific + buildVersionToDownload: specific + project: $(AzDOProjectName) + pipeline: $(AzDOPipelineId) + buildId: $(AzDOBuildId) + artifactName: PackageArtifacts + checkDownloadedFiles: true + + - task: PowerShell@2 + displayName: Validate + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/nuget-validation.ps1 + arguments: -PackagesPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ + -ToolDestinationPath $(Agent.BuildDirectory)/Extract/ + + - job: + displayName: Signing Validation + condition: and( eq( ${{ parameters.enableSigningValidation }}, 'true'), ne( variables['PostBuildSign'], 'true')) + pool: + # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + name: VSEngSS-MicroBuild2022-1ES + demands: Cmd + # If it's not devdiv, it's dnceng + ${{ else }}: + name: $(DncEngInternalBuildPool) + demands: ImageOverride -equals windows.vs2019.amd64 + steps: + - template: setup-maestro-vars.yml + parameters: + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} + + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: specific + buildVersionToDownload: specific + project: $(AzDOProjectName) + pipeline: $(AzDOPipelineId) + buildId: $(AzDOBuildId) + artifactName: PackageArtifacts + checkDownloadedFiles: true + itemPattern: | + ** + !**/Microsoft.SourceBuild.Intermediate.*.nupkg + + # This is necessary whenever we want to publish/restore to an AzDO private feed + # Since sdk-task.ps1 tries to restore packages we need to do this authentication here + # otherwise it'll complain about accessing a private feed. + - task: NuGetAuthenticate@0 + displayName: 'Authenticate to AzDO Feeds' + + # Signing validation will optionally work with the buildmanifest file which is downloaded from + # Azure DevOps above. + - task: PowerShell@2 + displayName: Validate + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task SigningValidation -restore -msbuildEngine vs + /p:PackageBasePath='$(Build.ArtifactStagingDirectory)/PackageArtifacts' + /p:SignCheckExclusionsFile='$(Build.SourcesDirectory)/eng/SignCheckExclusionsFile.txt' + ${{ parameters.signingValidationAdditionalParameters }} + + - template: ../steps/publish-logs.yml + parameters: + StageLabel: 'Validation' + JobLabel: 'Signing' + + - job: + displayName: SourceLink Validation + condition: eq( ${{ parameters.enableSourceLinkValidation }}, 'true') + pool: + # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + name: VSEngSS-MicroBuild2022-1ES + demands: Cmd + # If it's not devdiv, it's dnceng + ${{ else }}: + name: $(DncEngInternalBuildPool) + demands: ImageOverride -equals windows.vs2019.amd64 + steps: + - template: setup-maestro-vars.yml + parameters: + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} + + - task: DownloadBuildArtifacts@0 + displayName: Download Blob Artifacts + inputs: + buildType: specific + buildVersionToDownload: specific + project: $(AzDOProjectName) + pipeline: $(AzDOPipelineId) + buildId: $(AzDOBuildId) + artifactName: BlobArtifacts + checkDownloadedFiles: true + + - task: PowerShell@2 + displayName: Validate + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/sourcelink-validation.ps1 + arguments: -InputPath $(Build.ArtifactStagingDirectory)/BlobArtifacts/ + -ExtractPath $(Agent.BuildDirectory)/Extract/ + -GHRepoName $(Build.Repository.Name) + -GHCommit $(Build.SourceVersion) + -SourcelinkCliVersion $(SourceLinkCLIVersion) + continueOnError: true + + - template: /eng/common/templates/job/execute-sdl.yml + parameters: + enable: ${{ parameters.SDLValidationParameters.enable }} + publishGuardianDirectoryToPipeline: ${{ parameters.SDLValidationParameters.publishGdn }} + additionalParameters: ${{ parameters.SDLValidationParameters.params }} + continueOnError: ${{ parameters.SDLValidationParameters.continueOnError }} + artifactNames: ${{ parameters.SDLValidationParameters.artifactNames }} + downloadArtifacts: ${{ parameters.SDLValidationParameters.downloadArtifacts }} + +- ${{ if ne(parameters.publishAssetsImmediately, 'true') }}: + - stage: publish_using_darc + ${{ if or(eq(parameters.enableNugetValidation, 'true'), eq(parameters.enableSigningValidation, 'true'), eq(parameters.enableSourceLinkValidation, 'true'), eq(parameters.SDLValidationParameters.enable, 'true')) }}: + dependsOn: ${{ parameters.publishDependsOn }} + ${{ else }}: + dependsOn: ${{ parameters.validateDependsOn }} + displayName: Publish using Darc + variables: + - template: common-variables.yml + - template: /eng/common/templates/variables/pool-providers.yml + jobs: + - job: + displayName: Publish Using Darc + timeoutInMinutes: 120 + pool: + # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + name: VSEngSS-MicroBuild2022-1ES + demands: Cmd + # If it's not devdiv, it's dnceng + ${{ else }}: + name: $(DncEngInternalBuildPool) + demands: ImageOverride -equals windows.vs2019.amd64 + steps: + - template: setup-maestro-vars.yml + parameters: + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} + + - task: NuGetAuthenticate@0 + + - task: PowerShell@2 + displayName: Publish Using Darc + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/publish-using-darc.ps1 + arguments: -BuildId $(BARBuildId) + -PublishingInfraVersion ${{ parameters.publishingInfraVersion }} + -AzdoToken '$(publishing-dnceng-devdiv-code-r-build-re)' + -MaestroToken '$(MaestroApiAccessToken)' + -WaitPublishingFinish true + -ArtifactsPublishingAdditionalParameters '${{ parameters.artifactsPublishingAdditionalParameters }}' + -SymbolPublishingAdditionalParameters '${{ parameters.symbolPublishingAdditionalParameters }}' diff --git a/eng/common/templates/post-build/setup-maestro-vars.yml b/eng/common/templates/post-build/setup-maestro-vars.yml new file mode 100644 index 0000000000..0c87f149a4 --- /dev/null +++ b/eng/common/templates/post-build/setup-maestro-vars.yml @@ -0,0 +1,70 @@ +parameters: + BARBuildId: '' + PromoteToChannelIds: '' + +steps: + - ${{ if eq(coalesce(parameters.PromoteToChannelIds, 0), 0) }}: + - task: DownloadBuildArtifacts@0 + displayName: Download Release Configs + inputs: + buildType: current + artifactName: ReleaseConfigs + checkDownloadedFiles: true + + - task: PowerShell@2 + name: setReleaseVars + displayName: Set Release Configs Vars + inputs: + targetType: inline + pwsh: true + script: | + try { + if (!$Env:PromoteToMaestroChannels -or $Env:PromoteToMaestroChannels.Trim() -eq '') { + $Content = Get-Content $(Build.StagingDirectory)/ReleaseConfigs/ReleaseConfigs.txt + + $BarId = $Content | Select -Index 0 + $Channels = $Content | Select -Index 1 + $IsStableBuild = $Content | Select -Index 2 + + $AzureDevOpsProject = $Env:System_TeamProject + $AzureDevOpsBuildDefinitionId = $Env:System_DefinitionId + $AzureDevOpsBuildId = $Env:Build_BuildId + } + else { + $buildApiEndpoint = "${Env:MaestroApiEndPoint}/api/builds/${Env:BARBuildId}?api-version=${Env:MaestroApiVersion}" + + $apiHeaders = New-Object 'System.Collections.Generic.Dictionary[[String],[String]]' + $apiHeaders.Add('Accept', 'application/json') + $apiHeaders.Add('Authorization',"Bearer ${Env:MAESTRO_API_TOKEN}") + + $buildInfo = try { Invoke-WebRequest -Method Get -Uri $buildApiEndpoint -Headers $apiHeaders | ConvertFrom-Json } catch { Write-Host "Error: $_" } + + $BarId = $Env:BARBuildId + $Channels = $Env:PromoteToMaestroChannels -split "," + $Channels = $Channels -join "][" + $Channels = "[$Channels]" + + $IsStableBuild = $buildInfo.stable + $AzureDevOpsProject = $buildInfo.azureDevOpsProject + $AzureDevOpsBuildDefinitionId = $buildInfo.azureDevOpsBuildDefinitionId + $AzureDevOpsBuildId = $buildInfo.azureDevOpsBuildId + } + + Write-Host "##vso[task.setvariable variable=BARBuildId]$BarId" + Write-Host "##vso[task.setvariable variable=TargetChannels]$Channels" + Write-Host "##vso[task.setvariable variable=IsStableBuild]$IsStableBuild" + + Write-Host "##vso[task.setvariable variable=AzDOProjectName]$AzureDevOpsProject" + Write-Host "##vso[task.setvariable variable=AzDOPipelineId]$AzureDevOpsBuildDefinitionId" + Write-Host "##vso[task.setvariable variable=AzDOBuildId]$AzureDevOpsBuildId" + } + catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + exit 1 + } + env: + MAESTRO_API_TOKEN: $(MaestroApiAccessToken) + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToMaestroChannels: ${{ parameters.PromoteToChannelIds }} diff --git a/eng/common/templates/post-build/trigger-subscription.yml b/eng/common/templates/post-build/trigger-subscription.yml new file mode 100644 index 0000000000..da669030da --- /dev/null +++ b/eng/common/templates/post-build/trigger-subscription.yml @@ -0,0 +1,13 @@ +parameters: + ChannelId: 0 + +steps: +- task: PowerShell@2 + displayName: Triggering subscriptions + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/trigger-subscriptions.ps1 + arguments: -SourceRepo $(Build.Repository.Uri) + -ChannelId ${{ parameters.ChannelId }} + -MaestroApiAccessToken $(MaestroAccessToken) + -MaestroApiEndPoint $(MaestroApiEndPoint) + -MaestroApiVersion $(MaestroApiVersion) diff --git a/eng/common/templates/steps/add-build-to-channel.yml b/eng/common/templates/steps/add-build-to-channel.yml new file mode 100644 index 0000000000..f67a210d62 --- /dev/null +++ b/eng/common/templates/steps/add-build-to-channel.yml @@ -0,0 +1,13 @@ +parameters: + ChannelId: 0 + +steps: +- task: PowerShell@2 + displayName: Add Build to Channel + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/add-build-to-channel.ps1 + arguments: -BuildId $(BARBuildId) + -ChannelId ${{ parameters.ChannelId }} + -MaestroApiAccessToken $(MaestroApiAccessToken) + -MaestroApiEndPoint $(MaestroApiEndPoint) + -MaestroApiVersion $(MaestroApiVersion) diff --git a/eng/common/templates/steps/build-reason.yml b/eng/common/templates/steps/build-reason.yml new file mode 100644 index 0000000000..eba58109b5 --- /dev/null +++ b/eng/common/templates/steps/build-reason.yml @@ -0,0 +1,12 @@ +# build-reason.yml +# Description: runs steps if build.reason condition is valid. conditions is a string of valid build reasons +# to include steps (',' separated). +parameters: + conditions: '' + steps: [] + +steps: + - ${{ if and( not(startsWith(parameters.conditions, 'not')), contains(parameters.conditions, variables['build.reason'])) }}: + - ${{ parameters.steps }} + - ${{ if and( startsWith(parameters.conditions, 'not'), not(contains(parameters.conditions, variables['build.reason']))) }}: + - ${{ parameters.steps }} diff --git a/eng/common/templates/steps/component-governance.yml b/eng/common/templates/steps/component-governance.yml new file mode 100644 index 0000000000..0ecec47b0c --- /dev/null +++ b/eng/common/templates/steps/component-governance.yml @@ -0,0 +1,13 @@ +parameters: + disableComponentGovernance: false + componentGovernanceIgnoreDirectories: '' + +steps: +- ${{ if eq(parameters.disableComponentGovernance, 'true') }}: + - script: "echo ##vso[task.setvariable variable=skipComponentGovernanceDetection]true" + displayName: Set skipComponentGovernanceDetection variable +- ${{ if ne(parameters.disableComponentGovernance, 'true') }}: + - task: ComponentGovernanceComponentDetection@0 + continueOnError: true + inputs: + ignoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }} \ No newline at end of file diff --git a/eng/common/templates/steps/execute-codeql.yml b/eng/common/templates/steps/execute-codeql.yml new file mode 100644 index 0000000000..3930b16302 --- /dev/null +++ b/eng/common/templates/steps/execute-codeql.yml @@ -0,0 +1,32 @@ +parameters: + # Language that should be analyzed. Defaults to csharp + language: csharp + # Build Commands + buildCommands: '' + overrideParameters: '' # Optional: to override values for parameters. + additionalParameters: '' # Optional: parameters that need user specific values eg: '-SourceToolsList @("abc","def") -ArtifactToolsList @("ghi","jkl")' + # Optional: if specified, restore and use this version of Guardian instead of the default. + overrideGuardianVersion: '' + # Optional: if true, publish the '.gdn' folder as a pipeline artifact. This can help with in-depth + # diagnosis of problems with specific tool configurations. + publishGuardianDirectoryToPipeline: false + # The script to run to execute all SDL tools. Use this if you want to use a script to define SDL + # parameters rather than relying on YAML. It may be better to use a local script, because you can + # reproduce results locally without piecing together a command based on the YAML. + executeAllSdlToolsScript: 'eng/common/sdl/execute-all-sdl-tools.ps1' + # There is some sort of bug (has been reported) in Azure DevOps where if this parameter is named + # 'continueOnError', the parameter value is not correctly picked up. + # This can also be remedied by the caller (post-build.yml) if it does not use a nested parameter + # optional: determines whether to continue the build if the step errors; + sdlContinueOnError: false + +steps: +- template: /eng/common/templates/steps/execute-sdl.yml + parameters: + overrideGuardianVersion: ${{ parameters.overrideGuardianVersion }} + executeAllSdlToolsScript: ${{ parameters.executeAllSdlToolsScript }} + overrideParameters: ${{ parameters.overrideParameters }} + additionalParameters: '${{ parameters.additionalParameters }} + -CodeQLAdditionalRunConfigParams @("BuildCommands < ${{ parameters.buildCommands }}", "Language < ${{ parameters.language }}")' + publishGuardianDirectoryToPipeline: ${{ parameters.publishGuardianDirectoryToPipeline }} + sdlContinueOnError: ${{ parameters.sdlContinueOnError }} \ No newline at end of file diff --git a/eng/common/templates/steps/execute-sdl.yml b/eng/common/templates/steps/execute-sdl.yml new file mode 100644 index 0000000000..9dd5709f66 --- /dev/null +++ b/eng/common/templates/steps/execute-sdl.yml @@ -0,0 +1,88 @@ +parameters: + overrideGuardianVersion: '' + executeAllSdlToolsScript: '' + overrideParameters: '' + additionalParameters: '' + publishGuardianDirectoryToPipeline: false + sdlContinueOnError: false + condition: '' + +steps: +- task: NuGetAuthenticate@1 + inputs: + nuGetServiceConnections: GuardianConnect + +- task: NuGetToolInstaller@1 + displayName: 'Install NuGet.exe' + +- ${{ if ne(parameters.overrideGuardianVersion, '') }}: + - pwsh: | + Set-Location -Path $(Build.SourcesDirectory)\eng\common\sdl + . .\sdl.ps1 + $guardianCliLocation = Install-Gdn -Path $(Build.SourcesDirectory)\.artifacts -Version ${{ parameters.overrideGuardianVersion }} + Write-Host "##vso[task.setvariable variable=GuardianCliLocation]$guardianCliLocation" + displayName: Install Guardian (Overridden) + +- ${{ if eq(parameters.overrideGuardianVersion, '') }}: + - pwsh: | + Set-Location -Path $(Build.SourcesDirectory)\eng\common\sdl + . .\sdl.ps1 + $guardianCliLocation = Install-Gdn -Path $(Build.SourcesDirectory)\.artifacts + Write-Host "##vso[task.setvariable variable=GuardianCliLocation]$guardianCliLocation" + displayName: Install Guardian + +- ${{ if ne(parameters.overrideParameters, '') }}: + - powershell: ${{ parameters.executeAllSdlToolsScript }} ${{ parameters.overrideParameters }} + displayName: Execute SDL + continueOnError: ${{ parameters.sdlContinueOnError }} + condition: ${{ parameters.condition }} + +- ${{ if eq(parameters.overrideParameters, '') }}: + - powershell: ${{ parameters.executeAllSdlToolsScript }} + -GuardianCliLocation $(GuardianCliLocation) + -NugetPackageDirectory $(Build.SourcesDirectory)\.packages + -AzureDevOpsAccessToken $(dn-bot-dotnet-build-rw-code-rw) + ${{ parameters.additionalParameters }} + displayName: Execute SDL + continueOnError: ${{ parameters.sdlContinueOnError }} + condition: ${{ parameters.condition }} + +- ${{ if ne(parameters.publishGuardianDirectoryToPipeline, 'false') }}: + # We want to publish the Guardian results and configuration for easy diagnosis. However, the + # '.gdn' dir is a mix of configuration, results, extracted dependencies, and Guardian default + # tooling files. Some of these files are large and aren't useful during an investigation, so + # exclude them by simply deleting them before publishing. (As of writing, there is no documented + # way to selectively exclude a dir from the pipeline artifact publish task.) + - task: DeleteFiles@1 + displayName: Delete Guardian dependencies to avoid uploading + inputs: + SourceFolder: $(Agent.BuildDirectory)/.gdn + Contents: | + c + i + condition: succeededOrFailed() + + - publish: $(Agent.BuildDirectory)/.gdn + artifact: GuardianConfiguration + displayName: Publish GuardianConfiguration + condition: succeededOrFailed() + + # Publish the SARIF files in a container named CodeAnalysisLogs to enable integration + # with the "SARIF SAST Scans Tab" Azure DevOps extension + - task: CopyFiles@2 + displayName: Copy SARIF files + inputs: + flattenFolders: true + sourceFolder: $(Agent.BuildDirectory)/.gdn/rc/ + contents: '**/*.sarif' + targetFolder: $(Build.SourcesDirectory)/CodeAnalysisLogs + condition: succeededOrFailed() + + # Use PublishBuildArtifacts because the SARIF extension only checks this case + # see microsoft/sarif-azuredevops-extension#4 + - task: PublishBuildArtifacts@1 + displayName: Publish SARIF files to CodeAnalysisLogs container + inputs: + pathToPublish: $(Build.SourcesDirectory)/CodeAnalysisLogs + artifactName: CodeAnalysisLogs + condition: succeededOrFailed() \ No newline at end of file diff --git a/eng/common/templates/steps/generate-sbom.yml b/eng/common/templates/steps/generate-sbom.yml new file mode 100644 index 0000000000..a06373f38f --- /dev/null +++ b/eng/common/templates/steps/generate-sbom.yml @@ -0,0 +1,48 @@ +# BuildDropPath - The root folder of the drop directory for which the manifest file will be generated. +# PackageName - The name of the package this SBOM represents. +# PackageVersion - The version of the package this SBOM represents. +# ManifestDirPath - The path of the directory where the generated manifest files will be placed +# IgnoreDirectories - Directories to ignore for SBOM generation. This will be passed through to the CG component detector. + +parameters: + PackageVersion: 7.0.0 + BuildDropPath: '$(Build.SourcesDirectory)/artifacts' + PackageName: '.NET' + ManifestDirPath: $(Build.ArtifactStagingDirectory)/sbom + IgnoreDirectories: '' + sbomContinueOnError: true + +steps: +- task: PowerShell@2 + displayName: Prep for SBOM generation in (Non-linux) + condition: or(eq(variables['Agent.Os'], 'Windows_NT'), eq(variables['Agent.Os'], 'Darwin')) + inputs: + filePath: ./eng/common/generate-sbom-prep.ps1 + arguments: ${{parameters.manifestDirPath}} + +# Chmodding is a workaround for https://github.com/dotnet/arcade/issues/8461 +- script: | + chmod +x ./eng/common/generate-sbom-prep.sh + ./eng/common/generate-sbom-prep.sh ${{parameters.manifestDirPath}} + displayName: Prep for SBOM generation in (Linux) + condition: eq(variables['Agent.Os'], 'Linux') + continueOnError: ${{ parameters.sbomContinueOnError }} + +- task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 + displayName: 'Generate SBOM manifest' + continueOnError: ${{ parameters.sbomContinueOnError }} + inputs: + PackageName: ${{ parameters.packageName }} + BuildDropPath: ${{ parameters.buildDropPath }} + PackageVersion: ${{ parameters.packageVersion }} + ManifestDirPath: ${{ parameters.manifestDirPath }} + ${{ if ne(parameters.IgnoreDirectories, '') }}: + AdditionalComponentDetectorArgs: '--IgnoreDirectories ${{ parameters.IgnoreDirectories }}' + +- task: PublishPipelineArtifact@1 + displayName: Publish SBOM manifest + continueOnError: ${{parameters.sbomContinueOnError}} + inputs: + targetPath: '${{parameters.manifestDirPath}}' + artifactName: $(ARTIFACT_NAME) + diff --git a/eng/common/templates/steps/publish-logs.yml b/eng/common/templates/steps/publish-logs.yml new file mode 100644 index 0000000000..88f238f36b --- /dev/null +++ b/eng/common/templates/steps/publish-logs.yml @@ -0,0 +1,23 @@ +parameters: + StageLabel: '' + JobLabel: '' + +steps: +- task: Powershell@2 + displayName: Prepare Binlogs to Upload + inputs: + targetType: inline + script: | + New-Item -ItemType Directory $(Build.SourcesDirectory)/PostBuildLogs/${{parameters.StageLabel}}/${{parameters.JobLabel}}/ + Move-Item -Path $(Build.SourcesDirectory)/artifacts/log/Debug/* $(Build.SourcesDirectory)/PostBuildLogs/${{parameters.StageLabel}}/${{parameters.JobLabel}}/ + continueOnError: true + condition: always() + +- task: PublishBuildArtifacts@1 + displayName: Publish Logs + inputs: + PathtoPublish: '$(Build.SourcesDirectory)/PostBuildLogs' + PublishLocation: Container + ArtifactName: PostBuildLogs + continueOnError: true + condition: always() diff --git a/eng/common/templates/steps/retain-build.yml b/eng/common/templates/steps/retain-build.yml new file mode 100644 index 0000000000..83d97a26a0 --- /dev/null +++ b/eng/common/templates/steps/retain-build.yml @@ -0,0 +1,28 @@ +parameters: + # Optional azure devops PAT with build execute permissions for the build's organization, + # only needed if the build that should be retained ran on a different organization than + # the pipeline where this template is executing from + Token: '' + # Optional BuildId to retain, defaults to the current running build + BuildId: '' + # Azure devops Organization URI for the build in the https://dev.azure.com/ format. + # Defaults to the organization the current pipeline is running on + AzdoOrgUri: '$(System.CollectionUri)' + # Azure devops project for the build. Defaults to the project the current pipeline is running on + AzdoProject: '$(System.TeamProject)' + +steps: + - task: powershell@2 + inputs: + targetType: 'filePath' + filePath: eng/common/retain-build.ps1 + pwsh: true + arguments: > + -AzdoOrgUri: ${{parameters.AzdoOrgUri}} + -AzdoProject ${{parameters.AzdoProject}} + -Token ${{coalesce(parameters.Token, '$env:SYSTEM_ACCESSTOKEN') }} + -BuildId ${{coalesce(parameters.BuildId, '$env:BUILD_ID')}} + displayName: Enable permanent build retention + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + BUILD_ID: $(Build.BuildId) \ No newline at end of file diff --git a/eng/common/templates/steps/run-on-unix.yml b/eng/common/templates/steps/run-on-unix.yml new file mode 100644 index 0000000000..e1733814f6 --- /dev/null +++ b/eng/common/templates/steps/run-on-unix.yml @@ -0,0 +1,7 @@ +parameters: + agentOs: '' + steps: [] + +steps: +- ${{ if ne(parameters.agentOs, 'Windows_NT') }}: + - ${{ parameters.steps }} diff --git a/eng/common/templates/steps/run-on-windows.yml b/eng/common/templates/steps/run-on-windows.yml new file mode 100644 index 0000000000..73e7e9c275 --- /dev/null +++ b/eng/common/templates/steps/run-on-windows.yml @@ -0,0 +1,7 @@ +parameters: + agentOs: '' + steps: [] + +steps: +- ${{ if eq(parameters.agentOs, 'Windows_NT') }}: + - ${{ parameters.steps }} diff --git a/eng/common/templates/steps/run-script-ifequalelse.yml b/eng/common/templates/steps/run-script-ifequalelse.yml new file mode 100644 index 0000000000..3d1242f558 --- /dev/null +++ b/eng/common/templates/steps/run-script-ifequalelse.yml @@ -0,0 +1,33 @@ +parameters: + # if parameter1 equals parameter 2, run 'ifScript' command, else run 'elsescript' command + parameter1: '' + parameter2: '' + ifScript: '' + elseScript: '' + + # name of script step + name: Script + + # display name of script step + displayName: If-Equal-Else Script + + # environment + env: {} + + # conditional expression for step execution + condition: '' + +steps: +- ${{ if and(ne(parameters.ifScript, ''), eq(parameters.parameter1, parameters.parameter2)) }}: + - script: ${{ parameters.ifScript }} + name: ${{ parameters.name }} + displayName: ${{ parameters.displayName }} + env: ${{ parameters.env }} + condition: ${{ parameters.condition }} + +- ${{ if and(ne(parameters.elseScript, ''), ne(parameters.parameter1, parameters.parameter2)) }}: + - script: ${{ parameters.elseScript }} + name: ${{ parameters.name }} + displayName: ${{ parameters.displayName }} + env: ${{ parameters.env }} + condition: ${{ parameters.condition }} \ No newline at end of file diff --git a/eng/common/templates/steps/send-to-helix.yml b/eng/common/templates/steps/send-to-helix.yml new file mode 100644 index 0000000000..3eb7e2d5f8 --- /dev/null +++ b/eng/common/templates/steps/send-to-helix.yml @@ -0,0 +1,91 @@ +# Please remember to update the documentation if you make changes to these parameters! +parameters: + HelixSource: 'pr/default' # required -- sources must start with pr/, official/, prodcon/, or agent/ + HelixType: 'tests/default/' # required -- Helix telemetry which identifies what type of data this is; should include "test" for clarity and must end in '/' + HelixBuild: $(Build.BuildNumber) # required -- the build number Helix will use to identify this -- automatically set to the AzDO build number + HelixTargetQueues: '' # required -- semicolon-delimited list of Helix queues to test on; see https://helix.dot.net/ for a list of queues + HelixAccessToken: '' # required -- access token to make Helix API requests; should be provided by the appropriate variable group + HelixConfiguration: '' # optional -- additional property attached to a job + HelixPreCommands: '' # optional -- commands to run before Helix work item execution + HelixPostCommands: '' # optional -- commands to run after Helix work item execution + WorkItemDirectory: '' # optional -- a payload directory to zip up and send to Helix; requires WorkItemCommand; incompatible with XUnitProjects + WorkItemCommand: '' # optional -- a command to execute on the payload; requires WorkItemDirectory; incompatible with XUnitProjects + WorkItemTimeout: '' # optional -- a timeout in TimeSpan.Parse-ready value (e.g. 00:02:00) for the work item command; requires WorkItemDirectory; incompatible with XUnitProjects + CorrelationPayloadDirectory: '' # optional -- a directory to zip up and send to Helix as a correlation payload + XUnitProjects: '' # optional -- semicolon-delimited list of XUnitProjects to parse and send to Helix; requires XUnitRuntimeTargetFramework, XUnitPublishTargetFramework, XUnitRunnerVersion, and IncludeDotNetCli=true + XUnitWorkItemTimeout: '' # optional -- the workitem timeout in seconds for all workitems created from the xUnit projects specified by XUnitProjects + XUnitPublishTargetFramework: '' # optional -- framework to use to publish your xUnit projects + XUnitRuntimeTargetFramework: '' # optional -- framework to use for the xUnit console runner + XUnitRunnerVersion: '' # optional -- version of the xUnit nuget package you wish to use on Helix; required for XUnitProjects + IncludeDotNetCli: false # optional -- true will download a version of the .NET CLI onto the Helix machine as a correlation payload; requires DotNetCliPackageType and DotNetCliVersion + DotNetCliPackageType: '' # optional -- either 'sdk', 'runtime' or 'aspnetcore-runtime'; determines whether the sdk or runtime will be sent to Helix; see https://raw.githubusercontent.com/dotnet/core/main/release-notes/releases-index.json + DotNetCliVersion: '' # optional -- version of the CLI to send to Helix; based on this: https://raw.githubusercontent.com/dotnet/core/main/release-notes/releases-index.json + WaitForWorkItemCompletion: true # optional -- true will make the task wait until work items have been completed and fail the build if work items fail. False is "fire and forget." + IsExternal: false # [DEPRECATED] -- doesn't do anything, jobs are external if HelixAccessToken is empty and Creator is set + HelixBaseUri: 'https://helix.dot.net/' # optional -- sets the Helix API base URI (allows targeting https://helix.int-dot.net ) + Creator: '' # optional -- if the build is external, use this to specify who is sending the job + DisplayNamePrefix: 'Run Tests' # optional -- rename the beginning of the displayName of the steps in AzDO + condition: succeeded() # optional -- condition for step to execute; defaults to succeeded() + continueOnError: false # optional -- determines whether to continue the build if the step errors; defaults to false + +steps: + - powershell: 'powershell "$env:BUILD_SOURCESDIRECTORY\eng\common\msbuild.ps1 $env:BUILD_SOURCESDIRECTORY\eng\common\helixpublish.proj /restore /p:TreatWarningsAsErrors=false /t:Test /bl:$env:BUILD_SOURCESDIRECTORY\artifacts\log\$env:BuildConfig\SendToHelix.binlog"' + displayName: ${{ parameters.DisplayNamePrefix }} (Windows) + env: + BuildConfig: $(_BuildConfig) + HelixSource: ${{ parameters.HelixSource }} + HelixType: ${{ parameters.HelixType }} + HelixBuild: ${{ parameters.HelixBuild }} + HelixConfiguration: ${{ parameters.HelixConfiguration }} + HelixTargetQueues: ${{ parameters.HelixTargetQueues }} + HelixAccessToken: ${{ parameters.HelixAccessToken }} + HelixPreCommands: ${{ parameters.HelixPreCommands }} + HelixPostCommands: ${{ parameters.HelixPostCommands }} + WorkItemDirectory: ${{ parameters.WorkItemDirectory }} + WorkItemCommand: ${{ parameters.WorkItemCommand }} + WorkItemTimeout: ${{ parameters.WorkItemTimeout }} + CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }} + XUnitProjects: ${{ parameters.XUnitProjects }} + XUnitWorkItemTimeout: ${{ parameters.XUnitWorkItemTimeout }} + XUnitPublishTargetFramework: ${{ parameters.XUnitPublishTargetFramework }} + XUnitRuntimeTargetFramework: ${{ parameters.XUnitRuntimeTargetFramework }} + XUnitRunnerVersion: ${{ parameters.XUnitRunnerVersion }} + IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }} + DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }} + DotNetCliVersion: ${{ parameters.DotNetCliVersion }} + WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }} + HelixBaseUri: ${{ parameters.HelixBaseUri }} + Creator: ${{ parameters.Creator }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + condition: and(${{ parameters.condition }}, eq(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} + - script: $BUILD_SOURCESDIRECTORY/eng/common/msbuild.sh $BUILD_SOURCESDIRECTORY/eng/common/helixpublish.proj /restore /p:TreatWarningsAsErrors=false /t:Test /bl:$BUILD_SOURCESDIRECTORY/artifacts/log/$BuildConfig/SendToHelix.binlog + displayName: ${{ parameters.DisplayNamePrefix }} (Unix) + env: + BuildConfig: $(_BuildConfig) + HelixSource: ${{ parameters.HelixSource }} + HelixType: ${{ parameters.HelixType }} + HelixBuild: ${{ parameters.HelixBuild }} + HelixConfiguration: ${{ parameters.HelixConfiguration }} + HelixTargetQueues: ${{ parameters.HelixTargetQueues }} + HelixAccessToken: ${{ parameters.HelixAccessToken }} + HelixPreCommands: ${{ parameters.HelixPreCommands }} + HelixPostCommands: ${{ parameters.HelixPostCommands }} + WorkItemDirectory: ${{ parameters.WorkItemDirectory }} + WorkItemCommand: ${{ parameters.WorkItemCommand }} + WorkItemTimeout: ${{ parameters.WorkItemTimeout }} + CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }} + XUnitProjects: ${{ parameters.XUnitProjects }} + XUnitWorkItemTimeout: ${{ parameters.XUnitWorkItemTimeout }} + XUnitPublishTargetFramework: ${{ parameters.XUnitPublishTargetFramework }} + XUnitRuntimeTargetFramework: ${{ parameters.XUnitRuntimeTargetFramework }} + XUnitRunnerVersion: ${{ parameters.XUnitRunnerVersion }} + IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }} + DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }} + DotNetCliVersion: ${{ parameters.DotNetCliVersion }} + WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }} + HelixBaseUri: ${{ parameters.HelixBaseUri }} + Creator: ${{ parameters.Creator }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + condition: and(${{ parameters.condition }}, ne(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} diff --git a/eng/common/templates/steps/source-build.yml b/eng/common/templates/steps/source-build.yml new file mode 100644 index 0000000000..a97a185a36 --- /dev/null +++ b/eng/common/templates/steps/source-build.yml @@ -0,0 +1,114 @@ +parameters: + # This template adds arcade-powered source-build to CI. + + # This is a 'steps' template, and is intended for advanced scenarios where the existing build + # infra has a careful build methodology that must be followed. For example, a repo + # (dotnet/runtime) might choose to clone the GitHub repo only once and store it as a pipeline + # artifact for all subsequent jobs to use, to reduce dependence on a strong network connection to + # GitHub. Using this steps template leaves room for that infra to be included. + + # Defines the platform on which to run the steps. See 'eng/common/templates/job/source-build.yml' + # for details. The entire object is described in the 'job' template for simplicity, even though + # the usage of the properties on this object is split between the 'job' and 'steps' templates. + platform: {} + +steps: +# Build. Keep it self-contained for simple reusability. (No source-build-specific job variables.) +- script: | + set -x + df -h + + # If building on the internal project, the artifact feeds variable may be available (usually only if needed) + # In that case, call the feed setup script to add internal feeds corresponding to public ones. + # In addition, add an msbuild argument to copy the WIP from the repo to the target build location. + # This is because SetupNuGetSources.sh will alter the current NuGet.config file, and we need to preserve those + # changes. + internalRestoreArgs= + if [ '$(dn-bot-dnceng-artifact-feeds-rw)' != '$''(dn-bot-dnceng-artifact-feeds-rw)' ]; then + # Temporarily work around https://github.com/dotnet/arcade/issues/7709 + chmod +x $(Build.SourcesDirectory)/eng/common/SetupNugetSources.sh + $(Build.SourcesDirectory)/eng/common/SetupNugetSources.sh $(Build.SourcesDirectory)/NuGet.config $(dn-bot-dnceng-artifact-feeds-rw) + internalRestoreArgs='/p:CopyWipIntoInnerSourceBuildRepo=true' + + # The 'Copy WIP' feature of source build uses git stash to apply changes from the original repo. + # This only works if there is a username/email configured, which won't be the case in most CI runs. + git config --get user.email + if [ $? -ne 0 ]; then + git config user.email dn-bot@microsoft.com + git config user.name dn-bot + fi + fi + + # If building on the internal project, the internal storage variable may be available (usually only if needed) + # In that case, add variables to allow the download of internal runtimes if the specified versions are not found + # in the default public locations. + internalRuntimeDownloadArgs= + if [ '$(dotnetbuilds-internal-container-read-token-base64)' != '$''(dotnetbuilds-internal-container-read-token-base64)' ]; then + internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://dotnetbuilds.blob.core.windows.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://dotnetbuilds.blob.core.windows.net/internal --runtimesourcefeedkey $(dotnetbuilds-internal-container-read-token-base64)' + fi + + buildConfig=Release + # Check if AzDO substitutes in a build config from a variable, and use it if so. + if [ '$(_BuildConfig)' != '$''(_BuildConfig)' ]; then + buildConfig='$(_BuildConfig)' + fi + + officialBuildArgs= + if [ '${{ and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}' = 'True' ]; then + officialBuildArgs='/p:DotNetPublishUsingPipelines=true /p:OfficialBuildId=$(BUILD.BUILDNUMBER)' + fi + + targetRidArgs= + if [ '${{ parameters.platform.targetRID }}' != '' ]; then + targetRidArgs='/p:TargetRid=${{ parameters.platform.targetRID }}' + fi + + runtimeOsArgs= + if [ '${{ parameters.platform.runtimeOS }}' != '' ]; then + runtimeOsArgs='/p:RuntimeOS=${{ parameters.platform.runtimeOS }}' + fi + + publishArgs= + if [ '${{ parameters.platform.skipPublishValidation }}' != 'true' ]; then + publishArgs='--publish' + fi + + assetManifestFileName=SourceBuild_RidSpecific.xml + if [ '${{ parameters.platform.name }}' != '' ]; then + assetManifestFileName=SourceBuild_${{ parameters.platform.name }}.xml + fi + + ${{ coalesce(parameters.platform.buildScript, './build.sh') }} --ci \ + --configuration $buildConfig \ + --restore --build --pack $publishArgs -bl \ + $officialBuildArgs \ + $internalRuntimeDownloadArgs \ + $internalRestoreArgs \ + $targetRidArgs \ + $runtimeOsArgs \ + /p:SourceBuildNonPortable=${{ parameters.platform.nonPortable }} \ + /p:ArcadeBuildFromSource=true \ + /p:AssetManifestFileName=$assetManifestFileName + displayName: Build + +# Upload build logs for diagnosis. +- task: CopyFiles@2 + displayName: Prepare BuildLogs staging directory + inputs: + SourceFolder: '$(Build.SourcesDirectory)' + Contents: | + **/*.log + **/*.binlog + artifacts/source-build/self/prebuilt-report/** + TargetFolder: '$(Build.StagingDirectory)/BuildLogs' + CleanTargetFolder: true + continueOnError: true + condition: succeededOrFailed() + +- task: PublishPipelineArtifact@1 + displayName: Publish BuildLogs + inputs: + targetPath: '$(Build.StagingDirectory)/BuildLogs' + artifactName: BuildLogs_SourceBuild_${{ parameters.platform.name }}_Attempt$(System.JobAttempt) + continueOnError: true + condition: succeededOrFailed() diff --git a/eng/common/templates/steps/telemetry-end.yml b/eng/common/templates/steps/telemetry-end.yml new file mode 100644 index 0000000000..fadc04ca1b --- /dev/null +++ b/eng/common/templates/steps/telemetry-end.yml @@ -0,0 +1,102 @@ +parameters: + maxRetries: 5 + retryDelay: 10 # in seconds + +steps: +- bash: | + if [ "$AGENT_JOBSTATUS" = "Succeeded" ] || [ "$AGENT_JOBSTATUS" = "PartiallySucceeded" ]; then + errorCount=0 + else + errorCount=1 + fi + warningCount=0 + + curlStatus=1 + retryCount=0 + # retry loop to harden against spotty telemetry connections + # we don't retry successes and 4xx client errors + until [[ $curlStatus -eq 0 || ( $curlStatus -ge 400 && $curlStatus -le 499 ) || $retryCount -ge $MaxRetries ]] + do + if [ $retryCount -gt 0 ]; then + echo "Failed to send telemetry to Helix; waiting $RetryDelay seconds before retrying..." + sleep $RetryDelay + fi + + # create a temporary file for curl output + res=`mktemp` + + curlResult=` + curl --verbose --output $res --write-out "%{http_code}"\ + -H 'Content-Type: application/json' \ + -H "X-Helix-Job-Token: $Helix_JobToken" \ + -H 'Content-Length: 0' \ + -X POST -G "https://helix.dot.net/api/2018-03-14/telemetry/job/build/$Helix_WorkItemId/finish" \ + --data-urlencode "errorCount=$errorCount" \ + --data-urlencode "warningCount=$warningCount"` + curlStatus=$? + + if [ $curlStatus -eq 0 ]; then + if [ $curlResult -gt 299 ] || [ $curlResult -lt 200 ]; then + curlStatus=$curlResult + fi + fi + + let retryCount++ + done + + if [ $curlStatus -ne 0 ]; then + echo "Failed to Send Build Finish information after $retryCount retries" + vstsLogOutput="vso[task.logissue type=error;sourcepath=templates/steps/telemetry-end.yml;code=1;]Failed to Send Build Finish information: $curlStatus" + echo "##$vstsLogOutput" + exit 1 + fi + displayName: Send Unix Build End Telemetry + env: + # defined via VSTS variables in start-job.sh + Helix_JobToken: $(Helix_JobToken) + Helix_WorkItemId: $(Helix_WorkItemId) + MaxRetries: ${{ parameters.maxRetries }} + RetryDelay: ${{ parameters.retryDelay }} + condition: and(always(), ne(variables['Agent.Os'], 'Windows_NT')) +- powershell: | + if (($env:Agent_JobStatus -eq 'Succeeded') -or ($env:Agent_JobStatus -eq 'PartiallySucceeded')) { + $ErrorCount = 0 + } else { + $ErrorCount = 1 + } + $WarningCount = 0 + + # Basic retry loop to harden against server flakiness + $retryCount = 0 + while ($retryCount -lt $env:MaxRetries) { + try { + Invoke-RestMethod -Uri "https://helix.dot.net/api/2018-03-14/telemetry/job/build/$env:Helix_WorkItemId/finish?errorCount=$ErrorCount&warningCount=$WarningCount" -Method Post -ContentType "application/json" -Body "" ` + -Headers @{ 'X-Helix-Job-Token'=$env:Helix_JobToken } + break + } + catch { + $statusCode = $_.Exception.Response.StatusCode.value__ + if ($statusCode -ge 400 -and $statusCode -le 499) { + Write-Host "##vso[task.logissue]error Failed to send telemetry to Helix (status code $statusCode); not retrying (4xx client error)" + Write-Host "##vso[task.logissue]error ", $_.Exception.GetType().FullName, $_.Exception.Message + exit 1 + } + Write-Host "Failed to send telemetry to Helix (status code $statusCode); waiting $env:RetryDelay seconds before retrying..." + $retryCount++ + sleep $env:RetryDelay + continue + } + } + + if ($retryCount -ge $env:MaxRetries) { + Write-Host "##vso[task.logissue]error Failed to send telemetry to Helix after $retryCount retries." + exit 1 + } + displayName: Send Windows Build End Telemetry + env: + # defined via VSTS variables in start-job.ps1 + Helix_JobToken: $(Helix_JobToken) + Helix_WorkItemId: $(Helix_WorkItemId) + MaxRetries: ${{ parameters.maxRetries }} + RetryDelay: ${{ parameters.retryDelay }} + condition: and(always(),eq(variables['Agent.Os'], 'Windows_NT')) diff --git a/eng/common/templates/steps/telemetry-start.yml b/eng/common/templates/steps/telemetry-start.yml new file mode 100644 index 0000000000..32c01ef0b5 --- /dev/null +++ b/eng/common/templates/steps/telemetry-start.yml @@ -0,0 +1,241 @@ +parameters: + helixSource: 'undefined_defaulted_in_telemetry.yml' + helixType: 'undefined_defaulted_in_telemetry.yml' + buildConfig: '' + runAsPublic: false + maxRetries: 5 + retryDelay: 10 # in seconds + +steps: +- ${{ if and(eq(parameters.runAsPublic, 'false'), not(eq(variables['System.TeamProject'], 'public'))) }}: + - task: AzureKeyVault@1 + inputs: + azureSubscription: 'HelixProd_KeyVault' + KeyVaultName: HelixProdKV + SecretsFilter: 'HelixApiAccessToken' + condition: always() +- bash: | + # create a temporary file + jobInfo=`mktemp` + + # write job info content to temporary file + cat > $jobInfo < powershell invocations +# as dot sourcing isn't possible. +function InitializeDotNetCli([bool]$install, [bool]$createSdkLocationFile) { + if (Test-Path variable:global:_DotNetInstallDir) { + return $global:_DotNetInstallDir + } + + # Don't resolve runtime, shared framework, or SDK from other locations to ensure build determinism + $env:DOTNET_MULTILEVEL_LOOKUP=0 + + # Disable first run since we do not need all ASP.NET packages restored. + $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 + + # Disable telemetry on CI. + if ($ci) { + $env:DOTNET_CLI_TELEMETRY_OPTOUT=1 + } + + # Source Build uses DotNetCoreSdkDir variable + if ($env:DotNetCoreSdkDir -ne $null) { + $env:DOTNET_INSTALL_DIR = $env:DotNetCoreSdkDir + } + + # Find the first path on %PATH% that contains the dotnet.exe + if ($useInstalledDotNetCli -and (-not $globalJsonHasRuntimes) -and ($env:DOTNET_INSTALL_DIR -eq $null)) { + $dotnetExecutable = GetExecutableFileName 'dotnet' + $dotnetCmd = Get-Command $dotnetExecutable -ErrorAction SilentlyContinue + + if ($dotnetCmd -ne $null) { + $env:DOTNET_INSTALL_DIR = Split-Path $dotnetCmd.Path -Parent + } + } + + $dotnetSdkVersion = $GlobalJson.tools.dotnet + + # Use dotnet installation specified in DOTNET_INSTALL_DIR if it contains the required SDK version, + # otherwise install the dotnet CLI and SDK to repo local .dotnet directory to avoid potential permission issues. + if ((-not $globalJsonHasRuntimes) -and (-not [string]::IsNullOrEmpty($env:DOTNET_INSTALL_DIR)) -and (Test-Path(Join-Path $env:DOTNET_INSTALL_DIR "sdk\$dotnetSdkVersion"))) { + $dotnetRoot = $env:DOTNET_INSTALL_DIR + } else { + $dotnetRoot = Join-Path $RepoRoot '.dotnet' + + if (-not (Test-Path(Join-Path $dotnetRoot "sdk\$dotnetSdkVersion"))) { + if ($install) { + InstallDotNetSdk $dotnetRoot $dotnetSdkVersion + } else { + Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "Unable to find dotnet with SDK version '$dotnetSdkVersion'" + ExitWithExitCode 1 + } + } + + $env:DOTNET_INSTALL_DIR = $dotnetRoot + } + + # Creates a temporary file under the toolset dir. + # The following code block is protecting against concurrent access so that this function can + # be called in parallel. + if ($createSdkLocationFile) { + do { + $sdkCacheFileTemp = Join-Path $ToolsetDir $([System.IO.Path]::GetRandomFileName()) + } + until (!(Test-Path $sdkCacheFileTemp)) + Set-Content -Path $sdkCacheFileTemp -Value $dotnetRoot + + try { + Move-Item -Force $sdkCacheFileTemp (Join-Path $ToolsetDir 'sdk.txt') + } catch { + # Somebody beat us + Remove-Item -Path $sdkCacheFileTemp + } + } + + # Add dotnet to PATH. This prevents any bare invocation of dotnet in custom + # build steps from using anything other than what we've downloaded. + # It also ensures that VS msbuild will use the downloaded sdk targets. + $env:PATH = "$dotnetRoot;$env:PATH" + + # Make Sure that our bootstrapped dotnet cli is available in future steps of the Azure Pipelines build + Write-PipelinePrependPath -Path $dotnetRoot + + Write-PipelineSetVariable -Name 'DOTNET_MULTILEVEL_LOOKUP' -Value '0' + Write-PipelineSetVariable -Name 'DOTNET_SKIP_FIRST_TIME_EXPERIENCE' -Value '1' + + return $global:_DotNetInstallDir = $dotnetRoot +} + +function Retry($downloadBlock, $maxRetries = 5) { + $retries = 1 + + while($true) { + try { + & $downloadBlock + break + } + catch { + Write-PipelineTelemetryError -Category 'InitializeToolset' -Message $_ + } + + if (++$retries -le $maxRetries) { + $delayInSeconds = [math]::Pow(2, $retries) - 1 # Exponential backoff + Write-Host "Retrying. Waiting for $delayInSeconds seconds before next attempt ($retries of $maxRetries)." + Start-Sleep -Seconds $delayInSeconds + } + else { + Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "Unable to download file in $maxRetries attempts." + break + } + + } +} + +function GetDotNetInstallScript([string] $dotnetRoot) { + $installScript = Join-Path $dotnetRoot 'dotnet-install.ps1' + if (!(Test-Path $installScript)) { + Create-Directory $dotnetRoot + $ProgressPreference = 'SilentlyContinue' # Don't display the console progress UI - it's a huge perf hit + $uri = "https://dotnet.microsoft.com/download/dotnet/scripts/$dotnetInstallScriptVersion/dotnet-install.ps1" + + Retry({ + Write-Host "GET $uri" + Invoke-WebRequest $uri -OutFile $installScript + }) + } + + return $installScript +} + +function InstallDotNetSdk([string] $dotnetRoot, [string] $version, [string] $architecture = '', [switch] $noPath) { + InstallDotNet $dotnetRoot $version $architecture '' $false $runtimeSourceFeed $runtimeSourceFeedKey -noPath:$noPath +} + +function InstallDotNet([string] $dotnetRoot, + [string] $version, + [string] $architecture = '', + [string] $runtime = '', + [bool] $skipNonVersionedFiles = $false, + [string] $runtimeSourceFeed = '', + [string] $runtimeSourceFeedKey = '', + [switch] $noPath) { + + $dotnetVersionLabel = "'sdk v$version'" + + if ($runtime -ne '' -and $runtime -ne 'sdk') { + $runtimePath = $dotnetRoot + $runtimePath = $runtimePath + "\shared" + if ($runtime -eq "dotnet") { $runtimePath = $runtimePath + "\Microsoft.NETCore.App" } + if ($runtime -eq "aspnetcore") { $runtimePath = $runtimePath + "\Microsoft.AspNetCore.App" } + if ($runtime -eq "windowsdesktop") { $runtimePath = $runtimePath + "\Microsoft.WindowsDesktop.App" } + $runtimePath = $runtimePath + "\" + $version + + $dotnetVersionLabel = "runtime toolset '$runtime/$architecture v$version'" + + if (Test-Path $runtimePath) { + Write-Host " Runtime toolset '$runtime/$architecture v$version' already installed." + $installSuccess = $true + Exit + } + } + + $installScript = GetDotNetInstallScript $dotnetRoot + $installParameters = @{ + Version = $version + InstallDir = $dotnetRoot + } + + if ($architecture) { $installParameters.Architecture = $architecture } + if ($runtime) { $installParameters.Runtime = $runtime } + if ($skipNonVersionedFiles) { $installParameters.SkipNonVersionedFiles = $skipNonVersionedFiles } + if ($noPath) { $installParameters.NoPath = $True } + + $variations = @() + $variations += @($installParameters) + + $dotnetBuilds = $installParameters.Clone() + $dotnetbuilds.AzureFeed = "https://dotnetbuilds.azureedge.net/public" + $variations += @($dotnetBuilds) + + if ($runtimeSourceFeed) { + $runtimeSource = $installParameters.Clone() + $runtimeSource.AzureFeed = $runtimeSourceFeed + if ($runtimeSourceFeedKey) { + $decodedBytes = [System.Convert]::FromBase64String($runtimeSourceFeedKey) + $decodedString = [System.Text.Encoding]::UTF8.GetString($decodedBytes) + $runtimeSource.FeedCredential = $decodedString + } + $variations += @($runtimeSource) + } + + $installSuccess = $false + foreach ($variation in $variations) { + if ($variation | Get-Member AzureFeed) { + $location = $variation.AzureFeed + } else { + $location = "public location"; + } + Write-Host " Attempting to install $dotnetVersionLabel from $location." + try { + & $installScript @variation + $installSuccess = $true + break + } + catch { + Write-Host " Failed to install $dotnetVersionLabel from $location." + } + } + if (-not $installSuccess) { + Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "Failed to install $dotnetVersionLabel from any of the specified locations." + ExitWithExitCode 1 + } +} + +# +# Locates Visual Studio MSBuild installation. +# The preference order for MSBuild to use is as follows: +# +# 1. MSBuild from an active VS command prompt +# 2. MSBuild from a compatible VS installation +# 3. MSBuild from the xcopy tool package +# +# Returns full path to msbuild.exe. +# Throws on failure. +# +function InitializeVisualStudioMSBuild([bool]$install, [object]$vsRequirements = $null) { + if (-not (IsWindowsPlatform)) { + throw "Cannot initialize Visual Studio on non-Windows" + } + + if (Test-Path variable:global:_MSBuildExe) { + return $global:_MSBuildExe + } + + # Minimum VS version to require. + $vsMinVersionReqdStr = '16.8' + $vsMinVersionReqd = [Version]::new($vsMinVersionReqdStr) + + # If the version of msbuild is going to be xcopied, + # use this version. Version matches a package here: + # https://dev.azure.com/dnceng/public/_packaging?_a=package&feed=dotnet-eng&package=RoslynTools.MSBuild&protocolType=NuGet&version=17.4.1&view=overview + $defaultXCopyMSBuildVersion = '17.4.1' + + if (!$vsRequirements) { + if (Get-Member -InputObject $GlobalJson.tools -Name 'vs') { + $vsRequirements = $GlobalJson.tools.vs + } + else { + $vsRequirements = New-Object PSObject -Property @{ version = $vsMinVersionReqdStr } + } + } + $vsMinVersionStr = if ($vsRequirements.version) { $vsRequirements.version } else { $vsMinVersionReqdStr } + $vsMinVersion = [Version]::new($vsMinVersionStr) + + # Try msbuild command available in the environment. + if ($env:VSINSTALLDIR -ne $null) { + $msbuildCmd = Get-Command 'msbuild.exe' -ErrorAction SilentlyContinue + if ($msbuildCmd -ne $null) { + # Workaround for https://github.com/dotnet/roslyn/issues/35793 + # Due to this issue $msbuildCmd.Version returns 0.0.0.0 for msbuild.exe 16.2+ + $msbuildVersion = [Version]::new((Get-Item $msbuildCmd.Path).VersionInfo.ProductVersion.Split([char[]]@('-', '+'))[0]) + + if ($msbuildVersion -ge $vsMinVersion) { + return $global:_MSBuildExe = $msbuildCmd.Path + } + + # Report error - the developer environment is initialized with incompatible VS version. + throw "Developer Command Prompt for VS $($env:VisualStudioVersion) is not recent enough. Please upgrade to $vsMinVersionStr or build from a plain CMD window" + } + } + + # Locate Visual Studio installation or download x-copy msbuild. + $vsInfo = LocateVisualStudio $vsRequirements + if ($vsInfo -ne $null) { + # Ensure vsInstallDir has a trailing slash + $vsInstallDir = Join-Path $vsInfo.installationPath "\" + $vsMajorVersion = $vsInfo.installationVersion.Split('.')[0] + + InitializeVisualStudioEnvironmentVariables $vsInstallDir $vsMajorVersion + } else { + + if (Get-Member -InputObject $GlobalJson.tools -Name 'xcopy-msbuild') { + $xcopyMSBuildVersion = $GlobalJson.tools.'xcopy-msbuild' + $vsMajorVersion = $xcopyMSBuildVersion.Split('.')[0] + } else { + #if vs version provided in global.json is incompatible (too low) then use the default version for xcopy msbuild download + if($vsMinVersion -lt $vsMinVersionReqd){ + Write-Host "Using xcopy-msbuild version of $defaultXCopyMSBuildVersion since VS version $vsMinVersionStr provided in global.json is not compatible" + $xcopyMSBuildVersion = $defaultXCopyMSBuildVersion + $vsMajorVersion = $xcopyMSBuildVersion.Split('.')[0] + } + else{ + # If the VS version IS compatible, look for an xcopy msbuild package + # with a version matching VS. + # Note: If this version does not exist, then an explicit version of xcopy msbuild + # can be specified in global.json. This will be required for pre-release versions of msbuild. + $vsMajorVersion = $vsMinVersion.Major + $vsMinorVersion = $vsMinVersion.Minor + $xcopyMSBuildVersion = "$vsMajorVersion.$vsMinorVersion.0" + } + } + + $vsInstallDir = $null + if ($xcopyMSBuildVersion.Trim() -ine "none") { + $vsInstallDir = InitializeXCopyMSBuild $xcopyMSBuildVersion $install + if ($vsInstallDir -eq $null) { + throw "Could not xcopy msbuild. Please check that package 'RoslynTools.MSBuild @ $xcopyMSBuildVersion' exists on feed 'dotnet-eng'." + } + } + if ($vsInstallDir -eq $null) { + throw 'Unable to find Visual Studio that has required version and components installed' + } + } + + $msbuildVersionDir = if ([int]$vsMajorVersion -lt 16) { "$vsMajorVersion.0" } else { "Current" } + + $local:BinFolder = Join-Path $vsInstallDir "MSBuild\$msbuildVersionDir\Bin" + $local:Prefer64bit = if (Get-Member -InputObject $vsRequirements -Name 'Prefer64bit') { $vsRequirements.Prefer64bit } else { $false } + if ($local:Prefer64bit -and (Test-Path(Join-Path $local:BinFolder "amd64"))) { + $global:_MSBuildExe = Join-Path $local:BinFolder "amd64\msbuild.exe" + } else { + $global:_MSBuildExe = Join-Path $local:BinFolder "msbuild.exe" + } + + return $global:_MSBuildExe +} + +function InitializeVisualStudioEnvironmentVariables([string] $vsInstallDir, [string] $vsMajorVersion) { + $env:VSINSTALLDIR = $vsInstallDir + Set-Item "env:VS$($vsMajorVersion)0COMNTOOLS" (Join-Path $vsInstallDir "Common7\Tools\") + + $vsSdkInstallDir = Join-Path $vsInstallDir "VSSDK\" + if (Test-Path $vsSdkInstallDir) { + Set-Item "env:VSSDK$($vsMajorVersion)0Install" $vsSdkInstallDir + $env:VSSDKInstall = $vsSdkInstallDir + } +} + +function InstallXCopyMSBuild([string]$packageVersion) { + return InitializeXCopyMSBuild $packageVersion -install $true +} + +function InitializeXCopyMSBuild([string]$packageVersion, [bool]$install) { + $packageName = 'RoslynTools.MSBuild' + $packageDir = Join-Path $ToolsDir "msbuild\$packageVersion" + $packagePath = Join-Path $packageDir "$packageName.$packageVersion.nupkg" + + if (!(Test-Path $packageDir)) { + if (!$install) { + return $null + } + + Create-Directory $packageDir + + Write-Host "Downloading $packageName $packageVersion" + $ProgressPreference = 'SilentlyContinue' # Don't display the console progress UI - it's a huge perf hit + Retry({ + Invoke-WebRequest "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/flat2/$packageName/$packageVersion/$packageName.$packageVersion.nupkg" -OutFile $packagePath + }) + + Unzip $packagePath $packageDir + } + + return Join-Path $packageDir 'tools' +} + +# +# Locates Visual Studio instance that meets the minimal requirements specified by tools.vs object in global.json. +# +# The following properties of tools.vs are recognized: +# "version": "{major}.{minor}" +# Two part minimal VS version, e.g. "15.9", "16.0", etc. +# "components": ["componentId1", "componentId2", ...] +# Array of ids of workload components that must be available in the VS instance. +# See e.g. https://docs.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-enterprise?view=vs-2017 +# +# Returns JSON describing the located VS instance (same format as returned by vswhere), +# or $null if no instance meeting the requirements is found on the machine. +# +function LocateVisualStudio([object]$vsRequirements = $null){ + if (-not (IsWindowsPlatform)) { + throw "Cannot run vswhere on non-Windows platforms." + } + + if (Get-Member -InputObject $GlobalJson.tools -Name 'vswhere') { + $vswhereVersion = $GlobalJson.tools.vswhere + } else { + $vswhereVersion = '2.5.2' + } + + $vsWhereDir = Join-Path $ToolsDir "vswhere\$vswhereVersion" + $vsWhereExe = Join-Path $vsWhereDir 'vswhere.exe' + + if (!(Test-Path $vsWhereExe)) { + Create-Directory $vsWhereDir + Write-Host 'Downloading vswhere' + Retry({ + Invoke-WebRequest "https://netcorenativeassets.blob.core.windows.net/resource-packages/external/windows/vswhere/$vswhereVersion/vswhere.exe" -OutFile $vswhereExe + }) + } + + if (!$vsRequirements) { $vsRequirements = $GlobalJson.tools.vs } + $args = @('-latest', '-format', 'json', '-requires', 'Microsoft.Component.MSBuild', '-products', '*') + + if (!$excludePrereleaseVS) { + $args += '-prerelease' + } + + if (Get-Member -InputObject $vsRequirements -Name 'version') { + $args += '-version' + $args += $vsRequirements.version + } + + if (Get-Member -InputObject $vsRequirements -Name 'components') { + foreach ($component in $vsRequirements.components) { + $args += '-requires' + $args += $component + } + } + + $vsInfo =& $vsWhereExe $args | ConvertFrom-Json + + if ($lastExitCode -ne 0) { + return $null + } + + # use first matching instance + return $vsInfo[0] +} + +function InitializeBuildTool() { + if (Test-Path variable:global:_BuildTool) { + # If the requested msbuild parameters do not match, clear the cached variables. + if($global:_BuildTool.Contains('ExcludePrereleaseVS') -and $global:_BuildTool.ExcludePrereleaseVS -ne $excludePrereleaseVS) { + Remove-Item variable:global:_BuildTool + Remove-Item variable:global:_MSBuildExe + } else { + return $global:_BuildTool + } + } + + if (-not $msbuildEngine) { + $msbuildEngine = GetDefaultMSBuildEngine + } + + # Initialize dotnet cli if listed in 'tools' + $dotnetRoot = $null + if (Get-Member -InputObject $GlobalJson.tools -Name 'dotnet') { + $dotnetRoot = InitializeDotNetCli -install:$restore + } + + if ($msbuildEngine -eq 'dotnet') { + if (!$dotnetRoot) { + Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "/global.json must specify 'tools.dotnet'." + ExitWithExitCode 1 + } + $dotnetPath = Join-Path $dotnetRoot (GetExecutableFileName 'dotnet') + $buildTool = @{ Path = $dotnetPath; Command = 'msbuild'; Tool = 'dotnet'; Framework = 'net8.0' } + } elseif ($msbuildEngine -eq "vs") { + try { + $msbuildPath = InitializeVisualStudioMSBuild -install:$restore + } catch { + Write-PipelineTelemetryError -Category 'InitializeToolset' -Message $_ + ExitWithExitCode 1 + } + + $buildTool = @{ Path = $msbuildPath; Command = ""; Tool = "vs"; Framework = "net472"; ExcludePrereleaseVS = $excludePrereleaseVS } + } else { + Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "Unexpected value of -msbuildEngine: '$msbuildEngine'." + ExitWithExitCode 1 + } + + return $global:_BuildTool = $buildTool +} + +function GetDefaultMSBuildEngine() { + # Presence of tools.vs indicates the repo needs to build using VS msbuild on Windows. + if (Get-Member -InputObject $GlobalJson.tools -Name 'vs') { + return 'vs' + } + + if (Get-Member -InputObject $GlobalJson.tools -Name 'dotnet') { + return 'dotnet' + } + + Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "-msbuildEngine must be specified, or /global.json must specify 'tools.dotnet' or 'tools.vs'." + ExitWithExitCode 1 +} + +function GetNuGetPackageCachePath() { + if ($env:NUGET_PACKAGES -eq $null) { + # Use local cache on CI to ensure deterministic build. + # Avoid using the http cache as workaround for https://github.com/NuGet/Home/issues/3116 + # use global cache in dev builds to avoid cost of downloading packages. + # For directory normalization, see also: https://github.com/NuGet/Home/issues/7968 + if ($useGlobalNuGetCache) { + $env:NUGET_PACKAGES = Join-Path $env:UserProfile '.nuget\packages\' + } else { + $env:NUGET_PACKAGES = Join-Path $RepoRoot '.packages\' + $env:RESTORENOCACHE = $true + } + } + + return $env:NUGET_PACKAGES +} + +# Returns a full path to an Arcade SDK task project file. +function GetSdkTaskProject([string]$taskName) { + return Join-Path (Split-Path (InitializeToolset) -Parent) "SdkTasks\$taskName.proj" +} + +function InitializeNativeTools() { + if (-Not (Test-Path variable:DisableNativeToolsetInstalls) -And (Get-Member -InputObject $GlobalJson -Name "native-tools")) { + $nativeArgs= @{} + if ($ci) { + $nativeArgs = @{ + InstallDirectory = "$ToolsDir" + } + } + if ($env:NativeToolsOnMachine) { + Write-Host "Variable NativeToolsOnMachine detected, enabling native tool path promotion..." + $nativeArgs += @{ PathPromotion = $true } + } + & "$PSScriptRoot/init-tools-native.ps1" @nativeArgs + } +} + +function InitializeToolset() { + if (Test-Path variable:global:_ToolsetBuildProj) { + return $global:_ToolsetBuildProj + } + + $nugetCache = GetNuGetPackageCachePath + + $toolsetVersion = $GlobalJson.'msbuild-sdks'.'Microsoft.DotNet.Arcade.Sdk' + $toolsetLocationFile = Join-Path $ToolsetDir "$toolsetVersion.txt" + + if (Test-Path $toolsetLocationFile) { + $path = Get-Content $toolsetLocationFile -TotalCount 1 + if (Test-Path $path) { + return $global:_ToolsetBuildProj = $path + } + } + + if (-not $restore) { + Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "Toolset version $toolsetVersion has not been restored." + ExitWithExitCode 1 + } + + $buildTool = InitializeBuildTool + + $proj = Join-Path $ToolsetDir 'restore.proj' + $bl = if ($binaryLog) { '/bl:' + (Join-Path $LogDir 'ToolsetRestore.binlog') } else { '' } + + '' | Set-Content $proj + + MSBuild-Core $proj $bl /t:__WriteToolsetLocation /clp:ErrorsOnly`;NoSummary /p:__ToolsetLocationOutputFile=$toolsetLocationFile + + $path = Get-Content $toolsetLocationFile -Encoding UTF8 -TotalCount 1 + if (!(Test-Path $path)) { + throw "Invalid toolset path: $path" + } + + return $global:_ToolsetBuildProj = $path +} + +function ExitWithExitCode([int] $exitCode) { + if ($ci -and $prepareMachine) { + Stop-Processes + } + exit $exitCode +} + +# Check if $LASTEXITCODE is a nonzero exit code (NZEC). If so, print a Azure Pipeline error for +# diagnostics, then exit the script with the $LASTEXITCODE. +function Exit-IfNZEC([string] $category = "General") { + Write-Host "Exit code $LASTEXITCODE" + if ($LASTEXITCODE -ne 0) { + $message = "Last command failed with exit code $LASTEXITCODE." + Write-PipelineTelemetryError -Force -Category $category -Message $message + ExitWithExitCode $LASTEXITCODE + } +} + +function Stop-Processes() { + Write-Host 'Killing running build processes...' + foreach ($processName in $processesToStopOnExit) { + Get-Process -Name $processName -ErrorAction SilentlyContinue | Stop-Process + } +} + +# +# Executes msbuild (or 'dotnet msbuild') with arguments passed to the function. +# The arguments are automatically quoted. +# Terminates the script if the build fails. +# +function MSBuild() { + if ($pipelinesLog) { + $buildTool = InitializeBuildTool + + if ($ci -and $buildTool.Tool -eq 'dotnet') { + $env:NUGET_PLUGIN_HANDSHAKE_TIMEOUT_IN_SECONDS = 20 + $env:NUGET_PLUGIN_REQUEST_TIMEOUT_IN_SECONDS = 20 + Write-PipelineSetVariable -Name 'NUGET_PLUGIN_HANDSHAKE_TIMEOUT_IN_SECONDS' -Value '20' + Write-PipelineSetVariable -Name 'NUGET_PLUGIN_REQUEST_TIMEOUT_IN_SECONDS' -Value '20' + } + + Enable-Nuget-EnhancedRetry + + $toolsetBuildProject = InitializeToolset + $basePath = Split-Path -parent $toolsetBuildProject + $possiblePaths = @( + # new scripts need to work with old packages, so we need to look for the old names/versions + (Join-Path $basePath (Join-Path $buildTool.Framework 'Microsoft.DotNet.ArcadeLogging.dll')), + (Join-Path $basePath (Join-Path $buildTool.Framework 'Microsoft.DotNet.Arcade.Sdk.dll')), + (Join-Path $basePath (Join-Path netcoreapp2.1 'Microsoft.DotNet.ArcadeLogging.dll')), + (Join-Path $basePath (Join-Path netcoreapp2.1 'Microsoft.DotNet.Arcade.Sdk.dll')) + (Join-Path $basePath (Join-Path netcoreapp3.1 'Microsoft.DotNet.ArcadeLogging.dll')), + (Join-Path $basePath (Join-Path netcoreapp3.1 'Microsoft.DotNet.Arcade.Sdk.dll')) + (Join-Path $basePath (Join-Path net7.0 'Microsoft.DotNet.ArcadeLogging.dll')), + (Join-Path $basePath (Join-Path net7.0 'Microsoft.DotNet.Arcade.Sdk.dll')) + ) + $selectedPath = $null + foreach ($path in $possiblePaths) { + if (Test-Path $path -PathType Leaf) { + $selectedPath = $path + break + } + } + if (-not $selectedPath) { + Write-PipelineTelemetryError -Category 'Build' -Message 'Unable to find arcade sdk logger assembly.' + ExitWithExitCode 1 + } + $args += "/logger:$selectedPath" + } + + MSBuild-Core @args +} + +# +# Executes msbuild (or 'dotnet msbuild') with arguments passed to the function. +# The arguments are automatically quoted. +# Terminates the script if the build fails. +# +function MSBuild-Core() { + if ($ci) { + if (!$binaryLog -and !$excludeCIBinarylog) { + Write-PipelineTelemetryError -Category 'Build' -Message 'Binary log must be enabled in CI build, or explicitly opted-out from with the -excludeCIBinarylog switch.' + ExitWithExitCode 1 + } + + if ($nodeReuse) { + Write-PipelineTelemetryError -Category 'Build' -Message 'Node reuse must be disabled in CI build.' + ExitWithExitCode 1 + } + } + + Enable-Nuget-EnhancedRetry + + $buildTool = InitializeBuildTool + + $cmdArgs = "$($buildTool.Command) /m /nologo /clp:Summary /v:$verbosity /nr:$nodeReuse /p:ContinuousIntegrationBuild=$ci" + + if ($warnAsError) { + $cmdArgs += ' /warnaserror /p:TreatWarningsAsErrors=true' + } + else { + $cmdArgs += ' /p:TreatWarningsAsErrors=false' + } + + foreach ($arg in $args) { + if ($null -ne $arg -and $arg.Trim() -ne "") { + if ($arg.EndsWith('\')) { + $arg = $arg + "\" + } + $cmdArgs += " `"$arg`"" + } + } + + $env:ARCADE_BUILD_TOOL_COMMAND = "$($buildTool.Path) $cmdArgs" + + $exitCode = Exec-Process $buildTool.Path $cmdArgs + + if ($exitCode -ne 0) { + # We should not Write-PipelineTaskError here because that message shows up in the build summary + # The build already logged an error, that's the reason it failed. Producing an error here only adds noise. + Write-Host "Build failed with exit code $exitCode. Check errors above." -ForegroundColor Red + + $buildLog = GetMSBuildBinaryLogCommandLineArgument $args + if ($null -ne $buildLog) { + Write-Host "See log: $buildLog" -ForegroundColor DarkGray + } + + # When running on Azure Pipelines, override the returned exit code to avoid double logging. + if ($ci -and $env:SYSTEM_TEAMPROJECT -ne $null) { + Write-PipelineSetResult -Result "Failed" -Message "msbuild execution failed." + # Exiting with an exit code causes the azure pipelines task to log yet another "noise" error + # The above Write-PipelineSetResult will cause the task to be marked as failure without adding yet another error + ExitWithExitCode 0 + } else { + ExitWithExitCode $exitCode + } + } +} + +function GetMSBuildBinaryLogCommandLineArgument($arguments) { + foreach ($argument in $arguments) { + if ($argument -ne $null) { + $arg = $argument.Trim() + if ($arg.StartsWith('/bl:', "OrdinalIgnoreCase")) { + return $arg.Substring('/bl:'.Length) + } + + if ($arg.StartsWith('/binaryLogger:', 'OrdinalIgnoreCase')) { + return $arg.Substring('/binaryLogger:'.Length) + } + } + } + + return $null +} + +function GetExecutableFileName($baseName) { + if (IsWindowsPlatform) { + return "$baseName.exe" + } + else { + return $baseName + } +} + +function IsWindowsPlatform() { + return [environment]::OSVersion.Platform -eq [PlatformID]::Win32NT +} + +function Get-Darc($version) { + $darcPath = "$TempDir\darc\$(New-Guid)" + if ($version -ne $null) { + & $PSScriptRoot\darc-init.ps1 -toolpath $darcPath -darcVersion $version | Out-Host + } else { + & $PSScriptRoot\darc-init.ps1 -toolpath $darcPath | Out-Host + } + return "$darcPath\darc.exe" +} + +. $PSScriptRoot\pipeline-logging-functions.ps1 + +$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..\') +$EngRoot = Resolve-Path (Join-Path $PSScriptRoot '..') +$ArtifactsDir = Join-Path $RepoRoot 'artifacts' +$ToolsetDir = Join-Path $ArtifactsDir 'toolset' +$ToolsDir = Join-Path $RepoRoot '.tools' +$LogDir = Join-Path (Join-Path $ArtifactsDir 'log') $configuration +$TempDir = Join-Path (Join-Path $ArtifactsDir 'tmp') $configuration +$GlobalJson = Get-Content -Raw -Path (Join-Path $RepoRoot 'global.json') | ConvertFrom-Json +# true if global.json contains a "runtimes" section +$globalJsonHasRuntimes = if ($GlobalJson.tools.PSObject.Properties.Name -Match 'runtimes') { $true } else { $false } + +Create-Directory $ToolsetDir +Create-Directory $TempDir +Create-Directory $LogDir + +Write-PipelineSetVariable -Name 'Artifacts' -Value $ArtifactsDir +Write-PipelineSetVariable -Name 'Artifacts.Toolset' -Value $ToolsetDir +Write-PipelineSetVariable -Name 'Artifacts.Log' -Value $LogDir +Write-PipelineSetVariable -Name 'TEMP' -Value $TempDir +Write-PipelineSetVariable -Name 'TMP' -Value $TempDir + +# Import custom tools configuration, if present in the repo. +# Note: Import in global scope so that the script set top-level variables without qualification. +if (!$disableConfigureToolsetImport) { + $configureToolsetScript = Join-Path $EngRoot 'configure-toolset.ps1' + if (Test-Path $configureToolsetScript) { + . $configureToolsetScript + if ((Test-Path variable:failOnConfigureToolsetError) -And $failOnConfigureToolsetError) { + if ((Test-Path variable:LastExitCode) -And ($LastExitCode -ne 0)) { + Write-PipelineTelemetryError -Category 'Build' -Message 'configure-toolset.ps1 returned a non-zero exit code' + ExitWithExitCode $LastExitCode + } + } + } +} + +# +# If $ci flag is set, turn on (and log that we did) special environment variables for improved Nuget client retry logic. +# +function Enable-Nuget-EnhancedRetry() { + if ($ci) { + Write-Host "Setting NUGET enhanced retry environment variables" + $env:NUGET_ENABLE_ENHANCED_HTTP_RETRY = 'true' + $env:NUGET_ENHANCED_MAX_NETWORK_TRY_COUNT = 6 + $env:NUGET_ENHANCED_NETWORK_RETRY_DELAY_MILLISECONDS = 1000 + $env:NUGET_RETRY_HTTP_429 = 'true' + Write-PipelineSetVariable -Name 'NUGET_ENABLE_ENHANCED_HTTP_RETRY' -Value 'true' + Write-PipelineSetVariable -Name 'NUGET_ENHANCED_MAX_NETWORK_TRY_COUNT' -Value '6' + Write-PipelineSetVariable -Name 'NUGET_ENHANCED_NETWORK_RETRY_DELAY_MILLISECONDS' -Value '1000' + Write-PipelineSetVariable -Name 'NUGET_RETRY_HTTP_429' -Value 'true' + } +} diff --git a/eng/common/tools.sh b/eng/common/tools.sh new file mode 100755 index 0000000000..e8d4789433 --- /dev/null +++ b/eng/common/tools.sh @@ -0,0 +1,587 @@ +#!/usr/bin/env bash + +# Initialize variables if they aren't already defined. + +# CI mode - set to true on CI server for PR validation build or official build. +ci=${ci:-false} + +# Set to true to use the pipelines logger which will enable Azure logging output. +# https://github.com/Microsoft/azure-pipelines-tasks/blob/master/docs/authoring/commands.md +# This flag is meant as a temporary opt-opt for the feature while validate it across +# our consumers. It will be deleted in the future. +if [[ "$ci" == true ]]; then + pipelines_log=${pipelines_log:-true} +else + pipelines_log=${pipelines_log:-false} +fi + +# Build configuration. Common values include 'Debug' and 'Release', but the repository may use other names. +configuration=${configuration:-'Debug'} + +# Set to true to opt out of outputting binary log while running in CI +exclude_ci_binary_log=${exclude_ci_binary_log:-false} + +if [[ "$ci" == true && "$exclude_ci_binary_log" == false ]]; then + binary_log_default=true +else + binary_log_default=false +fi + +# Set to true to output binary log from msbuild. Note that emitting binary log slows down the build. +binary_log=${binary_log:-$binary_log_default} + +# Turns on machine preparation/clean up code that changes the machine state (e.g. kills build processes). +prepare_machine=${prepare_machine:-false} + +# True to restore toolsets and dependencies. +restore=${restore:-true} + +# Adjusts msbuild verbosity level. +verbosity=${verbosity:-'minimal'} + +# Set to true to reuse msbuild nodes. Recommended to not reuse on CI. +if [[ "$ci" == true ]]; then + node_reuse=${node_reuse:-false} +else + node_reuse=${node_reuse:-true} +fi + +# Configures warning treatment in msbuild. +warn_as_error=${warn_as_error:-true} + +# True to attempt using .NET Core already that meets requirements specified in global.json +# installed on the machine instead of downloading one. +use_installed_dotnet_cli=${use_installed_dotnet_cli:-true} + +# Enable repos to use a particular version of the on-line dotnet-install scripts. +# default URL: https://dotnet.microsoft.com/download/dotnet/scripts/v1/dotnet-install.sh +dotnetInstallScriptVersion=${dotnetInstallScriptVersion:-'v1'} + +# True to use global NuGet cache instead of restoring packages to repository-local directory. +if [[ "$ci" == true ]]; then + use_global_nuget_cache=${use_global_nuget_cache:-false} +else + use_global_nuget_cache=${use_global_nuget_cache:-true} +fi + +# Used when restoring .NET SDK from alternative feeds +runtime_source_feed=${runtime_source_feed:-''} +runtime_source_feed_key=${runtime_source_feed_key:-''} + +# Resolve any symlinks in the given path. +function ResolvePath { + local path=$1 + + while [[ -h $path ]]; do + local dir="$( cd -P "$( dirname "$path" )" && pwd )" + path="$(readlink "$path")" + + # if $path was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $path != /* ]] && path="$dir/$path" + done + + # return value + _ResolvePath="$path" +} + +# ReadVersionFromJson [json key] +function ReadGlobalVersion { + local key=$1 + + if command -v jq &> /dev/null; then + _ReadGlobalVersion="$(jq -r ".[] | select(has(\"$key\")) | .\"$key\"" "$global_json_file")" + elif [[ "$(cat "$global_json_file")" =~ \"$key\"[[:space:]\:]*\"([^\"]+) ]]; then + _ReadGlobalVersion=${BASH_REMATCH[1]} + fi + + if [[ -z "$_ReadGlobalVersion" ]]; then + Write-PipelineTelemetryError -category 'Build' "Error: Cannot find \"$key\" in $global_json_file" + ExitWithExitCode 1 + fi +} + +function InitializeDotNetCli { + if [[ -n "${_InitializeDotNetCli:-}" ]]; then + return + fi + + local install=$1 + + # Don't resolve runtime, shared framework, or SDK from other locations to ensure build determinism + export DOTNET_MULTILEVEL_LOOKUP=0 + + # Disable first run since we want to control all package sources + export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 + + # Disable telemetry on CI + if [[ $ci == true ]]; then + export DOTNET_CLI_TELEMETRY_OPTOUT=1 + fi + + # LTTNG is the logging infrastructure used by Core CLR. Need this variable set + # so it doesn't output warnings to the console. + export LTTNG_HOME="$HOME" + + # Source Build uses DotNetCoreSdkDir variable + if [[ -n "${DotNetCoreSdkDir:-}" ]]; then + export DOTNET_INSTALL_DIR="$DotNetCoreSdkDir" + fi + + # Find the first path on $PATH that contains the dotnet.exe + if [[ "$use_installed_dotnet_cli" == true && $global_json_has_runtimes == false && -z "${DOTNET_INSTALL_DIR:-}" ]]; then + local dotnet_path=`command -v dotnet` + if [[ -n "$dotnet_path" ]]; then + ResolvePath "$dotnet_path" + export DOTNET_INSTALL_DIR=`dirname "$_ResolvePath"` + fi + fi + + ReadGlobalVersion "dotnet" + local dotnet_sdk_version=$_ReadGlobalVersion + local dotnet_root="" + + # Use dotnet installation specified in DOTNET_INSTALL_DIR if it contains the required SDK version, + # otherwise install the dotnet CLI and SDK to repo local .dotnet directory to avoid potential permission issues. + if [[ $global_json_has_runtimes == false && -n "${DOTNET_INSTALL_DIR:-}" && -d "$DOTNET_INSTALL_DIR/sdk/$dotnet_sdk_version" ]]; then + dotnet_root="$DOTNET_INSTALL_DIR" + else + dotnet_root="$repo_root/.dotnet" + + export DOTNET_INSTALL_DIR="$dotnet_root" + + if [[ ! -d "$DOTNET_INSTALL_DIR/sdk/$dotnet_sdk_version" ]]; then + if [[ "$install" == true ]]; then + InstallDotNetSdk "$dotnet_root" "$dotnet_sdk_version" + else + Write-PipelineTelemetryError -category 'InitializeToolset' "Unable to find dotnet with SDK version '$dotnet_sdk_version'" + ExitWithExitCode 1 + fi + fi + fi + + # Add dotnet to PATH. This prevents any bare invocation of dotnet in custom + # build steps from using anything other than what we've downloaded. + Write-PipelinePrependPath -path "$dotnet_root" + + Write-PipelineSetVariable -name "DOTNET_MULTILEVEL_LOOKUP" -value "0" + Write-PipelineSetVariable -name "DOTNET_SKIP_FIRST_TIME_EXPERIENCE" -value "1" + + # return value + _InitializeDotNetCli="$dotnet_root" +} + +function InstallDotNetSdk { + local root=$1 + local version=$2 + local architecture="unset" + if [[ $# -ge 3 ]]; then + architecture=$3 + fi + InstallDotNet "$root" "$version" $architecture 'sdk' 'true' $runtime_source_feed $runtime_source_feed_key +} + +function InstallDotNet { + local root=$1 + local version=$2 + local runtime=$4 + + local dotnetVersionLabel="'$runtime v$version'" + if [[ -n "${4:-}" ]] && [ "$4" != 'sdk' ]; then + runtimePath="$root" + runtimePath="$runtimePath/shared" + case "$runtime" in + dotnet) + runtimePath="$runtimePath/Microsoft.NETCore.App" + ;; + aspnetcore) + runtimePath="$runtimePath/Microsoft.AspNetCore.App" + ;; + windowsdesktop) + runtimePath="$runtimePath/Microsoft.WindowsDesktop.App" + ;; + *) + ;; + esac + runtimePath="$runtimePath/$version" + + dotnetVersionLabel="runtime toolset '$runtime/$architecture v$version'" + + if [ -d "$runtimePath" ]; then + echo " Runtime toolset '$runtime/$architecture v$version' already installed." + local installSuccess=1 + return + fi + fi + + GetDotNetInstallScript "$root" + local install_script=$_GetDotNetInstallScript + + local installParameters=(--version $version --install-dir "$root") + + if [[ -n "${3:-}" ]] && [ "$3" != 'unset' ]; then + installParameters+=(--architecture $3) + fi + if [[ -n "${4:-}" ]] && [ "$4" != 'sdk' ]; then + installParameters+=(--runtime $4) + fi + if [[ "$#" -ge "5" ]] && [[ "$5" != 'false' ]]; then + installParameters+=(--skip-non-versioned-files) + fi + + local variations=() # list of variable names with parameter arrays in them + + local public_location=("${installParameters[@]}") + variations+=(public_location) + + local dotnetbuilds=("${installParameters[@]}" --azure-feed "https://dotnetbuilds.azureedge.net/public") + variations+=(dotnetbuilds) + + if [[ -n "${6:-}" ]]; then + variations+=(private_feed) + local private_feed=("${installParameters[@]}" --azure-feed $6) + if [[ -n "${7:-}" ]]; then + # The 'base64' binary on alpine uses '-d' and doesn't support '--decode' + # '-d'. To work around this, do a simple detection and switch the parameter + # accordingly. + decodeArg="--decode" + if base64 --help 2>&1 | grep -q "BusyBox"; then + decodeArg="-d" + fi + decodedFeedKey=`echo $7 | base64 $decodeArg` + private_feed+=(--feed-credential $decodedFeedKey) + fi + fi + + local installSuccess=0 + for variationName in "${variations[@]}"; do + local name="$variationName[@]" + local variation=("${!name}") + echo " Attempting to install $dotnetVersionLabel from $variationName." + bash "$install_script" "${variation[@]}" && installSuccess=1 + if [[ "$installSuccess" -eq 1 ]]; then + break + fi + + echo " Failed to install $dotnetVersionLabel from $variationName." + done + + if [[ "$installSuccess" -eq 0 ]]; then + Write-PipelineTelemetryError -category 'InitializeToolset' "Failed to install $dotnetVersionLabel from any of the specified locations." + ExitWithExitCode 1 + fi +} + +function with_retries { + local maxRetries=5 + local retries=1 + echo "Trying to run '$@' for maximum of $maxRetries attempts." + while [[ $((retries++)) -le $maxRetries ]]; do + "$@" + + if [[ $? == 0 ]]; then + echo "Ran '$@' successfully." + return 0 + fi + + timeout=$((3**$retries-1)) + echo "Failed to execute '$@'. Waiting $timeout seconds before next attempt ($retries out of $maxRetries)." 1>&2 + sleep $timeout + done + + echo "Failed to execute '$@' for $maxRetries times." 1>&2 + + return 1 +} + +function GetDotNetInstallScript { + local root=$1 + local install_script="$root/dotnet-install.sh" + local install_script_url="https://dotnet.microsoft.com/download/dotnet/scripts/$dotnetInstallScriptVersion/dotnet-install.sh" + + if [[ ! -a "$install_script" ]]; then + mkdir -p "$root" + + echo "Downloading '$install_script_url'" + + # Use curl if available, otherwise use wget + if command -v curl > /dev/null; then + # first, try directly, if this fails we will retry with verbose logging + curl "$install_script_url" -sSL --retry 10 --create-dirs -o "$install_script" || { + if command -v openssl &> /dev/null; then + echo "Curl failed; dumping some information about dotnet.microsoft.com for later investigation" + echo | openssl s_client -showcerts -servername dotnet.microsoft.com -connect dotnet.microsoft.com:443 + fi + echo "Will now retry the same URL with verbose logging." + with_retries curl "$install_script_url" -sSL --verbose --retry 10 --create-dirs -o "$install_script" || { + local exit_code=$? + Write-PipelineTelemetryError -category 'InitializeToolset' "Failed to acquire dotnet install script (exit code '$exit_code')." + ExitWithExitCode $exit_code + } + } + else + with_retries wget -v -O "$install_script" "$install_script_url" || { + local exit_code=$? + Write-PipelineTelemetryError -category 'InitializeToolset' "Failed to acquire dotnet install script (exit code '$exit_code')." + ExitWithExitCode $exit_code + } + fi + fi + # return value + _GetDotNetInstallScript="$install_script" +} + +function InitializeBuildTool { + if [[ -n "${_InitializeBuildTool:-}" ]]; then + return + fi + + InitializeDotNetCli $restore + + # return values + _InitializeBuildTool="$_InitializeDotNetCli/dotnet" + _InitializeBuildToolCommand="msbuild" + _InitializeBuildToolFramework="net8.0" +} + +# Set RestoreNoCache as a workaround for https://github.com/NuGet/Home/issues/3116 +function GetNuGetPackageCachePath { + if [[ -z ${NUGET_PACKAGES:-} ]]; then + if [[ "$use_global_nuget_cache" == true ]]; then + export NUGET_PACKAGES="$HOME/.nuget/packages" + else + export NUGET_PACKAGES="$repo_root/.packages" + export RESTORENOCACHE=true + fi + fi + + # return value + _GetNuGetPackageCachePath=$NUGET_PACKAGES +} + +function InitializeNativeTools() { + if [[ -n "${DisableNativeToolsetInstalls:-}" ]]; then + return + fi + if grep -Fq "native-tools" $global_json_file + then + local nativeArgs="" + if [[ "$ci" == true ]]; then + nativeArgs="--installDirectory $tools_dir" + fi + "$_script_dir/init-tools-native.sh" $nativeArgs + fi +} + +function InitializeToolset { + if [[ -n "${_InitializeToolset:-}" ]]; then + return + fi + + GetNuGetPackageCachePath + + ReadGlobalVersion "Microsoft.DotNet.Arcade.Sdk" + + local toolset_version=$_ReadGlobalVersion + local toolset_location_file="$toolset_dir/$toolset_version.txt" + + if [[ -a "$toolset_location_file" ]]; then + local path=`cat "$toolset_location_file"` + if [[ -a "$path" ]]; then + # return value + _InitializeToolset="$path" + return + fi + fi + + if [[ "$restore" != true ]]; then + Write-PipelineTelemetryError -category 'InitializeToolset' "Toolset version $toolset_version has not been restored." + ExitWithExitCode 2 + fi + + local proj="$toolset_dir/restore.proj" + + local bl="" + if [[ "$binary_log" == true ]]; then + bl="/bl:$log_dir/ToolsetRestore.binlog" + fi + + echo '' > "$proj" + MSBuild-Core "$proj" $bl /t:__WriteToolsetLocation /clp:ErrorsOnly\;NoSummary /p:__ToolsetLocationOutputFile="$toolset_location_file" + + local toolset_build_proj=`cat "$toolset_location_file"` + + if [[ ! -a "$toolset_build_proj" ]]; then + Write-PipelineTelemetryError -category 'Build' "Invalid toolset path: $toolset_build_proj" + ExitWithExitCode 3 + fi + + # return value + _InitializeToolset="$toolset_build_proj" +} + +function ExitWithExitCode { + if [[ "$ci" == true && "$prepare_machine" == true ]]; then + StopProcesses + fi + exit $1 +} + +function StopProcesses { + echo "Killing running build processes..." + pkill -9 "dotnet" || true + pkill -9 "vbcscompiler" || true + return 0 +} + +function MSBuild { + local args=$@ + if [[ "$pipelines_log" == true ]]; then + InitializeBuildTool + InitializeToolset + + if [[ "$ci" == true ]]; then + export NUGET_PLUGIN_HANDSHAKE_TIMEOUT_IN_SECONDS=20 + export NUGET_PLUGIN_REQUEST_TIMEOUT_IN_SECONDS=20 + Write-PipelineSetVariable -name "NUGET_PLUGIN_HANDSHAKE_TIMEOUT_IN_SECONDS" -value "20" + Write-PipelineSetVariable -name "NUGET_PLUGIN_REQUEST_TIMEOUT_IN_SECONDS" -value "20" + fi + + local toolset_dir="${_InitializeToolset%/*}" + # new scripts need to work with old packages, so we need to look for the old names/versions + local selectedPath= + local possiblePaths=() + possiblePaths+=( "$toolset_dir/$_InitializeBuildToolFramework/Microsoft.DotNet.ArcadeLogging.dll" ) + possiblePaths+=( "$toolset_dir/$_InitializeBuildToolFramework/Microsoft.DotNet.Arcade.Sdk.dll" ) + possiblePaths+=( "$toolset_dir/netcoreapp2.1/Microsoft.DotNet.ArcadeLogging.dll" ) + possiblePaths+=( "$toolset_dir/netcoreapp2.1/Microsoft.DotNet.Arcade.Sdk.dll" ) + possiblePaths+=( "$toolset_dir/netcoreapp3.1/Microsoft.DotNet.ArcadeLogging.dll" ) + possiblePaths+=( "$toolset_dir/netcoreapp3.1/Microsoft.DotNet.Arcade.Sdk.dll" ) + possiblePaths+=( "$toolset_dir/net7.0/Microsoft.DotNet.ArcadeLogging.dll" ) + possiblePaths+=( "$toolset_dir/net7.0/Microsoft.DotNet.Arcade.Sdk.dll" ) + for path in "${possiblePaths[@]}"; do + if [[ -f $path ]]; then + selectedPath=$path + break + fi + done + if [[ -z "$selectedPath" ]]; then + Write-PipelineTelemetryError -category 'Build' "Unable to find arcade sdk logger assembly." + ExitWithExitCode 1 + fi + args+=( "-logger:$selectedPath" ) + fi + + MSBuild-Core ${args[@]} +} + +function MSBuild-Core { + if [[ "$ci" == true ]]; then + if [[ "$binary_log" != true && "$exclude_ci_binary_log" != true ]]; then + Write-PipelineTelemetryError -category 'Build' "Binary log must be enabled in CI build, or explicitly opted-out from with the -noBinaryLog switch." + ExitWithExitCode 1 + fi + + if [[ "$node_reuse" == true ]]; then + Write-PipelineTelemetryError -category 'Build' "Node reuse must be disabled in CI build." + ExitWithExitCode 1 + fi + fi + + InitializeBuildTool + + local warnaserror_switch="" + if [[ $warn_as_error == true ]]; then + warnaserror_switch="/warnaserror" + fi + + function RunBuildTool { + export ARCADE_BUILD_TOOL_COMMAND="$_InitializeBuildTool $@" + + "$_InitializeBuildTool" "$@" || { + local exit_code=$? + # We should not Write-PipelineTaskError here because that message shows up in the build summary + # The build already logged an error, that's the reason it failed. Producing an error here only adds noise. + echo "Build failed with exit code $exit_code. Check errors above." + + # When running on Azure Pipelines, override the returned exit code to avoid double logging. + if [[ "$ci" == "true" && -n ${SYSTEM_TEAMPROJECT:-} ]]; then + Write-PipelineSetResult -result "Failed" -message "msbuild execution failed." + # Exiting with an exit code causes the azure pipelines task to log yet another "noise" error + # The above Write-PipelineSetResult will cause the task to be marked as failure without adding yet another error + ExitWithExitCode 0 + else + ExitWithExitCode $exit_code + fi + } + } + + RunBuildTool "$_InitializeBuildToolCommand" /m /nologo /clp:Summary /v:$verbosity /nr:$node_reuse $warnaserror_switch /p:TreatWarningsAsErrors=$warn_as_error /p:ContinuousIntegrationBuild=$ci "$@" +} + +function GetDarc { + darc_path="$temp_dir/darc" + version="$1" + + if [[ -n "$version" ]]; then + version="--darcversion $version" + fi + + "$eng_root/common/darc-init.sh" --toolpath "$darc_path" $version +} + +ResolvePath "${BASH_SOURCE[0]}" +_script_dir=`dirname "$_ResolvePath"` + +. "$_script_dir/pipeline-logging-functions.sh" + +eng_root=`cd -P "$_script_dir/.." && pwd` +repo_root=`cd -P "$_script_dir/../.." && pwd` +repo_root="${repo_root}/" +artifacts_dir="${repo_root}artifacts" +toolset_dir="$artifacts_dir/toolset" +tools_dir="${repo_root}.tools" +log_dir="$artifacts_dir/log/$configuration" +temp_dir="$artifacts_dir/tmp/$configuration" + +global_json_file="${repo_root}global.json" +# determine if global.json contains a "runtimes" entry +global_json_has_runtimes=false +if command -v jq &> /dev/null; then + if jq -e '.tools | has("runtimes")' "$global_json_file" &> /dev/null; then + global_json_has_runtimes=true + fi +elif [[ "$(cat "$global_json_file")" =~ \"runtimes\"[[:space:]\:]*\{ ]]; then + global_json_has_runtimes=true +fi + +# HOME may not be defined in some scenarios, but it is required by NuGet +if [[ -z $HOME ]]; then + export HOME="${repo_root}artifacts/.home/" + mkdir -p "$HOME" +fi + +mkdir -p "$toolset_dir" +mkdir -p "$temp_dir" +mkdir -p "$log_dir" + +Write-PipelineSetVariable -name "Artifacts" -value "$artifacts_dir" +Write-PipelineSetVariable -name "Artifacts.Toolset" -value "$toolset_dir" +Write-PipelineSetVariable -name "Artifacts.Log" -value "$log_dir" +Write-PipelineSetVariable -name "Temp" -value "$temp_dir" +Write-PipelineSetVariable -name "TMP" -value "$temp_dir" + +# Import custom tools configuration, if present in the repo. +if [ -z "${disable_configure_toolset_import:-}" ]; then + configure_toolset_script="$eng_root/configure-toolset.sh" + if [[ -a "$configure_toolset_script" ]]; then + . "$configure_toolset_script" + fi +fi + +# TODO: https://github.com/dotnet/arcade/issues/1468 +# Temporary workaround to avoid breaking change. +# Remove once repos are updated. +if [[ -n "${useInstalledDotNetCli:-}" ]]; then + use_installed_dotnet_cli="$useInstalledDotNetCli" +fi diff --git a/eng/pipelines/templates/BuildAndTest.yml b/eng/pipelines/templates/BuildAndTest.yml new file mode 100644 index 0000000000..e259898954 --- /dev/null +++ b/eng/pipelines/templates/BuildAndTest.yml @@ -0,0 +1,108 @@ +parameters: + - name: buildScript + type: string + - name: buildConfig + type: string + - name: repoLogPath + type: string + - name: repoTestResultsPath + type: string + - name: isDeltaBuild + type: string + - name: isWindows + type: string + - name: skipCodeCoverage + type: boolean + default: false + - name: skipTests + type: boolean + default: false + - name: warnAsError + type: number + default: 1 + +steps: + # Debug + # - pwsh: | + # Write-Host 'buildScript: ${{ parameters.buildScript }}' + # Write-Host 'buildConfig: ${{ parameters.buildConfig }}' + # Write-Host 'repoLogPath: ${{ parameters.repoLogPath }}' + # Write-Host 'repoTestResultsPath: ${{ parameters.repoTestResultsPath }}' + # Write-Host 'isDeltaBuild: ${{ parameters.isDeltaBuild }}' + # Write-Host 'isWindows: ${{ parameters.isWindows }}' + + # Write-Host 'skipCodeCoverage: ${{ parameters.skipCodeCoverage }}' + # Write-Host 'skipCodeCoverage == true: ${{ eq(parameters.skipCodeCoverage, true) }}' + # Write-Host "skipCodeCoverage == 'true': ${{ eq(parameters.skipCodeCoverage, 'true') }}" + # Write-Host 'skipCodeCoverage != true: ${{ ne(parameters.skipCodeCoverage, true) }}' + # Write-Host "skipCodeCoverage != 'true': ${{ ne(parameters.skipCodeCoverage, 'true') }}" + # Write-Host 'running: ${{ and(eq(parameters.isWindows, 'true'), ne(parameters.skipCodeCoverage, true)) }}' + + # Get-ChildItem env:* | Sort-Object Name + # displayName: Debug + + - pwsh: | + function Export { param($i); Write-Host "$i"; Write-Host "##$i" } + Export "vso[task.setvariable variable=IsDeltaBuild]${{ parameters.isDeltaBuild }}" + displayName: Set flags + + - script: ${{ parameters.buildScript }} + -restore + /bl:${{ parameters.repoLogPath }}/restore.binlog + displayName: Restore + + - script: ${{ parameters.buildScript }} + -build + -configuration ${{ parameters.buildConfig }} + -warnAsError ${{ parameters.warnAsError }} + /bl:${{ parameters.repoLogPath }}/build.binlog + $(_OfficialBuildIdArgs) + displayName: Build + + - ${{ if ne(parameters.skipTests, 'true') }}: + - script: $(Build.SourcesDirectory)/.dotnet/dotnet dotnet-coverage collect + --settings $(Build.SourcesDirectory)/eng/CodeCoverage.config + --output ${{ parameters.repoTestResultsPath }} + "${{ parameters.buildScript }} -test -configuration ${{ parameters.buildConfig }} /bl:${{ parameters.repoLogPath }}/tests.binlog $(_OfficialBuildIdArgs)" + displayName: Run tests + + - pwsh: | + Get-ChildItem ${{ parameters.repoTestResultsPath }} -Include "*_hangdump.dmp","Sequence_*.xml" -Recurse | ` + ForEach-Object { + $sourceFolder = $_.Directory.Name; + # The folder must be a GUID, see https://learn.microsoft.com/dotnet/core/tools/dotnet-test#options + $not_used = [System.Guid]::Empty; + if ([System.Guid]::TryParse($sourceFolder, [System.Management.Automation.PSReference]$not_used)) { + $destinationFolder = $(New-Item -Path ${{ parameters.repoLogPath }} -Name $sourceFolder -ItemType "Directory" -Force).FullName; + $destination = "$destinationFolder\$($_.Name)"; + Copy-Item -Path $_.FullName -Destination $destination -Force; + } + } + + displayName: Copy crash results to logs + condition: always() + continueOnError: true + + # Run code coverage reporting and validation on Windows + # a) we run a subset of tests on Linux + # b) we can only publish a single coverage report, + # see https://learn.microsoft.com/azure/devops/pipelines/tasks/reference/publish-code-coverage-results-v2#is-code-coverage-data-merged-when-multiple-files-are-provided-as-input-to-the-task-or-multiple-tasks-are-used-in-the-pipeline + - ${{ if and(eq(parameters.isWindows, 'true'), ne(parameters.skipCodeCoverage, 'true')) }}: + - template: \eng\pipelines\templates\TestCoverageReport.yml + parameters: + isDeltaBuild: ${{ parameters.isDeltaBuild }} + repoLogPath: ${{ parameters.repoLogPath }} + testResultsPath: ${{ parameters.repoTestResultsPath }} + testResultsFile: ${{ parameters.repoTestResultsPath }}/.cobertura.xml + + - ${{ if eq(parameters.isWindows, 'true') }}: + - script: ${{ parameters.buildScript }} + -pack + -sign $(_SignArgs) + -publish $(_PublishArgs) + -configuration ${{ parameters.buildConfig }} + -warnAsError 1 + /bl:${{ parameters.repoLogPath }}/pack.binlog + /p:Restore=false /p:Build=false + $(_OfficialBuildIdArgs) + displayName: Pack, Sign, and Publish diff --git a/eng/pipelines/templates/TestCoverageReport.yml b/eng/pipelines/templates/TestCoverageReport.yml new file mode 100644 index 0000000000..a47d5f2fc3 --- /dev/null +++ b/eng/pipelines/templates/TestCoverageReport.yml @@ -0,0 +1,75 @@ +parameters: + - name: isDeltaBuild + type: string + - name: repoLogPath + type: string + - name: testResultsPath + type: string + - name: testResultsFile + type: string + +steps: + - task: PowerShell@2 + displayName: Check coverage report exists + inputs: + targetType: 'inline' + script: | + Write-Host 'isDeltaBuild: ${{ parameters.isDeltaBuild }}' + Write-Host 'repoLogPath: ${{ parameters.repoLogPath }}' + Write-Host 'testResultsFile: ${{ parameters.testResultsFile }}' + + if (Test-Path '${{ parameters.testResultsFile }}') { + function Export { param($i); Write-Host "$i"; Write-Host "##$i" } + Export "vso[task.setvariable variable=PerformCoverageCheck]True" + } + else { + Write-Host "No coverage reports." + } + + - script: $(Build.SourcesDirectory)/.dotnet/dotnet reportgenerator + -reports:"${{ parameters.testResultsFile }}" + -targetdir:"${{ parameters.testResultsPath }}/CoverageResultsHtml" + -reporttypes:HtmlInline_AzurePipelines + displayName: Generate code coverage report + condition: and(succeeded(), eq(variables['PerformCoverageCheck'], 'True')) + + - task: PublishCodeCoverageResults@1 + displayName: Publish coverage report + condition: and(succeeded(), eq(variables['PerformCoverageCheck'], 'True')) + env: + DISABLE_COVERAGE_AUTOGENERATE: 'true' + inputs: + codeCoverageTool: cobertura + summaryFileLocation: '${{ parameters.testResultsFile }}' + pathToSources: $(Build.SourcesDirectory) + reportDirectory: '${{ parameters.testResultsPath }}/CoverageResultsHtml' + + - task: PublishBuildArtifacts@1 + displayName: Publish coverage results (cobertura.xml) + condition: and(always(), eq(variables['PerformCoverageCheck'], 'True')) + inputs: + PathtoPublish: '${{ parameters.testResultsFile }}' + PublishLocation: Container + ArtifactName: '$(Agent.Os)_$(Agent.JobName)' + continueOnError: true + + - task: PowerShell@2 + displayName: Check per-project coverage + condition: and(succeeded(), ne(variables['IsDeltaBuild'], 'True'), eq(variables['PerformCoverageCheck'], 'True')) + inputs: + targetType: 'filePath' + filePath: $(Build.SourcesDirectory)/scripts/ValidateProjectCoverage.ps1 + arguments: > + -CoberturaReportXml '${{ parameters.testResultsFile }}' + + - task: PowerShell@2 + displayName: ∆ Check per-project coverage + condition: and(succeeded(), eq(variables['IsDeltaBuild'], 'True'), eq(variables['PerformCoverageCheck'], 'True')) + inputs: + targetType: 'inline' + script: | + $DeltaBuildJsonFile = '${{ parameters.repoLogPath }}\DeltaBuild.json'; + $AffectedProjectChain = (Get-Content $DeltaBuildJsonFile | ConvertFrom-Json).AffectedProjectChain; + $(Build.SourcesDirectory)/scripts/ValidateProjectCoverage.ps1 ` + -CoberturaReportXml '${{ parameters.testResultsFile }}' ` + -OnlyForProjects $AffectedProjectChain diff --git a/eng/spellchecking_exclusions.dic b/eng/spellchecking_exclusions.dic new file mode 100644 index 0000000000000000000000000000000000000000..2f00ad64f92db410379819095977af7004cf3e3e GIT binary patch literal 26 gcmezWuZSU)p^zbmA(5d3$jW0#W~gM~W#D1}0CGMCO#lD@ literal 0 HcmV?d00001 diff --git a/eng/stryker-config.json b/eng/stryker-config.json new file mode 100644 index 0000000000..301f0394f6 --- /dev/null +++ b/eng/stryker-config.json @@ -0,0 +1,30 @@ +{ + "stryker-config": { + "reporters": [ + "json", + "html" + ], + "ignore-methods": [ + "*Exception.ctor", + "AddError", + "ConfigureAwait", + "Dispose", + "LogError", + "LogInformation", + "Throws.*", + "Throw.*", + "ValidateOptionsResult.Fail", + "ValidationResult.ctor" + ], + "ignore-mutations": [ + "block", + "statement" + ], + "additional-timeout": 7000, + "target-framework": "net7.0", + "thresholds": { + "high": 100, + "low": 100 + } + } +} \ No newline at end of file diff --git a/eng/xunit.runner.json b/eng/xunit.runner.json new file mode 100644 index 0000000000..6e96fab28c --- /dev/null +++ b/eng/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "diagnosticMessages": true, + "longRunningTestSeconds": 300 +} diff --git a/global.json b/global.json new file mode 100644 index 0000000000..e4b98569ed --- /dev/null +++ b/global.json @@ -0,0 +1,24 @@ +{ + "sdk": { + "version": "8.0.100-preview.5.23261.16" + }, + "tools": { + "dotnet": "8.0.100-preview.5.23261.16", + "runtimes": { + "dotnet/x64": [ + "3.1.32", + "6.0.13" + ], + "aspnetcore/x64": [ + "3.1.32", + "6.0.13" + ] + } + }, + "msbuild-sdks": { + "Microsoft.Build.NoTargets": "3.5.0", + "Microsoft.Build.Traversal": "3.2.0", + "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.23262.5", + "Microsoft.DotNet.Helix.Sdk": "8.0.0-beta.23262.5" + } +} diff --git a/restore.cmd b/restore.cmd new file mode 100644 index 0000000000..feb4457c37 --- /dev/null +++ b/restore.cmd @@ -0,0 +1,9 @@ +@echo off +SETLOCAL + +set _args=%* +if "%~1"=="-?" set _args=-help +if "%~1"=="/?" set _args=-help + +powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0eng\build.ps1""" -restore %_args%" +exit /b %ErrorLevel% \ No newline at end of file diff --git a/restore.sh b/restore.sh new file mode 100755 index 0000000000..403e3d75c2 --- /dev/null +++ b/restore.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +"$DIR/eng/build.sh" --restore "$@" diff --git a/scripts/Slngen.Tests.ps1 b/scripts/Slngen.Tests.ps1 new file mode 100644 index 0000000000..156ade6154 --- /dev/null +++ b/scripts/Slngen.Tests.ps1 @@ -0,0 +1,175 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '', Justification = 'False positive')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False positive')] +param() + +Describe "Slngen.ps1" { + BeforeAll { + function Invoke-SlngenExe ( + $Folders, + $OutSln, + $Globs, + $NoLaunch, + $Exclude, + $ConsoleOutput, + $MSBuild) {} + + function Compare-Array { + $($args[0] -join " ") -eq $($args[1] -join " ") + } + + $DefaultExcludePath = "--exclude src\Tools\MutationTesting\samples\ --exclude src\Templates\templates" + $DefaultSlnPath = '"' + (Join-Path -Path (Get-Location) -ChildPath "SDK.sln") + '"' + $PollyKeywordGlobs = 'test/TestUtilities/TestUtilities.csproj src/**/*Polly*/**/*.*sproj test/**/*Polly*/**/*.*sproj bench/**/*Polly*/**/*.*sproj int_test/**/*Polly*/**/*.*sproj docs/**/*Polly*/**/*.*sproj' + $PollyHttpKeywordsGlobs = 'test/TestUtilities/TestUtilities.csproj src/**/*Polly*/**/*.*sproj test/**/*Polly*/**/*.*sproj bench/**/*Polly*/**/*.*sproj int_test/**/*Polly*/**/*.*sproj docs/**/*Polly*/**/*.*sproj src/**/*Http*/**/*.*sproj test/**/*Http*/**/*.*sproj bench/**/*Http*/**/*.*sproj int_test/**/*Http*/**/*.*sproj docs/**/*Http*/**/*.*sproj' + } + + Context "Invoke-SlngenExe with test cases from examples" { + BeforeEach { + #Arrange + Mock Invoke-SlngenExe {} + } + It "Runs slngen with default params" { + #Act + . $PSScriptRoot/Slngen.ps1 + + #Assert + Should -Invoke -CommandName Invoke-SlngenExe -Times 1 -ParameterFilter { + $PesterBoundParameters.Folders -eq $false -and + $PesterBoundParameters.OutSln -eq $DefaultSlnPath -and + (Compare-Array $PesterBoundParameters.Globs 'test/TestUtilities/TestUtilities.csproj src/**/*.*sproj test/**/*.*sproj bench/**/*.*sproj int_test/**/*.*sproj') -and + $PesterBoundParameters.NoLaunch -eq $false -and + (Compare-Array $PesterBoundParameters.Exclude $DefaultExcludePath) -and + $PesterBoundParameters.ConsoleOutput -eq $null -and + $PesterBoundParameters.MSBuild -eq $null + } + } + + It "Runs slngen without integration tests" { + #Act + . $PSScriptRoot/Slngen.ps1 -IntegrationTests:$false + + #Assert + Should -Invoke -CommandName Invoke-SlngenExe -Times 1 -ParameterFilter { + $PesterBoundParameters.Folders -eq $false -and + $PesterBoundParameters.OutSln -eq $DefaultSlnPath -and + (Compare-Array $PesterBoundParameters.Globs 'test/TestUtilities/TestUtilities.csproj src/**/*.*sproj test/**/*.*sproj bench/**/*.*sproj') -and + $PesterBoundParameters.NoLaunch -eq $false -and + (Compare-Array $PesterBoundParameters.Exclude $DefaultExcludePath) -and + $PesterBoundParameters.ConsoleOutput -eq $null -and + $PesterBoundParameters.MSBuild -eq $null + } + } + + It "Runs slngen without benchmarks" { + #Act + . $PSScriptRoot/Slngen.ps1 -BenchmarkTests:$false + + #Assert + Should -Invoke -CommandName Invoke-SlngenExe -Times 1 -ParameterFilter { + $PesterBoundParameters.Folders -eq $false -and + $PesterBoundParameters.OutSln -eq $DefaultSlnPath -and + (Compare-Array $PesterBoundParameters.Globs 'test/TestUtilities/TestUtilities.csproj src/**/*.*sproj test/**/*.*sproj int_test/**/*.*sproj') -and + $PesterBoundParameters.NoLaunch -eq $false -and + (Compare-Array $PesterBoundParameters.Exclude $DefaultExcludePath) -and + $PesterBoundParameters.ConsoleOutput -eq $null -and + $PesterBoundParameters.MSBuild -eq $null + } + } + + It "Runs slngen with a keyword Polly" { + #Act + . $PSScriptRoot/Slngen.ps1 -Keywords "Polly" + + #Assert + Should -Invoke -CommandName Invoke-SlngenExe -Times 1 -ParameterFilter { + $PesterBoundParameters.Folders -eq $false -and + $PesterBoundParameters.OutSln -eq $DefaultSlnPath -and + (Compare-Array $PesterBoundParameters.Globs $PollyKeywordGlobs) -and + $PesterBoundParameters.NoLaunch -eq $false -and + (Compare-Array $PesterBoundParameters.Exclude $DefaultExcludePath) -and + $PesterBoundParameters.ConsoleOutput -eq $null -and + $PesterBoundParameters.MSBuild -eq $null + } + } + + It "Runs slngen with keywords Polly, Http" { + #Act + . $PSScriptRoot/Slngen.ps1 -Keywords "Polly", "Http" + + #Assert + Should -Invoke -CommandName Invoke-SlngenExe -Times 1 -ParameterFilter { + $PesterBoundParameters.Folders -eq $false -and + $PesterBoundParameters.OutSln -eq $DefaultSlnPath -and + (Compare-Array $PesterBoundParameters.Globs $PollyHttpKeywordsGlobs) -and + $PesterBoundParameters.NoLaunch -eq $false -and + (Compare-Array $PesterBoundParameters.Exclude $DefaultExcludePath) -and + $PesterBoundParameters.ConsoleOutput -eq $null -and + $PesterBoundParameters.MSBuild -eq $null + } + } + + It "Runs slngen with keywords Polly, Http and Folders switch on" { + #Act + . $PSScriptRoot/Slngen.ps1 -Keywords "Polly", "Http" -Folders + + #Assert + Should -Invoke -CommandName Invoke-SlngenExe -Times 1 -ParameterFilter { + $PesterBoundParameters.Folders -eq $true -and + $PesterBoundParameters.OutSln -eq $DefaultSlnPath -and + (Compare-Array $PesterBoundParameters.Globs $PollyHttpKeywordsGlobs) -and + $PesterBoundParameters.NoLaunch -eq $false -and + (Compare-Array $PesterBoundParameters.Exclude $DefaultExcludePath) -and + $PesterBoundParameters.ConsoleOutput -eq $null -and + $PesterBoundParameters.MSBuild -eq $null + } + } + + It "Runs slngen with keywords Polly, Http and NoLaunch switch on" { + #Act + . $PSScriptRoot/Slngen.ps1 -Keywords "Polly", "Http" -NoLaunch + + #Assert + Should -Invoke -CommandName Invoke-SlngenExe -Times 1 -ParameterFilter { + $PesterBoundParameters.Folders -eq $false -and + $PesterBoundParameters.OutSln -eq $DefaultSlnPath -and + (Compare-Array $PesterBoundParameters.Globs $PollyHttpKeywordsGlobs) -and + $PesterBoundParameters.NoLaunch -eq $true -and + (Compare-Array $PesterBoundParameters.Exclude $DefaultExcludePath) -and + $PesterBoundParameters.ConsoleOutput -eq $null -and + $PesterBoundParameters.MSBuild -eq $null + } + } + + It "Runs slngen with keywords Polly, Http and custom sln name" { + #Act + . $PSScriptRoot/Slngen.ps1 -Keywords "Polly", "Http" -OutSln 'test.sln' + + #Assert + Should -Invoke -CommandName Invoke-SlngenExe -Times 1 -ParameterFilter { + $PesterBoundParameters.Folders -eq $false -and + $PesterBoundParameters.OutSln -eq ('"' + (Join-Path -Path (Get-Location) -ChildPath "test.sln") + '"') -and + (Compare-Array $PesterBoundParameters.Globs $PollyHttpKeywordsGlobs) -and + $PesterBoundParameters.NoLaunch -eq $false -and + (Compare-Array $PesterBoundParameters.Exclude $DefaultExcludePath) -and + $PesterBoundParameters.ConsoleOutput -eq $null -and + $PesterBoundParameters.MSBuild -eq $null + } + } + + It "Runs slngen with exclude paths" { + #Act + . $PSScriptRoot/Slngen.ps1 -All -ExcludePaths "testpath\testpath" + + #Assert + Should -Invoke -CommandName Invoke-SlngenExe -Times 1 -ParameterFilter { + $PesterBoundParameters.Folders -eq $false -and + $PesterBoundParameters.OutSln -eq $DefaultSlnPath -and + (Compare-Array $PesterBoundParameters.Globs 'test/TestUtilities/TestUtilities.csproj src/**/*.*sproj test/**/*.*sproj bench/**/*.*sproj int_test/**/*.*sproj') -and + $PesterBoundParameters.NoLaunch -eq $false -and + (Compare-Array $PesterBoundParameters.Exclude "--exclude testpath\testpath") -and + $PesterBoundParameters.ConsoleOutput -eq $null -and + $PesterBoundParameters.MSBuild -eq $null + } + } + } +} \ No newline at end of file diff --git a/scripts/Slngen.ps1 b/scripts/Slngen.ps1 new file mode 100644 index 0000000000..3692b76b10 --- /dev/null +++ b/scripts/Slngen.ps1 @@ -0,0 +1,224 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + The script to generate a custom solution file for a given keyword(s). + +.DESCRIPTION + The script is a wrapper over slngen tool (see https://github.com/microsoft/slngen) to make it easier to build a solution file for a given keyword(s). + +.PARAMETER Keywords + Keywords to search for. +.PARAMETER All + Include all the projects (except docs). If no Keywords provided, $All is set to true automatically. +.PARAMETER OnlySources + Include only the source projects. +.PARAMETER IntegrationTests + Include integration test projects. +.PARAMETER BenchmarkTests + Include benchmark test projects. +.PARAMETER Docs + Include conceptual doc projects. +.PARAMETER Folders + Enables use of folders. +.PARAMETER ExcludePaths + Exclude paths from search for project files. +.PARAMETER Quiet + Minimizes console output. +.PARAMETER MsBuildParams + Parameters passed to MSBuild with slngen. +.PARAMETER RepositoryPath + Path to the repository +.PARAMETER Help + Determines whether to show help. + +.EXAMPLE + PS> .\Slngen.ps1 "Polly" +.EXAMPLE + PS> .\Slngen.ps1 "Polly","Http" +.EXAMPLE + PS> .\Slngen.ps1 -Folders "Polly","Http" +.EXAMPLE + PS> .\Slngen.ps1 +.EXAMPLE + PS> .\Slngen.ps1 -NoLaunch +.EXAMPLE + PS> .\Slngen.ps1 -All -OutSln "myDirectory/MySolution.sln" +.EXAMPLE + PS> .\Slngen.ps1 -All -ExcludePaths "src\Templates\templates" -OutSln "myDirectory/MySolution.sln" +#> +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidDefaultValueSwitchParameter', '', Justification="We need it to be turned on by default while still providing a capability to turn it off.")] +param ( + [Parameter(Mandatory = $true, HelpMessage="Keywords to search for.", Position = 0, ParameterSetName = "By Keywords")] + [string[]]$Keywords, + [Parameter(Mandatory = $false, HelpMessage="Include all projects (except docs).", ParameterSetName = "All")] + [switch]$All = $false, + [Parameter(Mandatory = $false, HelpMessage="Include only source projects.")] + [switch]$OnlySources = $false, + [Parameter(Mandatory = $false, HelpMessage="Include integration test projects. Not Compatible with OnlySources parameter")] + [switch]$IntegrationTests = $true, + [Parameter(Mandatory = $false, HelpMessage="Include benchmark test projects. Not Compatible with OnlySources parameter")] + [switch]$BenchmarkTests = $true, + [Parameter(Mandatory = $false, HelpMessage="Include documentation projects.")] + [switch]$Docs = $false, + [Parameter(Mandatory = $false, HelpMessage="Enables use of folders.")] + [switch]$Folders = $false, + [Parameter(Mandatory = $false, HelpMessage="Path to exclude from search for project files. Must be repo root folder based.")] + [string[]]$ExcludePaths = @('src\Tools\MutationTesting\samples\', 'src\Templates\templates'), + [Parameter(Mandatory = $false, HelpMessage="Don't launch Visual Studio.")] + [switch]$NoLaunch = $false, + [Parameter(Mandatory = $false, HelpMessage="Minimizes console output.")] + [switch]$Quiet = $false, + [Parameter(Mandatory = $false, HelpMessage="Output file name.")] + [string]$OutSln = "SDK.sln", + [Parameter(Mandatory = $false, HelpMessage="Parameters passed to MSBuild with slngen.")] + [string[]]$MsBuildParams, + [Parameter(Mandatory = $false, HelpMessage="Path to the repository.")] + [string]$RepositoryPath, + + [Parameter(HelpMessage="Show help.")] + [switch][Alias('h')] $Help +) + +# Show help +if ($Help -or ($PSBoundParameters.Count -lt 1)) { + Get-Help $PSCommandPath + exit 0; +} + +<# +.DESCRIPTION + This is just to isolate slngen.exe launch command and made it possible to mock and unit test the script. +#> +function Invoke-SlngenExe +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + $Folders, + [Parameter(Mandatory = $true)] + $OutSln, + [Parameter(Mandatory = $true)] + $Globs, + [Parameter(Mandatory = $true)] + $NoLaunch, + [Parameter(Mandatory = $true)] + [AllowNull()] + $Exclude, + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [AllowNull()] + $ConsoleOutput, + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [AllowNull()] + $MSBuild + ) + + dotnet tool restore --verbosity minimal + $process = Start-Process ` + -FilePath 'dotnet' ` + -ArgumentList @("slngen", "--folders $Folders", "--collapsefolders $Folders", "--ignoreMainProject", "--nologo", "-o $OutSln", "$Globs", "--launch $(!$NoLaunch) $Exclude $ConsoleOutput $MSBuild") ` + -Wait ` + -PassThru ` + -NoNewWindow; + + if ($process.ExitCode -ne 0) { + throw "Failed to generate the solution." + } +} + +if (!$Keywords) { + $All = $true +} +else { + $Docs = $true +} + +$InformationPreference = "Continue" + +if ([System.IO.Path]::IsPathRooted($OutSln)) { + $OutSlnPath = $OutSln +} +else { + $OutSlnPath = Join-Path -Path (Get-Location) -ChildPath $OutSln +} + +if (!$RepositoryPath) { + $RepositoryPath = Split-Path -Parent $PSScriptRoot +} + +#This is the list of paths to search for projects when $OnlySources set to $false: +$NonSourcePaths = @("test") +if($BenchmarkTests) +{ + $NonSourcePaths = $NonSourcePaths + "bench" +} +if($IntegrationTests) +{ + $NonSourcePaths = $NonSourcePaths + "int_test" +} + +if ($Docs -and (Test-Path ./docs)) +{ + $NonSourcePaths = $NonSourcePaths + "docs" +} + +Push-Location $RepositoryPath + +try { + [System.Collections.ArrayList]$Globs = @() + + if (!$OnlySources) { + $Globs += "test/TestUtilities/TestUtilities.csproj" + } + + if (!$All) { + foreach ($Keyword in $Keywords) { + $Globs += "src/**/*$($Keyword)*/**/*.*sproj" + if (!$OnlySources) { + foreach ($NonSourcePath in $NonSourcePaths) { + $Globs += $NonSourcePath + "/**/*$($Keyword)*/**/*.*sproj" + } + } + } + } + else { + $Globs += "src/**/*.*sproj" + + if (!$OnlySources) { + foreach ($NonSourcePath in $NonSourcePaths) { + $Globs += $NonSourcePath + "/**/*.*sproj" + } + } + } + + $ConsoleOutput = $null + if ($Quiet) { + $ConsoleOutput = "--verbosity quiet" + } + + if ($MsBuildParams) { + $Joined = $MsBuildParams -join ';' + $MSBuild = "--property $Joined" + } else { + $MSBuild = $null + } + + if ($ExcludePaths) { + #transform arrays from @("path1","path2") into @("--exclude {repository path}\path1", "--exclude {repository path}\path2") + $Exclude = $ExcludePaths | ForEach-Object { "--exclude $_" } + } else { + $Exclude = $null + } + + # Install required toolset + . $PSScriptRoot/../eng/common/tools.ps1 + $dotnetRoot = InitializeDotNetCli -install:$true + Write-Verbose ".NET root: $dotnetRoot" + + Invoke-SlngenExe -Folders $Folders -OutSln "`"$OutSlnPath`"" -Globs $Globs -NoLaunch $NoLaunch -Exclude $Exclude -ConsoleOutput $ConsoleOutput -MSBuild $MSBuild +} +finally { + Pop-Location -ErrorAction SilentlyContinue +} diff --git a/scripts/SlngenReferencing.ps1 b/scripts/SlngenReferencing.ps1 new file mode 100644 index 0000000000..2f2296a44d --- /dev/null +++ b/scripts/SlngenReferencing.ps1 @@ -0,0 +1,59 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS +Create solution file that contains all packages referencing mentioned keywords. +.DESCRIPTION +This script will help you when changing public API of the package and would like to adjust all internal R9 usages. +It will generate a solution file with all packages that references a project matching given keywords. +.EXAMPLE + PS> .\SlngenReferencing.ps1 AsyncState +#> + +param( + [Parameter(Mandatory = $true, HelpMessage="Keywords to search for references")] + [string[]]$Keywords +) + +function Get-ReferencingPackages([string]$repoRoot, [string]$directory, [System.Collections.Generic.HashSet[string]]$projectNames, [string]$lookingFor) { + Push-Location $repoRoot + + try { + Get-ChildItem -Path $directory -Include "*.csproj" -Exclude "Project.Title.csproj" -Recurse | ForEach-Object { + $projectName = $_.Directory.BaseName; + + (Select-Xml -Path $_.FullName -XPath "/Project/ItemGroup/ProjectReference/@Include").Node.Value | Foreach-Object { + + if ($_ -like "*$lookingFor*") { + Write-Output "Found reference to $lookingFor in $projectName." -ForegroundColor Green + $projectNames.Add($projectName) + return + } + } + } | Out-Null + } + finally { + Pop-Location + } +} + +$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\') +$SlnGenScriptPath = Resolve-Path (Join-Path $RepoRoot '.\scripts\SlnGen.ps1') + +[System.Collections.Generic.HashSet[string]]$ProjectNames = @(); + +$Keywords | ForEach-Object { + Get-ReferencingPackages $RepoRoot "src/Extensions" $ProjectNames $_ + Get-ReferencingPackages $RepoRoot "src/Service" $ProjectNames $_ +} + +$NamesString = $ProjectNames -join "," +$NamesStringSpace = $ProjectNames -join ", " +if ($NamesString -eq "") { + + $NamesString = $Keywords -join ","; + Write-Host "No references found for: ""$NamesString"". Going to build solution file just for those keywords..." -ForegroundColor Yellow +} + +Write-Host "Caching is referenced in the following projects [ $NamesStringSpace ]" +powershell $SlnGenScriptPath -Keywords $NamesString -NoLaunch -OutSln "$RepoRoot\SDK-Referencing.sln" \ No newline at end of file diff --git a/scripts/ValidateProjectCoverage.ps1 b/scripts/ValidateProjectCoverage.ps1 new file mode 100644 index 0000000000..feb2f3d751 --- /dev/null +++ b/scripts/ValidateProjectCoverage.ps1 @@ -0,0 +1,143 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Validates the code coverage policy for each project. +.DESCRIPTION + This script compares code coverage with thresholds given in "MinCodeCoverage" property in each project. + The script writes an error for each project that does not comply with the policy. +.PARAMETER CoberturaReportXml + Path to the XML file to read the code coverage report from in Cobertura format +.PARAMETER OnlyForProjects + Optional set of projects to check coverage for only. +.EXAMPLE + PS> .\ValidatePerProjectCoverage.ps1 -CoberturaReportXml .\Cobertura.xml -OnlyForProjects '.\src\Project1\Project1.csproj','.\src\Project2\Project2.csproj' +#> + +param ( + [Parameter(Mandatory = $true, HelpMessage="Path to the XML file to read the code coverage report from")] + [string]$CoberturaReportXml, + [Parameter(Mandatory = $false, HelpMessage="Optional set of projects to check coverage for only.")] + [string[]]$OnlyForProjects +) + +function Write-Header { param($m); Write-Output $NL$m, ("=" * 80), $NL } +function Get-XmlValue { param($X, $Y); return $X.SelectSingleNode($Y).'#text' } + +Write-Verbose "Reading cobertura report..." +[xml]$CoberturaReport = Get-Content $CoberturaReportXml + +$ProjectFileList = New-Object System.Collections.ArrayList + +if ($OnlyForProjects -and $OnlyForProjects.Count -gt 0) { + $ProjectFileList.AddRange($OnlyForProjects) +} +else { + $ProjectFileList.AddRange((Get-ChildItem -Path src ` + -Include '*.*sproj' ` + -Exclude ('Project.Title.csproj', '*ProjectTemplate.csproj', 'Templates.csproj') ` + -Recurse)) +} + +$ProjectToMinCoverageMap = @{} + +$ProjectFileList | ForEach-Object { + $XmlDoc = [xml](Get-Content $_) + $AssemblyName = Get-XmlValue $XmlDoc "//Project/PropertyGroup/AssemblyName" + $MinCodeCoverage = Get-XmlValue $XmlDoc "//Project/PropertyGroup/MinCodeCoverage" + $TempMinCodeCoverage = Get-XmlValue $XmlDoc "//Project/PropertyGroup/TempMinCodeCoverage" + + if ([string]::IsNullOrWhiteSpace($AssemblyName)) { + # Assembly name is empty for template projects and for packages projects. + return + } + + if ([string]::IsNullOrWhiteSpace($MinCodeCoverage)) { + # Test projects may not legitimely have min code coverage set. + return + } + + # Some projects currently fail code coverage checks. Allow to temporarily override the requirements + # See https://github.com/dotnet/r9/issues/75 + if (![string]::IsNullOrWhiteSpace($TempMinCodeCoverage)) { + $MinCodeCoverage = $TempMinCodeCoverage + } + + $ProjectToMinCoverageMap[$AssemblyName] = $MinCodeCoverage +} + +$Errors = New-Object System.Collections.ArrayList + +if ($null -eq $CoberturaReport.coverage -or $null -eq $CoberturaReport.coverage.packages) +{ + return +} + +if ($null -eq $CoberturaReport.coverage.packages.package -or 0 -eq $CoberturaReport.coverage.packages.package.count) +{ + return +} + +Write-Verbose "Collecting projects from code coverage report..." +$CoberturaReport.coverage.packages.package | ForEach-Object { + $Name = $_.name + $LineCoverage = [math]::Round([double]$_.'line-rate' * 100, 2) + $BranchCoverage = [math]::Round([double]$_.'branch-rate' * 100, 2) + $IsFailed = $false + + Write-Verbose "Project $Name with line coverage $LineCoverage and branch coverage $BranchCoverage" + + if ($ProjectToMinCoverageMap.ContainsKey($Name)) + { + if ($ProjectToMinCoverageMap[$Name] -eq 'n/a') + { + Write-Output "$Name ...code coverage is not applicable" + return + } + + [double]$MinCodeCoverage = $ProjectToMinCoverageMap[$Name] + + if ($MinCodeCoverage -gt $LineCoverage) + { + $IsFailed = $true + [void]$Errors.Add( + ( + New-Object PSObject -Property @{ + "Project"=$Name;"Coverage Type"="Line"; + "Actual Coverage"=$LineCoverage;"Expected Coverage"=$MinCodeCoverage + } + ) + ) + } + + if ($MinCodeCoverage -gt $BranchCoverage) + { + $IsFailed = $true + [void]$Errors.Add( + ( + New-Object PSObject -Property @{ + "Project"=$Name;"Coverage Type"="Branch"; + "Actual Coverage"=$BranchCoverage;"Expected Coverage"=$MinCodeCoverage + } + ) + ) + } + + if ($IsFailed) { Write-Output "$Name ...failed validation" } + else { Write-Output "$Name ...ok" } + } + else { + Write-Output "$Name ...skipping" + } +} + +if ($Errors.Count -ge 1) +{ + Write-Output "" + Write-Header "[!!] Found $($Errors.Count) issues!" + $Errors | Sort-Object Project, 'Coverage Type' | Format-Table "Project", "Coverage Type", "Actual Coverage", "Expected Coverage" -AutoSize -wrap + + throw "Validation failed (see the report above)!" +} + +Write-Output "" +Write-Output "All good, no issues found." \ No newline at end of file diff --git a/src/.editorconfig b/src/.editorconfig new file mode 100644 index 0000000000..4fcddb8a01 --- /dev/null +++ b/src/.editorconfig @@ -0,0 +1,7222 @@ +# Created by the R9 diagnostic config generator +# Generated : 2023-02-20 04:52:12Z +# Attributes: api, general, performance, production, r9internal +# Analyzers : ILLink.RoslynAnalyzer, Internal.Analyzers, Microsoft.AspNetCore.App.Analyzers, Microsoft.AspNetCore.Components.Analyzers, Microsoft.CodeAnalysis.CodeStyle, Microsoft.CodeAnalysis.CSharp.CodeStyle, Microsoft.CodeAnalysis.CSharp.NetAnalyzers, Microsoft.CodeAnalysis.NetAnalyzers, Microsoft.CPR.Standard.Analyzers.Basic.R3, Microsoft.R9.Analyzers.Roslyn4.0, Microsoft.VisualStudio.Threading.Analyzers, Microsoft.VisualStudio.Threading.Analyzers.CSharp, SonarAnalyzer.CSharp, StyleCop.Analyzers + +[*.cs] + +# Title : Do not use model binding attributes with route handlers +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0003.severity = warning + +# Title : Do not use action results with route handlers +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0004.severity = warning + +# Title : Do not place attribute on method called by route handler lambda +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0005.severity = warning + +# Title : Do not use non-literal sequence numbers +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0006.severity = warning + +# Title : Route parameter and argument optionality is mismatched +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0007.severity = warning + +# Title : Do not use ConfigureWebHost with WebApplicationBuilder.Host +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0008.severity = error + +# Title : Do not use Configure with WebApplicationBuilder.WebHost +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0009.severity = error + +# Title : Do not use UseStartup with WebApplicationBuilder.WebHost +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0010.severity = error + +# Title : Suggest using builder.Logging over Host.ConfigureLogging or WebHost.ConfigureLogging +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0011.severity = warning + +# Title : Suggest using builder.Services over Host.ConfigureServices or WebHost.ConfigureServices +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0012.severity = warning + +# Title : Suggest switching from using Configure methods to WebApplicationBuilder.Configuration +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0013.severity = warning + +# Title : Suggest using top level route registrations +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0014.severity = warning + +# Title : Component parameter should have public setters. +# Category : Encapsulation +dotnet_diagnostic.BL0001.severity = error + +# Title : Component has multiple CaptureUnmatchedValues parameters +# Category : Usage +dotnet_diagnostic.BL0002.severity = warning + +# Title : Component parameter with CaptureUnmatchedValues has the wrong type +# Category : Usage +dotnet_diagnostic.BL0003.severity = warning + +# Title : Component parameter should be public. +# Category : Encapsulation +dotnet_diagnostic.BL0004.severity = error + +# Title : Component parameter should not be set outside of its component. +# Category : Usage +dotnet_diagnostic.BL0005.severity = warning + +# Title : Do not use RenderTree types +# Category : Usage +dotnet_diagnostic.BL0006.severity = warning + +# Title : Component parameters should be auto properties +# Category : Usage +dotnet_diagnostic.BL0007.severity = warning + +# Title : Do not declare static members on generic types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1000 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1000.severity = warning +dotnet_code_quality.CA1000.api_surface = public + +# Title : Types that own disposable fields should be disposable +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1001 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1001.severity = warning + +# Title : Do not expose generic lists +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1002 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1002.severity = warning +dotnet_code_quality.CA1002.api_surface = public + +# Title : Use generic event handler instances +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1003 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1003.severity = warning +dotnet_code_quality.CA1003.api_surface = all + +# Title : Avoid excessive parameters on generic types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1005 +# Tags : PortedFromFxCop, Telemetry +dotnet_diagnostic.CA1005.severity = warning +dotnet_code_quality.CA1005.api_surface = public + +# Title : Enums should have zero value +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1008 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode, RuleNoZero +dotnet_diagnostic.CA1008.severity = warning +dotnet_code_quality.CA1008.api_surface = public + +# Title : Generic interface should also be implemented +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1010 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1010.severity = warning +dotnet_code_quality.CA1010.api_surface = public +dotnet_code_quality.CA1010.additional_required_generic_interfaces = T:System.Collections.IDictionary->T:System.Collections.Generic.IDictionary`2 + +# Title : Abstract types should not have public constructors +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1012 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1012.severity = warning +dotnet_code_quality.CA1012.api_surface = all + +# Title : Mark assemblies with CLSCompliant +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1014 +# Tags : PortedFromFxCop, Telemetry, CompilationEnd +dotnet_diagnostic.CA1014.severity = none + +# Title : Mark assemblies with assembly version +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1016 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA1016.severity = warning + +# Title : Mark assemblies with ComVisible +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1017 +# Tags : PortedFromFxCop, Telemetry, CompilationEnd +dotnet_diagnostic.CA1017.severity = none + +# Title : Mark attributes with AttributeUsageAttribute +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1018 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1018.severity = warning + +# Title : Define accessors for attribute arguments +# Category : Design +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1019 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1019.severity = warning + +# Title : Define accessors for attribute arguments +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1019 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1019.severity = warning + +# Title : Avoid out parameters +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1021 +# Tags : PortedFromFxCop, Telemetry +dotnet_diagnostic.CA1021.severity = none + +# Title : Use properties where appropriate +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1024 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1024.severity = warning +dotnet_code_quality.CA1024.api_surface = public + +# Title : Mark enums with FlagsAttribute +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1027 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1027.severity = warning +dotnet_code_quality.CA1027.api_surface = all + +# Title : Enum Storage should be Int32 +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1028 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1028.severity = none + +# Title : Use events where appropriate +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1030 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1030.severity = warning + +# Title : Do not catch general exception types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1031 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1031.severity = warning + +# Title : Implement standard exception constructors +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1032 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1032.severity = warning + +# Title : Interface methods should be callable by child types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1033.severity = warning + +# Title : Nested types should not be visible +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1034 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1034.severity = warning + +# Title : Override methods on comparable types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1036 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1036.severity = warning + +# Title : Avoid empty interfaces +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1040 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +# Comment : Reasonably frequent in modern .NET programming +dotnet_diagnostic.CA1040.severity = none + +# Title : Provide ObsoleteAttribute message +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1041 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1041.severity = warning +dotnet_code_quality.CA1041.api_surface = all + +# Title : Use Integral Or String Argument For Indexers +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1043 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1043.severity = warning +dotnet_code_quality.CA1043.api_surface = public, protected + +# Title : Properties should not be write only +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1044 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1044.severity = warning + +# Title : Do not pass types by reference +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1045 +# Tags : PortedFromFxCop, Telemetry +dotnet_diagnostic.CA1045.severity = suggestion +dotnet_code_quality.CA1045.api_surface = public + +# Title : Do not overload equality operator on reference types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1046 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1046.severity = warning +dotnet_code_quality.CA1046.api_surface = all + +# Title : Do not declare protected member in sealed type +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1047 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1047.severity = warning +dotnet_code_quality.CA1047.api_surface = all + +# Title : Declare types in namespaces +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1050 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1050.severity = warning +dotnet_code_quality.CA1050.api_surface = public + +# Title : Do not declare visible instance fields +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1051.severity = warning +dotnet_code_quality.CA1051.api_surface = public + +# Title : Static holder types should be Static or NotInheritable +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1052 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1052.severity = warning + +# Title : URI-like parameters should not be strings +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1054 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1054.severity = warning + +# Title : URI-like return values should not be strings +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1055 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1055.severity = warning + +# Title : URI-like properties should not be strings +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1056 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1056.severity = warning + +# Title : Types should not extend certain base types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1058 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1058.severity = warning +dotnet_code_quality.CA1058.api_surface = public + +# Title : Move pinvokes to native methods class +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1060 +# Tags : PortedFromFxCop, Telemetry +dotnet_diagnostic.CA1060.severity = warning + +# Title : Do not hide base class methods +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1061 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1061.severity = warning +dotnet_code_quality.CA1061.api_surface = all + +# Title : Validate arguments of public methods +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1062 +# Tags : PortedFromFxCop, Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1062.severity = warning +dotnet_code_quality.CA1062.api_surface = public, protected +dotnet_code_quality.CA1062.exclude_extension_method_this_parameter = false +dotnet_code_quality.CA1062.null_check_validation_methods = IfNull|IfNullOrEmpty|IfNullOrWhitespace|IfNullOrMemberNull + +# Title : Implement IDisposable Correctly +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1063 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1063.severity = warning + +# Title : Exceptions should be public +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1064 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1064.severity = warning + +# Title : Do not raise exceptions in unexpected locations +# Category : Design +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1065 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1065.severity = warning + +# Title : Do not raise exceptions in unexpected locations +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1065 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1065.severity = warning + +# Title : Implement IEquatable when overriding Object.Equals +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1066 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1066.severity = warning + +# Title : Override Object.Equals(object) when implementing IEquatable +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1067 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1067.severity = warning + +# Title : CancellationToken parameters must come last +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1068 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1068.severity = warning +dotnet_code_quality.CA1068.api_surface = public, protected + +# Title : Enums values should not be duplicated +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1069 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1069.severity = warning + +# Title : Do not declare event fields as virtual +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1070 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1070.severity = warning +dotnet_code_quality.CA1070.api_surface = all + +# Title : Avoid using cref tags with a prefix +# Category : Documentation +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1200 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1200.severity = warning + +# Title : Do not pass literals as localized parameters +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1303 +# Tags : PortedFromFxCop, Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1303.severity = warning + +# Title : Specify CultureInfo +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1304 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1304.severity = warning + +# Title : Specify IFormatProvider +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1305.severity = warning + +# Title : Specify StringComparison for clarity +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1307 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1307.severity = warning + +# Title : Normalize strings to uppercase +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1308 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1308.severity = warning + +# Title : Use ordinal string comparison +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1309 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1309.severity = warning + +# Title : Specify StringComparison for correctness +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1310 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1310.severity = warning + +# Title : Specify a culture or use an invariant version +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1311 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1311.severity = warning + +# Title : P/Invokes should not be visible +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1401 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1401.severity = warning + +# Title : Validate platform compatibility +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1416.severity = warning + +# Title : Do not use 'OutAttribute' on string parameters for P/Invokes +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1417 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1417.severity = warning + +# Title : Use valid platform string +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1418 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1418.severity = warning + +# Title : Provide a parameterless constructor that is as visible as the containing type for concrete types derived from 'System.Runtime.InteropServices.SafeHandle' +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1419 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1419.severity = warning + +# Title : Property, type, or attribute requires runtime marshalling +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1420 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1420.severity = warning + +# Title : This method uses runtime marshalling even when the 'DisableRuntimeMarshallingAttribute' is applied +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1421 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1421.severity = suggestion + +# Title : Validate platform compatibility +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1422 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1422.severity = warning + +# Title : Avoid excessive inheritance +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1501 +# Tags : PortedFromFxCop, Telemetry, CompilationEnd +dotnet_diagnostic.CA1501.severity = warning +dotnet_code_quality.CA1501.api_surface = public + +# Title : Avoid excessive complexity +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1502 +# Tags : PortedFromFxCop, Telemetry, CompilationEnd +# Comment : Code gets complicated +dotnet_diagnostic.CA1502.severity = none + +# Title : Avoid unmaintainable code +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1505 +# Tags : PortedFromFxCop, Telemetry, CompilationEnd +dotnet_diagnostic.CA1505.severity = warning + +# Title : Avoid excessive class coupling +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1506 +# Tags : PortedFromFxCop, Telemetry, CompilationEnd +# Comment : Code gets complicated +dotnet_diagnostic.CA1506.severity = none + +# Title : Use nameof to express symbol names +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1507 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1507.severity = warning + +# Title : Avoid dead conditional code +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1508 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1508.severity = warning + +# Title : Invalid entry in code metrics rule specification file +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1509 +# Tags : Telemetry, CompilationEnd +dotnet_diagnostic.CA1509.severity = warning + +# Title : Do not name enum values 'Reserved' +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1700 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1700.severity = warning +dotnet_code_quality.CA1700.api_surface = public + +# Title : Identifiers should not contain underscores +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1707 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +# Comment : StyleCop handles this +dotnet_diagnostic.CA1707.severity = none + +# Title : Identifiers should differ by more than case +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1708 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1708.severity = warning +dotnet_code_quality.CA1708.api_surface = public + +# Title : Identifiers should have correct suffix +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1710 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1710.severity = warning +dotnet_code_quality.CA1710.api_surface = public + +# Title : Identifiers should not have incorrect suffix +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1711 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1711.severity = warning +dotnet_code_quality.CA1711.api_surface = public + +# Title : Do not prefix enum values with type name +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1712 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1712.severity = warning + +# Title : Events should not have 'Before' or 'After' prefix +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1713 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1713.severity = warning + +# Title : Identifiers should have correct prefix +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1715 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1715.severity = none + +# Title : Identifiers should not match keywords +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1716 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1716.severity = warning +dotnet_code_quality.CA1716.api_surface = all +dotnet_code_quality.CA1716.analyzed_symbol_kinds = all + +# Title : Identifier contains type name +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1720 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1720.severity = warning +dotnet_code_quality.CA1720.api_surface = public + +# Title : Property names should not match get methods +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1721 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1721.severity = warning +dotnet_code_quality.CA1721.api_surface = public + +# Title : Type names should not match namespaces +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1724 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA1724.severity = warning +dotnet_code_quality.CA1724.api_surface = public + +# Title : Parameter names should match base declaration +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1725 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1725.severity = warning + +# Title : Use PascalCase for named placeholders +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1727 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1727.severity = silent + +# Title : Review unused parameters +# Category : Usage +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1801 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1801.severity = none + +# Title : Review unused parameters +# Category : Usage +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1801 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1801.severity = none + +# Title : Use literals where appropriate +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1802 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1802.severity = warning + +# Title : Use literals where appropriate +# Category : Performance +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1802 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1802.severity = warning + +# Title : Do not initialize unnecessarily +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1805 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1805.severity = warning + +# Title : Do not initialize unnecessarily +# Category : Performance +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1805 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1805.severity = warning + +# Title : Do not ignore method results +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1806 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1806.severity = warning + +# Title : Initialize reference type static fields inline +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1810 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1810.severity = warning + +# Title : Avoid uninstantiated internal classes +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1812 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +# Comment : S1144 finds more cases and has no false positives +dotnet_diagnostic.CA1812.severity = warning + +# Title : Avoid unsealed attributes +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1813 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1813.severity = warning + +# Title : Prefer jagged arrays over multidimensional +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1814 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1814.severity = warning + +# Title : Override equals and operator equals on value types +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1815 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1815.severity = warning + +# Title : Dispose methods should call SuppressFinalize +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1816 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1816.severity = warning + +# Title : Properties should not return arrays +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1819 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1819.severity = warning + +# Title : Test for empty strings using string length +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1820 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1820.severity = warning + +# Title : Remove empty Finalizers +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1821 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1821.severity = warning + +# Title : Mark members as static +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1822.severity = warning + +# Title : Avoid unused private fields +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1823 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1823.severity = warning + +# Title : Mark assemblies with NeutralResourcesLanguageAttribute +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1824 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA1824.severity = warning + +# Title : Avoid zero-length array allocations +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1825 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1825.severity = warning + +# Title : Do not use Enumerable methods on indexable collections +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1826 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1826.severity = warning + +# Title : Do not use Count() or LongCount() when Any() can be used +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1827 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1827.severity = warning + +# Title : Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1828 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1828.severity = warning + +# Title : Use Length/Count property instead of Count() when available +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1829 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1829.severity = warning + +# Title : Prefer strongly-typed Append and Insert method overloads on StringBuilder +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1830 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1830.severity = warning + +# Title : Use AsSpan or AsMemory instead of Range-based indexers when appropriate +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1831 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1831.severity = warning + +# Title : Use AsSpan or AsMemory instead of Range-based indexers when appropriate +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1832 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1832.severity = warning + +# Title : Use AsSpan or AsMemory instead of Range-based indexers when appropriate +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1833 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1833.severity = warning + +# Title : Consider using 'StringBuilder.Append(char)' when applicable +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1834 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1834.severity = warning + +# Title : Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1835 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1835.severity = warning + +# Title : Prefer IsEmpty over Count +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1836 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1836.severity = warning + +# Title : Use 'Environment.ProcessId' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1837 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1837.severity = warning + +# Title : Avoid 'StringBuilder' parameters for P/Invokes +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1838 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1838.severity = warning + +# Title : Use 'Environment.ProcessPath' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1839 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1839.severity = warning + +# Title : Use 'Environment.CurrentManagedThreadId' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1840 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1840.severity = warning + +# Title : Prefer Dictionary.Contains methods +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1841 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1841.severity = warning + +# Title : Do not use 'WhenAll' with a single task +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1842 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1842.severity = suggestion + +# Title : Do not use 'WaitAll' with a single task +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1843 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1843.severity = warning + +# Title : Provide memory-based overrides of async methods when subclassing 'Stream' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1844 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1844.severity = warning + +# Title : Use span-based 'string.Concat' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1845 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1845.severity = warning + +# Title : Prefer 'AsSpan' over 'Substring' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1846 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1846.severity = warning + +# Title : Use char literal for a single character lookup +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1847 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1847.severity = warning + +# Title : Use the LoggerMessage delegates +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848 +# Tags : Telemetry, EnabledRuleInAggressiveMode +# Comment : Use R9 logging model instead +dotnet_diagnostic.CA1848.severity = none + +# Title : Call async methods when in an async method +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1849 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1849.severity = warning + +# Title : Prefer static 'HashData' method over 'ComputeHash' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1850 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1850.severity = suggestion + +# Title : Possible multiple enumerations of 'IEnumerable' collection +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1851.severity = suggestion + +# Title : Seal internal types +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852 +# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA1852.severity = none + +# Title : Unnecessary call to 'Dictionary.ContainsKey(key)' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1853 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1853.severity = suggestion + +# Title : Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1854 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1854.severity = warning + +# Title : Prefer 'Clear' over 'Fill' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1855 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1855.severity = warning + +# Title : Dispose objects before losing scope +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000 +# Tags : PortedFromFxCop, Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2000.severity = warning + +# Title : Do not lock on objects with weak identity +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2002 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2002.severity = warning + +# Title : Consider calling ConfigureAwait on the awaited task +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2007 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2007.severity = warning + +# Title : Do not create tasks without passing a TaskScheduler +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2008 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2008.severity = warning + +# Title : Do not call ToImmutableCollection on an ImmutableCollection value +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2009 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2009.severity = warning + +# Title : Avoid infinite recursion +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2011 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2011.severity = error + +# Title : Use ValueTasks correctly +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2012 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2012.severity = warning + +# Title : Do not use ReferenceEquals with value types +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2013 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2013.severity = warning + +# Title : Do not use stackalloc in loops +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2014 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2014.severity = warning + +# Title : Do not use stackalloc in loops +# Category : Reliability +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2014 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2014.severity = warning + +# Title : Do not define finalizers for types derived from MemoryManager +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2015 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2015.severity = warning + +# Title : Forward the 'CancellationToken' parameter to methods +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2016 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2016.severity = warning + +# Title : Parameter count mismatch +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2017 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2017.severity = warning + +# Title : 'Buffer.BlockCopy' expects the number of bytes to be copied for the 'count' argument +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2018 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2018.severity = warning + +# Title : Improper 'ThreadStatic' field initialization +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2019 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2019.severity = warning + +# Title : Prevent from behavioral change +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2020 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2020.severity = suggestion + +# Title : Review SQL queries for security vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2100 +# Tags : PortedFromFxCop, Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2100.severity = warning + +# Title : Specify marshaling for P/Invoke string arguments +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2101 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2101.severity = warning + +# Title : Review visible event handlers +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2109 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2109.severity = warning +dotnet_code_quality.CA2109.api_surface = public + +# Title : Seal methods that satisfy private interfaces +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2119 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2119.severity = warning + +# Title : Do Not Catch Corrupted State Exceptions +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2153 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2153.severity = warning + +# Title : Rethrow to preserve stack details +# Category : Usage +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2200 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2200.severity = warning + +# Title : Rethrow to preserve stack details +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2200 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2200.severity = warning + +# Title : Do not raise reserved exception types +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2201 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2201.severity = warning + +# Title : Initialize value type static fields inline +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2207 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2207.severity = warning + +# Title : Instantiate argument exceptions correctly +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2208 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2208.severity = warning + +# Title : Non-constant fields should not be visible +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2211 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2211.severity = warning + +# Title : Disposable fields should be disposed +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2213 +# Tags : PortedFromFxCop, Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2213.severity = warning + +# Title : Do not call overridable methods in constructors +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2214 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2214.severity = warning + +# Title : Dispose methods should call base class dispose +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2215 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2215.severity = warning + +# Title : Disposable types should declare finalizer +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2216 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2216.severity = warning + +# Title : Do not mark enums with FlagsAttribute +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2217 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2217.severity = warning +dotnet_code_quality.CA2217.api_surface = all + +# Title : Do not raise exceptions in finally clauses +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2219 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2219.severity = warning + +# Title : Operator overloads have named alternates +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2225 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2225.severity = warning +dotnet_code_quality.CA2225.api_surface = public + +# Title : Operators should have symmetrical overloads +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2226 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2226.severity = none + +# Title : Collection properties should be read only +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2227 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2227.severity = warning + +# Title : Implement serialization constructors +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2229 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +# Comment : Obsolete +dotnet_diagnostic.CA2229.severity = none + +# Title : Overload operator equals on overriding value type Equals +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2231 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2231.severity = error + +# Title : Pass system uri objects instead of strings +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2234 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2234.severity = warning + +# Title : Mark all non-serializable fields +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2235 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +# Comment : Obsolete +dotnet_diagnostic.CA2235.severity = none + +# Title : Mark ISerializable types with serializable +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2237 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +# Comment : Obsolete +dotnet_diagnostic.CA2237.severity = none + +# Title : Provide correct arguments to formatting methods +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2241 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2241.severity = warning + +# Title : Test for NaN correctly +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2242 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2242.severity = warning + +# Title : Attribute string literals should parse correctly +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2243 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2243.severity = warning + +# Title : Do not duplicate indexed element initializations +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2244 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2244.severity = warning + +# Title : Do not assign a property to itself +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2245 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2245.severity = warning + +# Title : Assigning symbol and its member in the same statement +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2246 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2246.severity = warning + +# Title : Argument passed to TaskCompletionSource constructor should be TaskCreationOptions enum instead of TaskContinuationOptions enum +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2247 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2247.severity = warning + +# Title : Provide correct 'enum' argument to 'Enum.HasFlag' +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2248 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2248.severity = warning + +# Title : Consider using 'string.Contains' instead of 'string.IndexOf' +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2249 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2249.severity = warning + +# Title : Use 'ThrowIfCancellationRequested' +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2250 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2250.severity = suggestion + +# Title : Use 'string.Equals' +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2251 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2251.severity = warning + +# Title : This API requires opting into preview features +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2252 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2252.severity = error + +# Title : Named placeholders should not be numeric values +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2253 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2253.severity = warning + +# Title : Template should be a static expression +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2254 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2254.severity = suggestion + +# Title : The 'ModuleInitializer' attribute should not be used in libraries +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2255 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2255.severity = warning + +# Title : All members declared in parent interfaces must have an implementation in a DynamicInterfaceCastableImplementation-attributed interface +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2256 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2256.severity = error + +# Title : Members defined on an interface with the 'DynamicInterfaceCastableImplementationAttribute' should be 'static' +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2257 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2257.severity = warning + +# Title : Providing a 'DynamicInterfaceCastableImplementation' interface in Visual Basic is unsupported +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2258 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2258.severity = warning + +# Title : 'ThreadStatic' only affects static fields +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2259 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2259.severity = warning + +# Title : Use correct type parameter +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2260 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2260.severity = warning + +# Title : Do not use insecure deserializer BinaryFormatter +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2300 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2300.severity = warning + +# Title : Do not call BinaryFormatter.Deserialize without first setting BinaryFormatter.Binder +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2301 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2301.severity = warning + +# Title : Ensure BinaryFormatter.Binder is set before calling BinaryFormatter.Deserialize +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2302 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2302.severity = warning + +# Title : Do not use insecure deserializer LosFormatter +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2305 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2305.severity = warning + +# Title : Do not use insecure deserializer NetDataContractSerializer +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2310 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2310.severity = warning + +# Title : Do not deserialize without first setting NetDataContractSerializer.Binder +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2311 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2311.severity = warning + +# Title : Ensure NetDataContractSerializer.Binder is set before deserializing +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2312 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2312.severity = warning + +# Title : Do not use insecure deserializer ObjectStateFormatter +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2315 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2315.severity = warning + +# Title : Do not deserialize with JavaScriptSerializer using a SimpleTypeResolver +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2321 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2321.severity = warning + +# Title : Ensure JavaScriptSerializer is not initialized with SimpleTypeResolver before deserializing +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2322 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2322.severity = warning + +# Title : Do not use TypeNameHandling values other than None +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2326 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2326.severity = warning + +# Title : Do not use insecure JsonSerializerSettings +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2327 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2327.severity = warning + +# Title : Ensure that JsonSerializerSettings are secure +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2328 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2328.severity = warning + +# Title : Do not deserialize with JsonSerializer using an insecure configuration +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2329 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2329.severity = warning + +# Title : Ensure that JsonSerializer has a secure configuration when deserializing +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2330 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2330.severity = warning + +# Title : Do not use DataTable.ReadXml() with untrusted data +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2350 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2350.severity = warning + +# Title : Do not use DataSet.ReadXml() with untrusted data +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2351 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2351.severity = warning + +# Title : Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2352 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2352.severity = warning + +# Title : Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2352 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2352.severity = warning + +# Title : Unsafe DataSet or DataTable in serializable type +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2353 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2353.severity = warning + +# Title : Unsafe DataSet or DataTable in serializable type +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2353 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2353.severity = warning + +# Title : Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attacks +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2354 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2354.severity = warning + +# Title : Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attacks +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2354 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2354.severity = warning + +# Title : Unsafe DataSet or DataTable type found in deserializable object graph +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2355 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2355.severity = warning + +# Title : Unsafe DataSet or DataTable type found in deserializable object graph +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2355 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2355.severity = warning + +# Title : Unsafe DataSet or DataTable type in web deserializable object graph +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2356 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2356.severity = warning + +# Title : Unsafe DataSet or DataTable type in web deserializable object graph +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2356 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2356.severity = warning + +# Title : Ensure auto-generated class containing DataSet.ReadXml() is not used with untrusted data +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2361 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2361.severity = warning + +# Title : Unsafe DataSet or DataTable in auto-generated serializable type can be vulnerable to remote code execution attacks +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2362 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2362.severity = warning + +# Title : Unsafe DataSet or DataTable in autogenerated serializable type can be vulnerable to remote code execution attacks +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2362 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2362.severity = warning + +# Title : Review code for SQL injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3001 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3001.severity = warning + +# Title : Review code for XSS vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3002 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3002.severity = warning + +# Title : Review code for file path injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3003 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3003.severity = warning + +# Title : Review code for information disclosure vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3004 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3004.severity = warning + +# Title : Review code for LDAP injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3005 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3005.severity = warning + +# Title : Review code for process command injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3006 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3006.severity = warning + +# Title : Review code for open redirect vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3007 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3007.severity = warning + +# Title : Review code for XPath injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3008 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3008.severity = warning + +# Title : Review code for XML injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3009 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3009.severity = warning + +# Title : Review code for XAML injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3010 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3010.severity = warning + +# Title : Review code for DLL injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3011 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3011.severity = warning + +# Title : Review code for regex injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3012 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3012.severity = warning + +# Title : Do Not Add Schema By URL +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3061 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3061.severity = silent + +# Title : Insecure DTD processing in XML +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3075 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3075.severity = warning + +# Title : Insecure XSLT script processing. +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3076 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3076.severity = warning + +# Title : Insecure XSLT script processing +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3076 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3076.severity = warning + +# Title : Insecure Processing in API Design, XmlDocument and XmlTextReader +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3077 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3077.severity = warning + +# Title : Insecure Processing in API Design, XmlDocument and XmlTextReader +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3077 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3077.severity = warning + +# Title : Mark Verb Handlers With Validate Antiforgery Token +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3147 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3147.severity = warning + +# Title : Do Not Use Weak Cryptographic Algorithms +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5350 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5350.severity = error + +# Title : Do Not Use Broken Cryptographic Algorithms +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5351 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5351.severity = error + +# Title : Review cipher mode usage with cryptography experts +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5358 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5358.severity = warning + +# Title : Do Not Disable Certificate Validation +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5359 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5359.severity = warning + +# Title : Do Not Call Dangerous Methods In Deserialization +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5360 +# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5360.severity = warning + +# Title : Do Not Disable SChannel Use of Strong Crypto +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5361 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5361.severity = warning + +# Title : Potential reference cycle in deserialized object graph +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5362 +# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5362.severity = warning + +# Title : Do Not Disable Request Validation +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5363 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5363.severity = warning + +# Title : Do Not Use Deprecated Security Protocols +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5364 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5364.severity = warning + +# Title : Do Not Disable HTTP Header Checking +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5365 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5365.severity = warning + +# Title : Use XmlReader for 'DataSet.ReadXml()' +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5366 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5366.severity = warning + +# Title : Do Not Serialize Types With Pointer Fields +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5367 +# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5367.severity = warning + +# Title : Set ViewStateUserKey For Classes Derived From Page +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5368 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5368.severity = warning + +# Title : Use XmlReader for 'XmlSerializer.Deserialize()' +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5369 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5369.severity = warning + +# Title : Use XmlReader for XmlValidatingReader constructor +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5370 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5370.severity = warning + +# Title : Use XmlReader for 'XmlSchema.Read()' +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5371 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5371.severity = warning + +# Title : Use XmlReader for XPathDocument constructor +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5372 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5372.severity = warning + +# Title : Do not use obsolete key derivation function +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5373 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5373.severity = warning + +# Title : Do Not Use XslTransform +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5374 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5374.severity = warning + +# Title : Do Not Use Account Shared Access Signature +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5375 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5375.severity = warning + +# Title : Use SharedAccessProtocol HttpsOnly +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5376 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5376.severity = warning + +# Title : Use Container Level Access Policy +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5377 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5377.severity = warning + +# Title : Do not disable ServicePointManagerSecurityProtocols +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5378 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5378.severity = warning + +# Title : Ensure Key Derivation Function algorithm is sufficiently strong +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5379 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5379.severity = warning + +# Title : Do Not Add Certificates To Root Store +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5380 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5380.severity = warning + +# Title : Ensure Certificates Are Not Added To Root Store +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5381 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5381.severity = warning + +# Title : Use Secure Cookies In ASP.NET Core +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5382 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5382.severity = warning + +# Title : Ensure Use Secure Cookies In ASP.NET Core +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5383 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5383.severity = warning + +# Title : Do Not Use Digital Signature Algorithm (DSA) +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5384 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5384.severity = warning + +# Title : Use Rivest-Shamir-Adleman (RSA) Algorithm With Sufficient Key Size +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5385 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5385.severity = warning + +# Title : Avoid hardcoding SecurityProtocolType value +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5386 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5386.severity = warning + +# Title : Do Not Use Weak Key Derivation Function With Insufficient Iteration Count +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5387 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5387.severity = warning + +# Title : Ensure Sufficient Iteration Count When Using Weak Key Derivation Function +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5388 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5388.severity = warning + +# Title : Do Not Add Archive Item's Path To The Target File System Path +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5389 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5389.severity = warning + +# Title : Do not hard-code encryption key +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5390 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5390.severity = warning + +# Title : Use antiforgery tokens in ASP.NET Core MVC controllers +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5391 +# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5391.severity = warning + +# Title : Use DefaultDllImportSearchPaths attribute for P/Invokes +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5392 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5392.severity = warning + +# Title : Do not use unsafe DllImportSearchPath value +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5393 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5393.severity = warning + +# Title : Do not use insecure randomness +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5394 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5394.severity = warning + +# Title : Miss HttpVerb attribute for action methods +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5395 +# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5395.severity = warning + +# Title : Set HttpOnly to true for HttpCookie +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5396 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5396.severity = warning + +# Title : Do not use deprecated SslProtocols values +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5397 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5397.severity = warning + +# Title : Avoid hardcoded SslProtocols values +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5398 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5398.severity = warning + +# Title : HttpClients should enable certificate revocation list checks +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5399 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5399.severity = warning + +# Title : Ensure HttpClient certificate revocation list check is not disabled +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5400 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5400.severity = warning + +# Title : Do not use CreateEncryptor with non-default IV +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5401 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5401.severity = warning + +# Title : Use CreateEncryptor with the default IV +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5402 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5402.severity = warning + +# Title : Do not hard-code certificate +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5403 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5403.severity = warning + +# Title : Do not disable token validation checks +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5404 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5404.severity = warning + +# Title : Do not always skip token validation in delegates +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5405 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5405.severity = warning + +# Title : Avoid single use string builders in frequently called class members. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR1001.severity = warning + +# Title : Use bitwise operations instead of 'Enum.HasFlag' +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : Obsolete +dotnet_diagnostic.CPR101.severity = none + +# Title : Use HashSet.Contains instead of List.Contains +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR102.severity = warning + +# Title : Use Ordinal and OrdinalIgnoreCase instead of InvariantCulture when localization is not required. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR103.severity = warning + +# Title : Use DateTime.UtcNow instead of DateTime.Now when time zone conversion is not applicable. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR105.severity = warning + +# Title : MemoryStream.ToArray() is memory inefficient and can often be avoided. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR107.severity = warning + +# Title : List.AddRange() is memory inefficient. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : Obsolete +dotnet_diagnostic.CPR108.severity = none + +# Title : List.Reverse can cause boxing. Implement your own reverse for better performance. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : Obsolete +dotnet_diagnostic.CPR109.severity = none + +# Title : Specify an initial list size when initializing a list to reduce reallocations. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : Too noisy +dotnet_diagnostic.CPR110.severity = none + +# Title : ImmutableDictionary is memory inefficient. Use IReadOnlyDictionary if readonly collection is needed. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR111.severity = warning + +# Title : ImmutableList is memory inefficient. Use IReadOnlyList if a readonly collection is needed. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR112.severity = warning + +# Title : Avoid Linq as much as possible since much of the functionality is inefficient. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : Too noisy +dotnet_diagnostic.CPR113.severity = none + +# Title : string.StartWith and string.EndsWith can be implemented more efficiently checking characters with the indexer for short strings. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR114.severity = warning + +# Title : string.Contains with string.ToLower() or string.ToUpper() causes allocations which can be avoided with a case insensitive comparison. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR115.severity = warning + +# Title : string.Equals with string.ToLower() or string.ToUpper() causes allocations which can be avoided with a case insensitive comparison. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR116.severity = warning + +# Title : operator== with string.ToLower() or string.ToUpper() causes allocations which can be avoided with a case insensitive comparison. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR117.severity = warning + +# Title : Linq.SequenceEqual is inefficient. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR118.severity = warning + +# Title : Use Debug.WriteLine for debugging instead of Console.WriteLine. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : Duplicate +dotnet_diagnostic.CPR119.severity = none + +# Title : File.ReadAllXXX should be replaced by using a StreamReader to avoid adding objects to the large object heap (LOH). +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR120.severity = warning + +# Title : Specify 'concurrencyLevel' and 'capacity' in the ConcurrentDictionary ctor. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR121.severity = warning + +# Title : ConcurrentDictionary.Keys and ConcurrentDictionary.Values takes a lock defeats the benefits of concurrency. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR122.severity = warning + +# Title : ConcurrentDictionary Count, ToArray(), CopyTo() and Clear() take locks and defeats the benefits of the concurrency. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR123.severity = warning + +# Title : TraceSource.TraceEvent does a lock on each listener and can cause lock contention. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR124.severity = warning + +# Title : String interning can cause lock contention. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR125.severity = warning + +# Title : string.Format and StringBuilder.AppendFormat are not efficient for concatenation. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR126.severity = warning + +# Title : Use a custom implementation of IComparer rather than Nullable.Compare. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR127.severity = warning + +# Title : Process.GetProcessName/Process.GetMachineName does a lot of processing. Use once per process and save the result. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR128.severity = warning + +# Title : string.IndexOf is inefficient when used to check the beginning of string. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR129.severity = warning + +# Title : ArrayList is non-generic. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR130.severity = none + +# Title : Hashtable is non-generic. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR131.severity = none + +# Title : Avoid char.ToString. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR134.severity = warning + +# Title : Stream.CopyTo should be used with buffer size. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR135.severity = warning + +# Title : StreamReader.ReadLine can allocate StringBuilder instances each call if the lines are longer than the buffer size. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR136.severity = warning + +# Title : Random class instances should be shared as statics. Random is not thread safe so locks, ThreadLocal class or [ThreadStatic] attribute should be used for synchronization. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR138.severity = warning + +# Title : Regular expressions should be reused from static fields or properties. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR139.severity = warning + +# Title : TextWriter.WriteLine(string) allocates a char array. Use different overload or make two calls. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR140.severity = warning + +# Title : Reduce delegate allocations by storing them in static fields and properties. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : This rule flags most lambda uses, which makes it extremely verbose and not particularly useful +dotnet_diagnostic.CPR145.severity = none + +# Title : Extra dictionary access +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR500.severity = warning + +# Title : Avoid repeated type checking. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR501.severity = warning + +# Title : Do not cast multiple times. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR502.severity = warning + +# Title : Extra HashSet access +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR503.severity = warning + +# Title : Do not use banned insecure deserialization APIs +# Category : Security +# Help Link: https://aka.ms/ia2989 +dotnet_diagnostic.IA2989.severity = error + +# Title : Do Not Use Banned APIs For Insecure Deserializers +# Category : Security +# Help Link: https://aka.ms/ia2992 +dotnet_diagnostic.IA2992.severity = error + +# Title : Do Not Use Banned Constructors For Insecure Deserializers +# Category : Security +# Help Link: https://aka.ms/ia2993 +dotnet_diagnostic.IA2993.severity = error + +# Title : Do Not Use ResourceSet Without ResourceReader +# Category : Security +# Help Link: https://aka.ms/ia2994 +dotnet_diagnostic.IA2994.severity = error + +# Title : Do Not Use ResourceReader +# Category : Security +# Help Link: https://aka.ms/ia2995 +dotnet_diagnostic.IA2995.severity = error + +# Title : Do Not Use ResXResourceReader Without ITypeResolutionService +# Category : Security +# Help Link: https://aka.ms/ia2996 +dotnet_diagnostic.IA2996.severity = error + +# Title : Do Not Use TypeNameHandling Other Than None +# Category : Security +# Help Link: https://aka.ms/ia2997 +dotnet_diagnostic.IA2997.severity = error + +# Title : Do Not Deserialize With BinaryFormatter Without Binder +# Category : Security +# Help Link: https://aka.ms/ia2998 +dotnet_diagnostic.IA2998.severity = error + +# Title : Do Not Set BinaryFormatter.Binder to null +# Category : Security +# Help Link: https://aka.ms/ia2999 +dotnet_diagnostic.IA2999.severity = error + +# Title : Do Not Use Weak Cryptographic Algorithms +# Category : Security +# Help Link: http://aka.ms/IA5350 +# Tags : Telemetry +dotnet_diagnostic.IA5350.severity = error + +# Title : Do Not Use Broken Cryptographic Algorithms +# Category : Security +# Help Link: http://aka.ms/IA5351 +# Tags : Telemetry +dotnet_diagnostic.IA5351.severity = error + +# Title : Do Not Misuse Cryptographic APIs +# Category : Security +# Help Link: http://aka.ms/IA5352 +# Tags : Telemetry +dotnet_diagnostic.IA5352.severity = error + +# Title : Use approved crypto libraries for the supported platform +# Category : Security +# Help Link: https://aka.ms/ia5359 +# Tags : Telemetry +dotnet_diagnostic.IA5359.severity = error + +# Title : Custom web token handler was found +# Category : Security +# Help Link: https://aka.ms/ia6450 +# Tags : Telemetry +dotnet_diagnostic.IA6450.severity = error + +# Title : Implement required validations for app asserted actor token +# Category : Security +# Help Link: https://aka.ms/ia6451 +# Tags : Telemetry +dotnet_diagnostic.IA6451.severity = warning + +# Title : Do not disable {0} +# Category : Security +# Help Link: https://aka.ms/ia6452 +# Tags : Telemetry +dotnet_diagnostic.IA6452.severity = error + +# Title : Do not disable {0} +# Category : Security +# Help Link: https://aka.ms/ia6453 +# Tags : Telemetry +dotnet_diagnostic.IA6453.severity = error + +# Title : Do not disable {0} +# Category : Security +# Help Link: https://aka.ms/ia6454 +# Tags : Telemetry +dotnet_diagnostic.IA6454.severity = error + +# Title : Do Not Use Insecure PowerShell LanguageModes +# Category : Security +# Help Link: https://aka.ms/ia6456 +# Tags : Telemetry +dotnet_diagnostic.IA6456.severity = error + +# Title : Review PowerShell Execution for PowerShell Injection +# Category : Security +# Help Link: https://aka.ms/IA6457 +dotnet_diagnostic.IA6457.severity = warning + +# Title : Simplify name +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0001 +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line. This metadata was entered manually, since it is not exposed in the normal way from the analyzer assembly. +dotnet_diagnostic.IDE0001.severity = silent + +# Title : Simplify name +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0002 +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line. This metadata was entered manually, since it is not exposed in the normal way from the analyzer assembly. +dotnet_diagnostic.IDE0002.severity = silent + +# Title : Remove this or Me qualification +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0003-ide0009 +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line. This metadata was entered manually, since it is not exposed in the normal way from the analyzer assembly. +dotnet_diagnostic.IDE0003.severity = silent +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Title : Remove Unnecessary Cast +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0004 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled, Unnecessary +dotnet_diagnostic.IDE0004.severity = warning + +# Title : Using directive is unnecessary. +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended, Unnecessary +dotnet_diagnostic.IDE0005.severity = none + +# Title : Using directive is unnecessary. +# Category : Style +# Tags : Telemetry, EnforceOnBuild_Never, NotConfigurable, Unnecessary +dotnet_diagnostic.IDE0005_gen.severity = none + +# Title : Use implicit type +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0007 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0007.severity = silent +csharp_style_var_elsewhere = true +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = true + +# Title : Use explicit type +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0008 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0008.severity = silent + +# Title : Member access should be qualified. +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0009 +# Tags : Telemetry, EnforceOnBuild_Never +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line +dotnet_diagnostic.IDE0009.severity = none + +# Title : Add missing cases +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0010 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0010.severity = silent + +# Title : Add braces +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0011 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0011.severity = warning +csharp_prefer_braces = true + +# Title : Use 'throw' expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0016 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0016.severity = warning +csharp_style_throw_expression = true + +# Title : Simplify object initialization +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0017 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0017.severity = warning +dotnet_style_object_initializer = true + +# Title : Inline variable declaration +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0018 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0018.severity = warning +csharp_style_inlined_variable_declaration = true + +# Title : Use pattern matching +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0019 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0019.severity = silent +csharp_style_pattern_matching_over_is_with_cast_check = true + +# Title : Use pattern matching +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0020 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0020.severity = silent +csharp_style_pattern_matching_over_is_with_cast_check + +# Title : Use expression body for constructors +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0021 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0021.severity = warning +csharp_style_expression_bodied_constructors = false + +# Title : Use expression body for methods +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0022 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0022.severity = silent +csharp_style_expression_bodied_methods = when_on_single_line + +# Title : Use expression body for conversion operators +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0023 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0023.severity = warning +csharp_style_expression_bodied_operators = false + +# Title : Use expression body for operators +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0024 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0024.severity = warning +csharp_style_expression_bodied_operators = false + +# Title : Use expression body for properties +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0025 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0025.severity = warning +csharp_style_expression_bodied_properties = true + +# Title : Use expression body for indexers +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0026 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0026.severity = warning +csharp_style_expression_bodied_indexers = true + +# Title : Use expression body for accessors +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0027 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0027.severity = warning +csharp_style_expression_bodied_accessors = true + +# Title : Simplify collection initialization +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0028.severity = warning +dotnet_style_collection_initializer = true + +# Title : Use coalesce expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0029 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0029.severity = warning +dotnet_style_coalesce_expression = true + +# Title : Use coalesce expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0030 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0030.severity = warning +dotnet_style_coalesce_expression = true + +# Title : Use null propagation +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0031 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0031.severity = warning +dotnet_style_null_propagation = true + +# Title : Use auto property +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0032 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0032.severity = warning +dotnet_style_prefer_auto_properties = true + +# Title : Use explicitly provided tuple name +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0033 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0033.severity = warning +dotnet_style_explicit_tuple_names = true + +# Title : Simplify 'default' expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0034 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0034.severity = warning +csharp_prefer_simple_default_expression = true + +# Title : Unreachable code detected +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0035 +# Tags : Telemetry, EnforceOnBuild_Never, NotConfigurable, Unnecessary +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line +dotnet_diagnostic.IDE0035.severity = warning + +# Title : Order modifiers +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0036 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0036.severity = silent +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async + +# Title : Use inferred member name +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0037 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled, Unnecessary +dotnet_diagnostic.IDE0037.severity = silent +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true + +# Title : Use local function +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0039 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0039.severity = silent +csharp_style_prefer_local_over_anonymous_function = false + +# Title : Add accessibility modifiers +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0040 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0040.severity = warning +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Title : Use 'is null' check +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0041 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0041.severity = warning +dotnet_style_prefer_is_null_check_over_reference_equality_method = true + +# Title : Deconstruct variable declaration +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0042 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0042.severity = silent +csharp_style_deconstructed_variable_declaration = true + +# Title : Invalid format string +# Category : Compiler +# Tags : EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0043.severity = warning + +# Title : Add readonly modifier +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0044 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0044.severity = warning +dotnet_style_readonly_field = true:warning + +# Title : Convert to conditional expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0045 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0045.severity = silent +dotnet_style_prefer_conditional_expression_over_assignment = true + +# Title : Convert to conditional expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0046 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0046.severity = silent +dotnet_style_prefer_conditional_expression_over_return = true + +# Title : Remove unnecessary parentheses +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0047 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0047.severity = silent +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Title : Add parentheses for clarity +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0048 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0048.severity = silent +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Title : Use language keywords instead of framework type names for type references +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0049 +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line. This metadata was entered manually, since it is not exposed in the normal way from the analyzer assembly. +dotnet_diagnostic.IDE0049.severity = none + +# Title : Convert to tuple +# Category : Style +# Tags : Telemetry +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line +dotnet_diagnostic.IDE0050.severity = silent + +# Title : Remove unused private members +# Category : CodeQuality +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended, Unnecessary +dotnet_diagnostic.IDE0051.severity = warning + +# Title : Remove unread private members +# Category : CodeQuality +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0052 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended, Unnecessary +dotnet_diagnostic.IDE0052.severity = warning + +# Title : Use expression body for lambda expressions +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0053 +# Tags : Telemetry, EnforceOnBuild_Recommended +# Comment : This metadata was entered manually, since it is not exposed in the normal way from the analyzer assembly. +dotnet_diagnostic.IDE0053.severity = suggestion +csharp_style_expression_bodied_lambdas = when_on_single_line + +# Title : Use compound assignment +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0054 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0054.severity = warning +dotnet_style_prefer_compound_assignment = true + +# Title : Fix formatting +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0055.severity = warning +dotnet_style_namespace_match_folder = true +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_labels = flush_left +csharp_indent_switch_labels = true +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = false + +# Title : Use index operator +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0056 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0056.severity = silent +csharp_style_prefer_index_operator = true + +# Title : Use range operator +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0057 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0057.severity = silent +csharp_style_prefer_range_operator = true:warning + +# Title : Expression value is never used +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0058 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0058.severity = warning +csharp_style_unused_value_expression_statement_preference = discard_variable + +# Title : Unnecessary assignment of a value +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0059 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended, Unnecessary +dotnet_diagnostic.IDE0059.severity = none + +# Title : Remove unused parameter +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0060 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended, Unnecessary +dotnet_diagnostic.IDE0060.severity = warning +dotnet_code_quality_unused_parameters = all + +# Title : Use expression body for local functions +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0061 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0061.severity = warning +csharp_style_expression_bodied_local_functions = true + +# Title : Make local function 'static' +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0062 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0062.severity = warning +csharp_prefer_static_local_function = true + +# Title : Use simple 'using' statement +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0063 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0063.severity = warning +csharp_prefer_simple_using_statement = true + +# Title : Make readonly fields writable +# Category : CodeQuality +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0064 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0064.severity = warning + +# Title : Misplaced using directive +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0065 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0065.severity = warning +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true +csharp_using_directive_placement = outside_namespace:suggestion + +# Title : Convert switch statement to expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0066 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0066.severity = warning +csharp_style_prefer_switch_expression = true:warning + +# Title : Use 'System.HashCode' +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0070 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0070.severity = warning + +# Title : Simplify interpolation +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0071 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0071.severity = warning +dotnet_style_prefer_simplified_interpolation = true + +# Title : Add missing cases +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0072 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0072.severity = silent + +# Title : The file header is missing or not located at the top of the file +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0073 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0073.severity = warning + +# Title : Use compound assignment +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0074 +# Tags : Telemetry, EnforceOnBuild_Recommended +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line +dotnet_diagnostic.IDE0074.severity = suggestion +dotnet_style_prefer_compound_assignment = true + +# Title : Simplify conditional expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0075 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0075.severity = warning +dotnet_style_prefer_simplified_boolean_expressions = true + +# Title : Invalid global 'SuppressMessageAttribute' +# Category : CodeQuality +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0076 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended, Unnecessary +dotnet_diagnostic.IDE0076.severity = warning + +# Title : Avoid legacy format target in 'SuppressMessageAttribute' +# Category : CodeQuality +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0077 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0077.severity = warning + +# Title : Use pattern matching +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0078 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0078.severity = silent +csharp_style_prefer_pattern_matching = true + +# Title : Remove unnecessary suppression +# Category : Style +# Tags : Telemetry +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line +dotnet_diagnostic.IDE0079.severity = suggestion +dotnet_remove_unnecessary_suppression_exclusions = CS0618 + +# Title : Remove unnecessary suppression operator +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0080 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0080.severity = warning + +# Title : 'typeof' can be converted to 'nameof' +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0082 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0082.severity = warning + +# Title : Use pattern matching +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0083 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0083.severity = silent +csharp_style_prefer_not_pattern = true + +# Title : Use 'new(...)' +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0090 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0090.severity = warning +csharp_style_implicit_object_creation_when_type_is_apparent = true + +# Title : Remove redundant equality +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0100 +# Tags : Telemetry, EnforceOnBuild_Recommended +# Comment : S1125 triggers in more cases +dotnet_diagnostic.IDE0100.severity = none + +# Title : Remove unnecessary discard +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0110 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0110.severity = warning + +# Title : Simplify LINQ expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0120 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0120.severity = silent + +# Title : Namespace does not match folder structure +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0130 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0130.severity = silent + +# Title : Prefer 'null' check over type check +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0150 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0150.severity = warning +csharp_style_prefer_null_check_over_type_check = true + +# Title : Convert to block scoped namespace +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0160 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0160.severity = silent +csharp_style_namespace_declarations = file_scoped + +# Title : Convert to file-scoped namespace +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0161 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0161.severity = silent +csharp_style_namespace_declarations = file_scoped + +# Title : Property pattern can be simplified +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0170 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0170.severity = silent +csharp_style_prefer_extended_property_pattern = true + +# Title : Use tuple to swap values +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0180 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0180.severity = silent +csharp_style_prefer_tuple_swap = true + +# Title : Null check can be simplified +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0190 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0190.severity = silent + +# Title : Remove unnecessary lambda expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0200 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0200.severity = silent + +# Title : Convert to top-level statements +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0210 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0210.severity = silent + +# Title : Convert to 'Program.Main' style program +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0211 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0211.severity = silent + +# Title : Add explicit cast +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0220 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0220.severity = silent + +# Title : Use UTF-8 string literal +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0230 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0230.severity = silent + +# Title : Remove redundant nullable directive +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0240 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0240.severity = warning + +# Title : Remove unnecessary nullable directive +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0241 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0241.severity = warning + +# Title : Make struct 'readonly' +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0250 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0250.severity = warning + +# Title : Delegate invocation can be simplified. +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide1005 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE1005.severity = warning +csharp_style_conditional_delegate_call = true + +# Title : Naming Styles +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide1006 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE1006.severity = warning +dotnet_naming_rule.interface_should_be_ipascalcase.severity = warning +dotnet_naming_rule.interface_should_be_ipascalcase.symbols = interface +dotnet_naming_rule.interface_should_be_ipascalcase.style = ipascalcase +dotnet_naming_rule.types_should_be_pascalcase.severity = warning +dotnet_naming_rule.types_should_be_pascalcase.symbols = types +dotnet_naming_rule.types_should_be_pascalcase.style = pascalcase +dotnet_naming_rule.constant_should_be_pascalcase.severity = warning +dotnet_naming_rule.constant_should_be_pascalcase.symbols = constant +dotnet_naming_rule.constant_should_be_pascalcase.style = pascalcase +dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = warning +dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase +dotnet_naming_rule.method_parameter_should_be_camelcase.severity = warning +dotnet_naming_rule.method_parameter_should_be_camelcase.symbols = method_parameter +dotnet_naming_rule.method_parameter_should_be_camelcase.style = camelcase +dotnet_naming_rule.local_variable_should_be_camelcase.severity = warning +dotnet_naming_rule.local_variable_should_be_camelcase.symbols = local_variable +dotnet_naming_rule.local_variable_should_be_camelcase.style = camelcase +dotnet_naming_rule.type_parameter_should_be_tpascalcase.severity = warning +dotnet_naming_rule.type_parameter_should_be_tpascalcase.symbols = type_parameter +dotnet_naming_rule.type_parameter_should_be_tpascalcase.style = tpascalcase +dotnet_naming_rule.private_field_should_be__camelcase.severity = warning +dotnet_naming_rule.private_field_should_be__camelcase.symbols = private_field +dotnet_naming_rule.private_field_should_be__camelcase.style = _camelcase +dotnet_naming_rule.field_should_be_pascalcase.severity = warning +dotnet_naming_rule.field_should_be_pascalcase.symbols = field +dotnet_naming_rule.field_should_be_pascalcase.style = pascalcase +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = +dotnet_naming_symbols.method_parameter.applicable_kinds = parameter +dotnet_naming_symbols.method_parameter.applicable_accessibilities = * +dotnet_naming_symbols.method_parameter.required_modifiers = +dotnet_naming_symbols.local_variable.applicable_kinds = local +dotnet_naming_symbols.local_variable.applicable_accessibilities = local +dotnet_naming_symbols.local_variable.required_modifiers = +dotnet_naming_symbols.type_parameter.applicable_kinds = type_parameter +dotnet_naming_symbols.type_parameter.applicable_accessibilities = * +dotnet_naming_symbols.type_parameter.required_modifiers = +dotnet_naming_symbols.constant.applicable_kinds = field, local +dotnet_naming_symbols.constant.applicable_accessibilities = * +dotnet_naming_symbols.constant.required_modifiers = const +dotnet_naming_symbols.private_field.applicable_kinds = field +dotnet_naming_symbols.private_field.applicable_accessibilities = private +dotnet_naming_symbols.private_field.required_modifiers = +dotnet_naming_symbols.field.applicable_kinds = field +dotnet_naming_symbols.field.applicable_accessibilities = public, internal, protected, protected_internal, private_protected, local +dotnet_naming_symbols.field.required_modifiers = +dotnet_naming_style.pascalcase.required_prefix = +dotnet_naming_style.pascalcase.required_suffix = +dotnet_naming_style.pascalcase.word_separator = +dotnet_naming_style.pascalcase.capitalization = pascal_case +dotnet_naming_style.ipascalcase.required_prefix = I +dotnet_naming_style.ipascalcase.required_suffix = +dotnet_naming_style.ipascalcase.word_separator = +dotnet_naming_style.ipascalcase.capitalization = pascal_case +dotnet_naming_style.camelcase.required_prefix = +dotnet_naming_style.camelcase.required_suffix = +dotnet_naming_style.camelcase.word_separator = +dotnet_naming_style.camelcase.capitalization = camel_case +dotnet_naming_style._camelcase.required_prefix = _ +dotnet_naming_style._camelcase.required_suffix = +dotnet_naming_style._camelcase.word_separator = +dotnet_naming_style._camelcase.capitalization = camel_case +dotnet_naming_style.tpascalcase.required_prefix = T +dotnet_naming_style.tpascalcase.required_suffix = +dotnet_naming_style.tpascalcase.word_separator = +dotnet_naming_style.tpascalcase.capitalization = pascal_case + +# Title : Avoid multiple blank lines +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2000 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE2000.severity = warning +dotnet_style_allow_multiple_blank_lines_experimental = false + +# Title : Embedded statements must be on their own line +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2001 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE2001.severity = warning + +# Title : Consecutive braces must not have blank line between them +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2002 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE2002.severity = warning + +# Title : Blank line required between block and subsequent statement +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2003 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE2003.severity = silent + +# Title : Blank line not allowed after constructor initializer colon +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2004 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE2004.severity = warning + +# Title : Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +# Category : Trimming +dotnet_diagnostic.IL2026.severity = warning + +# Title : The value passed as the assembly name or type name to the CreateInstance method can't be statically analyzed. +# Category : Trimming +dotnet_diagnostic.IL2032.severity = warning + +# Title : The 'DynamicallyAccessedMembersAttribute' is not allowed on methods. It is allowed on method return value or method parameters. +# Category : Trimming +dotnet_diagnostic.IL2041.severity = warning + +# Title : 'DynamicallyAccessedMembersAttribute' on property conflicts with the same attribute on its accessor. +# Category : Trimming +dotnet_diagnostic.IL2043.severity = warning + +# Title : 'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides. +# Category : Trimming +dotnet_diagnostic.IL2046.severity = warning + +# Title : Correctness of COM interop cannot be guaranteed after trimming. Interfaces and interface members might be removed. +# Category : Trimming +dotnet_diagnostic.IL2050.severity = warning + +# Title : Either the type on which the MakeGenericType is called can't be statically determined, or the type parameters to be used for generic arguments can't be statically determined. +# Category : Trimming +dotnet_diagnostic.IL2055.severity = warning + +# Title : Unrecognized value passed to the parameter of method. It's not possible to guarantee the availability of the target type. +# Category : Trimming +dotnet_diagnostic.IL2057.severity = warning + +# Title : Parameters passed to method cannot be analyzed. Consider using methods 'System.Type.GetType' and `System.Activator.CreateInstance` instead. +# Category : Trimming +dotnet_diagnostic.IL2058.severity = warning + +# Title : The type passed to the RunClassConstructor is not statically known, Trimmer can't make sure that its static constructor is available. +# Category : Trimming +dotnet_diagnostic.IL2059.severity = warning + +# Title : Call to 'System.Reflection.MethodInfo.MakeGenericMethod' can not be statically analyzed. It's not possible to guarantee the availability of requirements of the generic method. +# Category : Trimming +dotnet_diagnostic.IL2060.severity = warning + +# Title : The parameter of method has a DynamicallyAccessedMembersAttribute, but the value passed to it can not be statically analyzed. +# Category : Trimming +dotnet_diagnostic.IL2062.severity = warning + +# Title : The return value of method has a DynamicallyAccessedMembersAttribute, but the value returned from the method can not be statically analyzed. +# Category : Trimming +dotnet_diagnostic.IL2063.severity = warning + +# Title : The field has a DynamicallyAccessedMembersAttribute, but the value assigned to it can not be statically analyzed. +# Category : Trimming +dotnet_diagnostic.IL2064.severity = warning + +# Title : The method has a DynamicallyAccessedMembersAttribute (which applies to the implicit 'this' parameter), but the value used for the 'this' parameter can not be statically analyzed. +# Category : Trimming +dotnet_diagnostic.IL2065.severity = warning + +# Title : The generic parameter of type or method has a DynamicallyAccessedMembersAttribute, but the value used for it can not be statically analyzed. +# Category : Trimming +dotnet_diagnostic.IL2066.severity = warning + +# Title : Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The parameter of method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2067.severity = warning + +# Title : Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The parameter of method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2068.severity = warning + +# Title : Value stored in field does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The parameter of method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2069.severity = warning + +# Title : 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The parameter of method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2070.severity = warning + +# Title : Generic argument does not satisfy 'DynamicallyAccessedMembersAttribute' in target method or type. The parameter of method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2071.severity = warning + +# Title : Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2072.severity = warning + +# Title : Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The return value of the source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2073.severity = warning + +# Title : Value stored in field does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The return value of the source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2074.severity = warning + +# Title : 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2075.severity = warning + +# Title : Target generic argument does not satisfy 'DynamicallyAccessedMembersAttribute' in target method or type. The return value of the source method does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. +# Category : Trimming +dotnet_diagnostic.IL2076.severity = warning + +# Title : Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The source field does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2077.severity = warning + +# Title : Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The source field does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2078.severity = warning + +# Title : Value stored in target field does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The source field does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2079.severity = warning + +# Title : 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The source field does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2080.severity = warning + +# Title : Target generic argument does not satisfy 'DynamicallyAccessedMembersAttribute' in target method or type. The source field does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2081.severity = warning + +# Title : Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The implicit 'this' argument of source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2082.severity = warning + +# Title : Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The implicit 'this' argument of source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2083.severity = warning + +# Title : Value stored in target field does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The implicit 'this' argument of source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2084.severity = warning + +# Title : 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The implicit 'this' argument of source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2085.severity = warning + +# Title : Target generic argument does not satisfy 'DynamicallyAccessedMembersAttribute' in target method or type. The implicit 'this' argument of source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2086.severity = warning + +# Title : Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The generic parameter of the source method or type does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2087.severity = warning + +# Title : Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The generic parameter of the source method or type does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2088.severity = warning + +# Title : Value stored in target field does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The generic parameter of the source method or type does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2089.severity = warning + +# Title : 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The generic parameter of the source method or type does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2090.severity = warning + +# Title : Target generic argument does not satisfy 'DynamicallyAccessedMembersAttribute' in target method or type. The generic parameter of the source method or type does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2091.severity = warning + +# Title : 'DynamicallyAccessedMemberTypes' on the parameter of method don't match overridden parameter of method. All overridden members must have the same 'DynamicallyAccessedMembersAttribute' usage. +# Category : Trimming +dotnet_diagnostic.IL2092.severity = warning + +# Title : 'DynamicallyAccessedMemberTypes' on the return value of method don't match overridden return value of method. All overridden members must have the same 'DynamicallyAccessedMembersAttribute' usage. +# Category : Trimming +dotnet_diagnostic.IL2093.severity = warning + +# Title : 'DynamicallyAccessedMemberTypes' on the implicit 'this' parameter of method don't match overridden implicit 'this' parameter of method. All overridden members must have the same 'DynamicallyAccessedMembersAttribute' usage. +# Category : Trimming +dotnet_diagnostic.IL2094.severity = warning + +# Title : 'DynamicallyAccessedMemberTypes' on the generic parameter of method or type don't match overridden generic parameter method or type. All overridden members must have the same 'DynamicallyAccessedMembersAttribute' usage. +# Category : Trimming +dotnet_diagnostic.IL2095.severity = warning + +# Title : Call to 'Type.GetType' method can perform case insensitive lookup of the type, currently ILLink can not guarantee presence of all the matching types. +# Category : Trimming +dotnet_diagnostic.IL2096.severity = warning + +# Title : Field has 'DynamicallyAccessedMembersAttribute', but that attribute can only be applied to fields of type 'System.Type' or 'System.String'. +# Category : Trimming +dotnet_diagnostic.IL2097.severity = warning + +# Title : Parameter of method has 'DynamicallyAccessedMembersAttribute', but that attribute can only be applied to parameters of type 'System.Type' or 'System.String'. +# Category : Trimming +dotnet_diagnostic.IL2098.severity = warning + +# Title : Property has 'DynamicallyAccessedMembersAttribute', but that attribute can only be applied to properties of type 'System.Type' or 'System.String'. +# Category : Trimming +dotnet_diagnostic.IL2099.severity = warning + +# Title : Value passed to the parameter of method cannot be statically determined as a property accessor. +# Category : Trimming +dotnet_diagnostic.IL2103.severity = warning + +# Title : Return type of method has 'DynamicallyAccessedMembersAttribute', but that attribute can only be applied to properties of type 'System.Type' or 'System.String'. +# Category : Trimming +dotnet_diagnostic.IL2106.severity = warning + +# Title : Types that derive from a base class with 'RequiresUnreferencedCodeAttribute' need to explicitly use the 'RequiresUnreferencedCodeAttribute' or suppress this warning +# Category : Trimming +dotnet_diagnostic.IL2109.severity = warning + +# Title : Field with 'DynamicallyAccessedMembersAttribute' is accessed via reflection. Trimmer can't guarantee availability of the requirements of the field. +# Category : Trimming +dotnet_diagnostic.IL2110.severity = warning + +# Title : Method with parameters or return value with `DynamicallyAccessedMembersAttribute` is accessed via reflection. Trimmer can't guarantee availability of the requirements of the method. +# Category : Trimming +dotnet_diagnostic.IL2111.severity = warning + +# Title : The use of 'RequiresUnreferencedCodeAttribute' on static constructors is disallowed since is a method not callable by the user, is only called by the runtime. Placing the attribute directly on the static constructor will have no effect, instead use 'RequiresUnreferencedCodeAttribute' on the type which will handle warning and silencing from the static constructor. +# Category : Trimming +dotnet_diagnostic.IL2116.severity = warning + +# Title : Avoid accessing Assembly file path when publishing as a single file +# Category : SingleFile +dotnet_diagnostic.IL3000.severity = warning + +# Title : Avoid using accessing Assembly file path when publishing as a single-file +# Category : Publish +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/il3000 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.IL3000.severity = warning + +# Title : Avoid accessing Assembly file path when publishing as a single file +# Category : SingleFile +dotnet_diagnostic.IL3001.severity = warning + +# Title : Avoid using accessing Assembly file path when publishing as a single-file +# Category : Publish +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/il3001 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.IL3001.severity = warning + +# Title : Avoid calling members marked with 'RequiresAssemblyFilesAttribute' when publishing as a single-file +# Category : SingleFile +dotnet_diagnostic.IL3002.severity = warning + +# Title : 'RequiresAssemblyFilesAttribute' annotations must match across all interface implementations or overrides. +# Category : SingleFile +dotnet_diagnostic.IL3003.severity = warning + +# Title : The use of 'RequiresAssemblyFilesAttribute' on static constructors is disallowed since is a method not callable by the user, is only called by the runtime. Placing the attribute directly on the static constructor will have no effect, instead use 'RequiresUnreferencedCodeAttribute' on the type which will handle warning and silencing from the static constructor. +# Category : SingleFile +dotnet_diagnostic.IL3004.severity = warning + +# Title : Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. +# Category : AOT +dotnet_diagnostic.IL3050.severity = warning + +# Title : 'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides. +# Category : AOT +dotnet_diagnostic.IL3051.severity = warning + +# Title : The use of 'RequiresDynamicCodeAttribute' on static constructors is disallowed since is a method not callable by the user, is only called by the runtime. Placing the attribute directly on the static constructor will have no effect, instead use 'RequiresUnreferencedCodeAttribute' on the type which will handle warning and silencing from the static constructor. +# Category : AOT +dotnet_diagnostic.IL3056.severity = warning + +# Title : Use source generated logging methods for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a000 +dotnet_diagnostic.R9A000.severity = warning + +# Title : Use 'Microsoft.IO.RecyclableMemoryStream' instead of 'System.IO.MemoryStream' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a001 +dotnet_diagnostic.R9A001.severity = warning + +# Title : Use higher performance methods from 'IExtendedDistributedCache' +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a003 +dotnet_diagnostic.R9A003.severity = warning + +# Title : Use the 'Microsoft.R9.Extensions.Caching.Redis' package instead of 'Microsoft.Extensions.Caching.StackExchangeRedis' +# Category : Resilience +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a005 +dotnet_diagnostic.R9A005.severity = warning + +# Title : Update return type to match metric type +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a006 +dotnet_diagnostic.R9A006.severity = error + +# Title : Remove method body +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a007 +dotnet_diagnostic.R9A007.severity = error + +# Title : Add a parameter of type 'IMeter' to the method declaration +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a008 +dotnet_diagnostic.R9A008.severity = error + +# Title : Update method parameters for dimensions to be string type +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a009 +dotnet_diagnostic.R9A009.severity = error + +# Title : Make method static +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a010 +dotnet_diagnostic.R9A010.severity = error + +# Title : Make method partial +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a011 +dotnet_diagnostic.R9A011.severity = error + +# Title : Make method public +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a012 +dotnet_diagnostic.R9A012.severity = warning + +# Title : Seal non-public classes for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a013 +dotnet_diagnostic.R9A013.severity = warning + +# Title : Use the 'Microsoft.R9.Extensions.Diagnostics.Throws' class instead of explicitly throwing exception for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a014 +dotnet_diagnostic.R9A014.severity = warning + +# Title : Use the 'Microsoft.R9.Extensions.Diagnostics.Throws' class instead of explicitly throwing exception for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a015 +dotnet_diagnostic.R9A015.severity = warning + +# Title : Use eager options validation +# Category : Reliability +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a016 +dotnet_diagnostic.R9A016.severity = warning + +# Title : Use asynchronous operations instead of legacy thread blocking code +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a017 +dotnet_diagnostic.R9A017.severity = warning + +# Title : Use 'Microsoft.R9.Extensions.Text.CompositeFormat' instead of 'string.Format' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a018 +dotnet_diagnostic.R9A018.severity = warning + +# Title : Remove unnecessary dictionary lookups +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a019 +dotnet_diagnostic.R9A019.severity = warning + +# Title : Remove unnecessary set lookups +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a020 +dotnet_diagnostic.R9A020.severity = warning + +# Title : Perform message formatting in the body of the logging method +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a021 +dotnet_diagnostic.R9A021.severity = warning + +# Title : Use 'System.TimeProvider' to make the code easier to test +# Category : Reliability +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a022 +dotnet_diagnostic.R9A022.severity = warning + +# Title : Use 'Microsoft.R9.Extensions.Time.PerfStopwatch' instead of 'System.Diagnostics.Stopwatch' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a023 +dotnet_diagnostic.R9A023.severity = warning + +# Title : Propagate data classification +# Category : Privacy +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a024 +dotnet_diagnostic.R9A024.severity = warning + +# Title : Use fixed format for 'System.ObsoleteAttribute' message +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a025 +dotnet_diagnostic.R9A025.severity = warning + +# Title : Minimum deprecation period for an obsolete public API +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a026 +dotnet_diagnostic.R9A026.severity = warning + +# Title : Use fixed API surface format for soft deleted members +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a027 +dotnet_diagnostic.R9A027.severity = warning + +# Title : Argument provided for user input parameter on user data vending API is not from any request context +# Category : Privacy +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a028 +dotnet_diagnostic.R9A028.severity = warning + +# Title : Using experimental API +# Category : Reliability +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a029 +dotnet_diagnostic.R9A029.severity = none + +# Title : Use the character-based overloads of 'String.StartsWith' or 'String.EndsWith' +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a030 +dotnet_diagnostic.R9A030.severity = warning + +# Title : Make types declared in an executable internal +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a031 +dotnet_diagnostic.R9A031.severity = warning + +# Title : Consider using an array instead of a collection +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a032 +dotnet_diagnostic.R9A032.severity = none + +# Title : Replace uses of 'Enum.GetName' and 'Enum.ToString' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a033 +dotnet_diagnostic.R9A033.severity = warning + +# Title : Optimize method group use to avoid allocations +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a034 +dotnet_diagnostic.R9A034.severity = warning + +# Title : Make struct readonly +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a035 +dotnet_diagnostic.R9A035.severity = warning + +# Title : Use 'Microsoft.R9.Extensions.Text.NumericExtensions.ToInvariantString' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a036 +dotnet_diagnostic.R9A036.severity = warning + +# Title : Use 'System.ValueTuple' instead of 'System.Tuple' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a037 +dotnet_diagnostic.R9A037.severity = warning + +# Title : Use 'Microsoft.R9.Extensions.Pools.PoolFactory' instead for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a038 +dotnet_diagnostic.R9A038.severity = warning + +# Title : Remove superfluous null checks when compiling in a nullable context +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a039 +dotnet_diagnostic.R9A039.severity = none + +# Title : Use generic collections instead of legacy collections for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a040 +dotnet_diagnostic.R9A040.severity = warning + +# Title : Use concrete types when possible for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a041 +dotnet_diagnostic.R9A041.severity = warning + +# Title : Annotate all User Data APIs parameters +# Category : Privacy +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a042 +dotnet_diagnostic.R9A042.severity = warning + +# Title : Use 'Microsoft.R9.Extensions.Text.StringSplitExtensions.TrySplit' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a043 +dotnet_diagnostic.R9A043.severity = warning + +# Title : Assign array of literal values to a static field for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a044 +dotnet_diagnostic.R9A044.severity = warning + +# Title : Use 'Array.Empty' instead of allocating a 0-element array for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a045 +dotnet_diagnostic.R9A045.severity = warning + +# Title : Source generated metrics (fast metrics) should be located in 'Metric' class +# Category : Readability +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a046 +dotnet_diagnostic.R9A046.severity = warning + +# Title : Do not use manual metrics, use fast (source generated) metrics instead for better performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a047 +dotnet_diagnostic.R9A047.severity = warning + +# Title : Use the 'Count' or 'Length' properties instead of the 'Any' method for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a048 +dotnet_diagnostic.R9A048.severity = warning + +# Title : Newly added API must be annotated with experimental attribute +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a049 +dotnet_diagnostic.R9A049.severity = warning + +# Title : An experimental API was marked as obsolete +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a050 +dotnet_diagnostic.R9A050.severity = warning + +# Title : A stable API was marked as experimental +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a051 +dotnet_diagnostic.R9A051.severity = warning + +# Title : A stable API was deleted outside the deprecation period +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a052 +dotnet_diagnostic.R9A052.severity = warning + +# Title : A deprecated API is not annotated with the obsolete attribute +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a053 +dotnet_diagnostic.R9A053.severity = warning + +# Title : A deprecated API is marked as experimental +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a054 +dotnet_diagnostic.R9A054.severity = warning + +# Title : The signature of a stable API has changed +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a055 +dotnet_diagnostic.R9A055.severity = none + +# Title : Fire-and-forget async call inside a 'using' block +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a056 +dotnet_diagnostic.R9A056.severity = warning + +# Title : Use consistent versions of R9 assemblies +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a057 +dotnet_diagnostic.R9A057.severity = warning + +# Title : Consider removing unnecessary conditional access operator (?) +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a058 +dotnet_diagnostic.R9A058.severity = suggestion + +# Title : Consider removing unnecessary null coalescing assignment (??=) +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a059 +dotnet_diagnostic.R9A059.severity = suggestion + +# Title : Consider removing unnecessary null coalescing operator (??) +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a060 +dotnet_diagnostic.R9A060.severity = suggestion + +# Title : The async method doesn't support cancellation +# Category : Resilience +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a061 +dotnet_diagnostic.R9A061.severity = warning + +# Title : +# Category : Style +# Tags : Telemetry, EnforceOnBuild_Never, NotConfigurable +dotnet_diagnostic.RemoveUnnecessaryImportsFixable.severity = silent + +# Title : Methods and properties should be named in PascalCase +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-100 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S100.severity = none + +# Title : Method overrides should not change parameter defaults +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1006 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1006.severity = warning + +# Title : Types should be named in PascalCase +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-101 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S101.severity = none + +# Title : Lines should not be too long +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-103 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S103.severity = warning + +# Title : Files should not have too many lines of code +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-104 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S104.severity = warning + +# Title : Destructors should not throw exceptions +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1048 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1048.severity = none + +# Title : Tabulation characters should not be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-105 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S105.severity = none + +# Title : Standard outputs should not be used directly to log anything +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-106 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S106.severity = warning + +# Title : Collapsible "if" statements should be merged +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1066 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1066.severity = none + +# Title : Expressions should not be too complex +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1067 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1067.severity = warning + +# Title : Methods should not have too many parameters +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-107 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S107.severity = warning + +# Title : URIs should not be hardcoded +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1075 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1075.severity = warning + +# Title : Nested blocks of code should not be left empty +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-108 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S108.severity = warning + +# Title : Magic numbers should not be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-109 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S109.severity = warning + +# Title : Inheritance tree of classes should not be too deep +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-110 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S110.severity = warning + +# Title : Fields should not have public accessibility +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1104 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1104.severity = none + +# Title : A close curly brace should be located at the beginning of a line +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1109 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1109.severity = none + +# Title : Redundant pairs of parentheses should be removed +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1110 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1110.severity = none + +# Title : Empty statements should be removed +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1116 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1116.severity = none + +# Title : Local variables should not shadow class fields +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1117 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1117.severity = warning + +# Title : Utility classes should not have public constructors +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1118 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1118.severity = none + +# Title : General exceptions should never be thrown +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-112 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S112.severity = none + +# Title : Assignments should not be made from within sub-expressions +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1121 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1121.severity = warning + +# Title : "Obsolete" attributes should include explanations +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1123 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1123.severity = none + +# Title : Boolean literals should not be redundant +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1125 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1125.severity = warning + +# Title : Unused "using" should be removed +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1128 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1128.severity = warning + +# Title : Files should contain an empty newline at the end +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-113 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S113.severity = none + +# Title : Track uses of "FIXME" tags +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1134 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1134.severity = warning + +# Title : Track uses of "TODO" tags +# Category : Info Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1135 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1135.severity = warning + +# Title : Unused private types or members should be removed +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1144 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay, Unnecessary +dotnet_diagnostic.S1144.severity = warning + +# Title : Useless "if(true) {...}" and "if(false){...}" blocks should be removed +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1145 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1145.severity = warning + +# Title : Exit methods should not be called +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1147 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1147.severity = warning + +# Title : "switch case" clauses should not have too many lines of code +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1151 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1151.severity = none + +# Title : "Any()" should be used to test for emptiness +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1155 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1155.severity = warning + +# Title : Exceptions should not be thrown in finally blocks +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1163 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1163.severity = none + +# Title : Empty arrays and collections should be returned instead of null +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1168 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1168.severity = warning + +# Title : Unused method parameters should be removed +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1172 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1172.severity = none + +# Title : Overriding members should do more than simply call the same member in the base class +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1185 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1185.severity = warning + +# Title : Methods should not be empty +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1186 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1186.severity = warning + +# Title : String literals should not be duplicated +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1192 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1192.severity = suggestion + +# Title : Nested code blocks should not be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1199 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1199.severity = warning + +# Title : Classes should not be coupled to too many other classes (Single Responsibility Principle) +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1200 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1200.severity = none + +# Title : "Equals(Object)" and "GetHashCode()" should be overridden in pairs +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1206 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1206.severity = none + +# Title : Control structures should use curly braces +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-121 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S121.severity = none + +# Title : "Equals" and the comparison operators should be overridden when implementing "IComparable" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1210 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1210.severity = none + +# Title : "GC.Collect" should not be called +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1215 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1215.severity = warning + +# Title : Statements should be on separate lines +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-122 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S122.severity = none + +# Title : Method parameters, caught exceptions and foreach variables' initial values should not be ignored +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1226 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1226.severity = warning + +# Title : break statements should not be used except for switch cases +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1227 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1227.severity = none + +# Title : Floating point numbers should not be tested for equality +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1244 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1244.severity = error + +# Title : Sections of code should not be commented out +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-125 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S125.severity = warning + +# Title : "if ... else if" constructs should end with "else" clauses +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-126 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S126.severity = none + +# Title : A "while" loop should be used instead of a "for" loop +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1264 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1264.severity = warning + +# Title : "for" loop stop conditions should be invariant +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-127 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S127.severity = warning + +# Title : "switch" statements should have at least 3 "case" clauses +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1301 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1301.severity = none + +# Title : Track uses of in-source issue suppressions +# Category : Info Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1309 +# Tags : C#, MainSourceScope, TestSourceScope +# Comment : Suppressions are frequently necessary. +dotnet_diagnostic.S1309.severity = none + +# Title : "switch/Select" statements should contain a "default/Case Else" clauses +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-131 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S131.severity = none + +# Title : Using hardcoded IP addresses is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1313 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1313.severity = warning + +# Title : Control flow statements "if", "switch", "for", "foreach", "while", "do" and "try" should not be nested too deeply +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-134 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S134.severity = none + +# Title : Functions should not have too many lines of code +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-138 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S138.severity = none + +# Title : Culture should be specified for "string" operations +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1449 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1449.severity = warning + +# Title : Private fields only used as local variables in methods should become local variables +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1450 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1450.severity = warning + +# Title : Track lack of copyright and license headers +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1451 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1451.severity = none + +# Title : "switch" statements should not have too many "case" clauses +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1479 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1479.severity = warning + +# Title : Unused local variables should be removed +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1481 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1481.severity = none + +# Title : Methods and properties should not be too complex +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1541 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1541.severity = none + +# Title : Tests should not be ignored +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1607 +# Tags : C#, TestSourceScope, SonarWay +dotnet_diagnostic.S1607.severity = warning + +# Title : Strings should not be concatenated using '+' in a loop +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1643 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1643.severity = warning + +# Title : Variables should not be self-assigned +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1656 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1656.severity = warning + +# Title : Multiple variables should not be declared on the same line +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1659 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1659.severity = warning + +# Title : An abstract class should have both abstract and concrete methods +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1694 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1694.severity = warning + +# Title : NullReferenceException should not be caught +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1696 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1696.severity = warning + +# Title : Short-circuit logic should be used to prevent null pointer dereferences in conditionals +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1697 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1697.severity = warning + +# Title : "==" should not be used when "Equals" is overridden +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1698 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1698.severity = warning + +# Title : Constructors should only call non-overridable methods +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1699 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1699.severity = warning + +# Title : Loops with at most one iteration should be refactored +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1751 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1751.severity = warning + +# Title : Identical expressions should not be used on both sides of a binary operator +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1764 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1764.severity = warning + +# Title : "switch" statements should not be nested +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1821 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1821.severity = none + +# Title : Objects should not be created to be dropped immediately without being used +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1848 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1848.severity = warning + +# Title : Unused assignments should be removed +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1854 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1854.severity = none + +# Title : "ToString()" calls should not be redundant +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1858 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1858.severity = warning + +# Title : Related "if/else if" statements should not have the same condition +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1862 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1862.severity = warning + +# Title : Two branches in a conditional structure should not have exactly the same implementation +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1871 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1871.severity = warning + +# Title : Redundant casts should not be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1905 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1905.severity = none + +# Title : Inheritance list should not be redundant +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1939 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1939.severity = warning + +# Title : Boolean checks should not be inverted +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1940 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1940.severity = warning + +# Title : Inappropriate casts should not be made +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1944 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1944.severity = warning + +# Title : "for" loop increment clauses should modify the loops' counters +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1994 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1994.severity = warning + +# Title : Hashes should include an unpredictable salt +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2053 +# Tags : C#, MainSourceScope, SonarWay +# Comment : Analysis is too slow +dotnet_diagnostic.S2053.severity = none + +# Title : Hard-coded credentials are security-sensitive +# Category : Blocker Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2068 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2068.severity = warning + +# Title : SHA-1 and Message-Digest hash algorithms should not be used in secure contexts +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2070 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2070.severity = none + +# Title : Formatting SQL queries is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2077 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2077.severity = warning + +# Title : Creating cookies without the "secure" flag is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2092 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2092.severity = warning + +# Title : Collections should not be passed as arguments to their own methods +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2114 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2114.severity = warning + +# Title : A secure password should be used when connecting to a database +# Category : Blocker Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2115 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2115.severity = warning + +# Title : Values should not be uselessly incremented +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2123 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2123.severity = warning + +# Title : Underscores should be used to make large numbers readable +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2148 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2148.severity = warning + +# Title : "sealed" classes should not have "protected" members +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2156 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2156.severity = warning + +# Title : Short-circuit logic should be used in boolean contexts +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2178 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2178.severity = warning + +# Title : Integral numbers should not be shifted by zero or more than their number of bits-1 +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2183 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2183.severity = warning + +# Title : Results of integer division should not be assigned to floating point variables +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2184 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2184.severity = warning + +# Title : TestCases should contain tests +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2187 +# Tags : C#, TestSourceScope, SonarWay +dotnet_diagnostic.S2187.severity = warning + +# Title : Recursion should not be infinite +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2190 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2190.severity = warning + +# Title : Modulus results should not be checked for direct equality +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2197 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2197.severity = warning + +# Title : Return values from functions without side effects should not be ignored +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2201 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2201.severity = none + +# Title : Runtime type checking should be simplified +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2219 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2219.severity = warning + +# Title : "Exception" should not be caught +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2221 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2221.severity = none + +# Title : Locks should be released on all paths +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2222 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2222.severity = warning + +# Title : Non-constant static fields should not be visible +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2223 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2223.severity = warning + +# Title : "ToString()" method should not return null +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2225 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2225.severity = warning + +# Title : Console logging should not be used +# Category : Minor Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2228 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2228.severity = warning + +# Title : Parameters should be passed in the correct order +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2234 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2234.severity = warning + +# Title : Using pseudorandom number generators (PRNGs) is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2245 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2245.severity = warning + +# Title : A "for" loop update clause should move the counter in the right direction +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2251 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2251.severity = warning + +# Title : For-loop conditions should be true at least once +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2252 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2252.severity = warning + +# Title : Writing cookies is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2255 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2255.severity = warning + +# Title : Using non-standard cryptographic algorithms is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2257 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2257.severity = warning + +# Title : Null pointers should not be dereferenced +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2259 +# Tags : C#, MainSourceScope, SonarWay +# Comment : Redundant, covered by modern C# compiler +dotnet_diagnostic.S2259.severity = none + +# Title : Composite format strings should not lead to unexpected behavior at runtime +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2275 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2275.severity = warning + +# Title : Neither DES (Data Encryption Standard) nor DESede (3DES) should be used +# Category : Blocker Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2278 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2278.severity = warning + +# Title : Field-like events should not be virtual +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2290 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2290.severity = warning + +# Title : Overflow checking should not be disabled for "Enumerable.Sum" +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2291 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2291.severity = warning + +# Title : Trivial properties should be auto-implemented +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2292 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2292.severity = none + +# Title : "nameof" should be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2302 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2302.severity = warning + +# Title : "async" and "await" should not be used as identifiers +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2306 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2306.severity = warning + +# Title : Methods and properties that don't access instance data should be static +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2325 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2325.severity = none + +# Title : Unused type parameters should be removed +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2326 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +# Comment : Valid pattern used in a number of places +dotnet_diagnostic.S2326.severity = none + +# Title : "try" statements with identical "catch" and/or "finally" blocks should be merged +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2327 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2327.severity = warning + +# Title : "GetHashCode" should not reference mutable fields +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2328 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2328.severity = warning + +# Title : Array covariance should not be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2330 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2330.severity = warning + +# Title : Redundant modifiers should not be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2333 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2333.severity = warning + +# Title : Public constant members should not be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2339 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2339.severity = none + +# Title : Enumeration types should comply with a naming convention +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2342 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2342.severity = none + +# Title : Enumeration type names should not have "Flags" or "Enum" suffixes +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2344 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2344.severity = warning + +# Title : Flags enumerations should explicitly initialize all their members +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2345 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2345.severity = warning + +# Title : Flags enumerations zero-value members should be named "None" +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2346 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2346.severity = none + +# Title : Fields should be private +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2357 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2357.severity = none + +# Title : Optional parameters should not be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2360 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2360.severity = none + +# Title : Properties should not make collection or array copies +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2365 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2365.severity = warning + +# Title : Public methods should not have multidimensional array parameters +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2368 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2368.severity = warning + +# Title : Exceptions should not be thrown from property getters +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2372 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2372.severity = warning + +# Title : Write-only properties should not be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2376 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2376.severity = warning + +# Title : Mutable fields should not be "public static" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2386 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2386.severity = warning + +# Title : Child class fields should not shadow parent class fields +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2387 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2387.severity = warning + +# Title : Types and methods should not have too many generic parameters +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2436 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2436.severity = warning + +# Title : Silly bit operations should not be performed +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2437 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay, Unnecessary +dotnet_diagnostic.S2437.severity = warning + +# Title : Whitespace and control characters in string literals should be explicit +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2479 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2479.severity = warning + +# Title : Generic exceptions should not be ignored +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2486 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2486.severity = warning + +# Title : Shared resources should not be used for locking +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2551 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2551.severity = warning + +# Title : Conditionally executed code should be reachable +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2583 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2583.severity = none + +# Title : Boolean expressions should not be gratuitous +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2589 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2589.severity = warning + +# Title : Setting loose file permissions is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2612 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2612.severity = warning + +# Title : The length returned from a stream read should be checked +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2674 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2674.severity = warning + +# Title : Multiline blocks should be enclosed in curly braces +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2681 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2681.severity = warning + +# Title : "NaN" should not be used in comparisons +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2688 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2688.severity = warning + +# Title : "IndexOf" checks should not be for positive numbers +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2692 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2692.severity = warning + +# Title : Instance members should not write to "static" fields +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2696 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2696.severity = warning + +# Title : Tests should include assertions +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2699 +# Tags : C#, TestSourceScope, SonarWay +dotnet_diagnostic.S2699.severity = warning + +# Title : Literal boolean values should not be used in assertions +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2701 +# Tags : C#, TestSourceScope +dotnet_diagnostic.S2701.severity = warning + +# Title : "catch" clauses should do more than rethrow +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2737 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2737.severity = warning + +# Title : Static fields should not be used in generic types +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2743 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2743.severity = none + +# Title : XML parsers should not be vulnerable to XXE attacks +# Category : Blocker Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2755 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2755.severity = warning + +# Title : "=+" should not be used instead of "+=" +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2757 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2757.severity = warning + +# Title : The ternary operator should not return the same value regardless of the condition +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2758 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2758.severity = warning + +# Title : Sequential tests should not check the same condition +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2760 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2760.severity = warning + +# Title : Doubled prefix operators "!!" and "~~" should not be used +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2761 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2761.severity = warning + +# Title : SQL keywords should be delimited by whitespace +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2857 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2857.severity = warning + +# Title : "IDisposables" should be disposed +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2930 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +# Comment : Duplicate, see CA2000 +dotnet_diagnostic.S2930.severity = none + +# Title : Classes with "IDisposable" members should implement "IDisposable" +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2931 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2931.severity = warning + +# Title : Fields that are only assigned in the constructor should be "readonly" +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2933 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2933.severity = none + +# Title : Property assignments should not be made for "readonly" fields not constrained to reference types +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2934 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2934.severity = warning + +# Title : Classes should "Dispose" of members from the classes' own "Dispose" methods +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2952 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2952.severity = warning + +# Title : Methods named "Dispose" should implement "IDisposable.Dispose" +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2953 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2953.severity = warning + +# Title : Generic parameters not constrained to reference types should not be compared to "null" +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2955 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2955.severity = warning + +# Title : "IEnumerable" LINQs should be simplified +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2971 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2971.severity = warning + +# Title : "Object.ReferenceEquals" should not be used for value types +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2995 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2995.severity = warning + +# Title : "ThreadStatic" fields should not be initialized +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2996 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2996.severity = warning + +# Title : "IDisposables" created in a "using" statement should not be returned +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2997 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2997.severity = warning + +# Title : "ThreadStatic" should not be used on non-static fields +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3005 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3005.severity = warning + +# Title : Static fields should not be updated in constructors +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3010 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3010.severity = warning + +# Title : Reflection should not be used to increase accessibility of classes, methods, or fields +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3011 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3011.severity = warning + +# Title : Members should not be initialized to default values +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3052 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3052.severity = none + +# Title : Types should not have members with visibility set higher than the type's visibility +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3059 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3059.severity = none + +# Title : "is" should not be used with "this" +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3060 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3060.severity = warning + +# Title : "async" methods should not return "void" +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3168 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3168.severity = none + +# Title : Multiple "OrderBy" calls should not be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3169 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3169.severity = warning + +# Title : Delegates should not be subtracted +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3172 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3172.severity = warning + +# Title : "interface" instances should not be cast to concrete types +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3215 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3215.severity = warning + +# Title : "ConfigureAwait(false)" should be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3216 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3216.severity = none + +# Title : "Explicit" conversions of "foreach" loops should not be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3217 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3217.severity = warning + +# Title : Inner class members should not shadow outer class "static" or type members +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3218 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3218.severity = warning + +# Title : Method calls should not resolve ambiguously to overloads with "params" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3220 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3220.severity = warning + +# Title : "GC.SuppressFinalize" should not be invoked for types without destructors +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3234 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3234.severity = warning + +# Title : Redundant parentheses should not be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3235 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3235.severity = warning + +# Title : Caller information arguments should not be provided explicitly +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3236 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3236.severity = warning + +# Title : "value" parameters should be used +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3237 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3237.severity = warning + +# Title : The simplest possible condition syntax should be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3240 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3240.severity = none + +# Title : Methods should not return values that are never used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3241 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3241.severity = warning + +# Title : Method parameters should be declared with base types +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3242 +# Tags : C#, MainSourceScope, TestSourceScope +# Comment : We want to encourage concrete types instead of interface types when possible as it's considerably faster. +dotnet_diagnostic.S3242.severity = none + +# Title : Anonymous delegates should not be used to unsubscribe from Events +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3244 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3244.severity = warning + +# Title : Generic type parameters should be co/contravariant when possible +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3246 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3246.severity = warning + +# Title : Duplicate casts should not be made +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3247 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3247.severity = warning + +# Title : Classes directly extending "object" should not call "base" in "GetHashCode" or "Equals" +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3249 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3249.severity = warning + +# Title : Implementations should be provided for "partial" methods +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3251 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3251.severity = warning + +# Title : Constructor and destructor declarations should not be redundant +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3253 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3253.severity = warning + +# Title : Default parameter values should not be passed as arguments +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3254 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3254.severity = warning + +# Title : "string.IsNullOrEmpty" should be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3256 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3256.severity = warning + +# Title : Declarations and initializations should be as concise as possible +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3257 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3257.severity = warning + +# Title : Non-derived "private" classes and records should be "sealed" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3260 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3260.severity = none + +# Title : Namespaces should not be empty +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3261 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3261.severity = warning + +# Title : "params" should be used on overrides +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3262 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3262.severity = warning + +# Title : Static fields should appear in the order they must be initialized +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3263 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3263.severity = warning + +# Title : Events should be invoked +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3264 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3264.severity = warning + +# Title : Non-flags enums should not be used in bitwise operations +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3265 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3265.severity = warning + +# Title : Loops should be simplified with "LINQ" expressions +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3267 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3267.severity = none + +# Title : Cipher Block Chaining IVs should be unpredictable +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3329 +# Tags : C#, MainSourceScope, SonarWay +# Comment : Analysis is too slow +dotnet_diagnostic.S3329.severity = none + +# Title : Creating cookies without the "HttpOnly" flag is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3330 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3330.severity = warning + +# Title : Caller information parameters should come at the end of the parameter list +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3343 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3343.severity = warning + +# Title : Expressions used in "Debug.Assert" should not produce side effects +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3346 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3346.severity = warning + +# Title : Unchanged local variables should be "const" +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3353 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3353.severity = warning + +# Title : Ternary operators should not be nested +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3358 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3358.severity = warning + +# Title : "this" should not be exposed from constructors +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3366 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3366.severity = warning + +# Title : Attribute, EventArgs, and Exception type names should end with the type being extended +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3376 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3376.severity = none + +# Title : "base.Equals" should not be used to check for reference equality in "Equals" if "base" is not "object" +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3397 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3397.severity = warning + +# Title : Methods should not return constants +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3400 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3400.severity = warning + +# Title : Assertion arguments should be passed in the correct order +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3415 +# Tags : C#, TestSourceScope, SonarWay +dotnet_diagnostic.S3415.severity = warning + +# Title : Method overloads with default parameter values should not overlap +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3427 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3427.severity = warning + +# Title : "[ExpectedException]" should not be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3431 +# Tags : C#, TestSourceScope +dotnet_diagnostic.S3431.severity = warning + +# Title : Test method signatures should be correct +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3433 +# Tags : C#, TestSourceScope, SonarWay +dotnet_diagnostic.S3433.severity = warning + +# Title : Variables should not be checked against the values they're about to be assigned +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3440 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3440.severity = warning + +# Title : Redundant property names should be omitted in anonymous classes +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3441 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3441.severity = warning + +# Title : "abstract" classes should not have "public" constructors +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3442 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3442.severity = warning + +# Title : Type should not be examined on "System.Type" instances +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3443 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3443.severity = warning + +# Title : Interfaces should not simply inherit from base interfaces with colliding members +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3444 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3444.severity = warning + +# Title : Exceptions should not be explicitly rethrown +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3445 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3445.severity = warning + +# Title : "[Optional]" should not be used on "ref" or "out" parameters +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3447 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3447.severity = warning + +# Title : Right operands of shift operators should be integers +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3449 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3449.severity = warning + +# Title : Parameters with "[DefaultParameterValue]" attributes should also be marked "[Optional]" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3450 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3450.severity = warning + +# Title : "[DefaultValue]" should not be used when "[DefaultParameterValue]" is meant +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3451 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3451.severity = warning + +# Title : Classes should not have only "private" constructors +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3453 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3453.severity = warning + +# Title : "string.ToCharArray()" and "ReadOnlySpan.ToArray()" should not be called redundantly +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3456 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3456.severity = warning + +# Title : Composite format strings should be used correctly +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3457 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3457.severity = warning + +# Title : Empty "case" clauses that fall through to the "default" should be omitted +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3458 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3458.severity = warning + +# Title : Unassigned members should be removed +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3459 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3459.severity = warning + +# Title : Type inheritance should not be recursive +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3464 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3464.severity = warning + +# Title : Optional parameters should be passed to "base" calls +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3466 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3466.severity = warning + +# Title : Empty "default" clauses should be removed +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3532 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3532.severity = warning + +# Title : "ServiceContract" and "OperationContract" attributes should be used together +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3597 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3597.severity = warning + +# Title : One-way "OperationContract" methods should have "void" return type +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3598 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3598.severity = warning + +# Title : "params" should not be introduced on overrides +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3600 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3600.severity = warning + +# Title : Methods with "Pure" attribute should return a value +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3603 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3603.severity = warning + +# Title : Member initializer values should not be redundant +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3604 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3604.severity = warning + +# Title : Nullable type comparison should not be redundant +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3610 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3610.severity = warning + +# Title : Jump statements should not be redundant +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3626 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3626.severity = warning + +# Title : Empty nullable value should not be accessed +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3655 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3655.severity = warning + +# Title : Exception constructors should not throw exceptions +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3693 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3693.severity = warning + +# Title : Track use of "NotImplementedException" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3717 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3717.severity = warning + +# Title : Cognitive Complexity of methods should not be too high +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3776 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +# Comment : Code gets complicated +dotnet_diagnostic.S3776.severity = none + +# Title : "SafeHandle.DangerousGetHandle" should not be called +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3869 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3869.severity = warning + +# Title : Exception types should be "public" +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3871 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3871.severity = none + +# Title : Parameter names should not duplicate the names of their methods +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3872 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3872.severity = warning + +# Title : "out" and "ref" parameters should not be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3874 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3874.severity = none + +# Title : "operator==" should not be overloaded on reference types +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3875 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3875.severity = warning + +# Title : Strings or integral types should be used for indexers +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3876 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3876.severity = warning + +# Title : Exceptions should not be thrown from unexpected methods +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3877 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3877.severity = warning + +# Title : Finalizers should not be empty +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3880 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3880.severity = warning + +# Title : "IDisposable" should be implemented correctly +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3881 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3881.severity = none + +# Title : "CoSetProxyBlanket" and "CoInitializeSecurity" should not be used +# Category : Blocker Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3884 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3884.severity = warning + +# Title : "Assembly.Load" should be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3885 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3885.severity = warning + +# Title : Mutable, non-private fields should not be "readonly" +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3887 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3887.severity = warning + +# Title : Neither "Thread.Resume" nor "Thread.Suspend" should be used +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3889 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3889.severity = warning + +# Title : Classes that provide "Equals()" should implement "IEquatable" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3897 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3897.severity = warning + +# Title : Value types should implement "IEquatable" +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3898 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3898.severity = none + +# Title : Arguments of public methods should be validated against null +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3900 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3900.severity = none + +# Title : "Assembly.GetExecutingAssembly" should not be called +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3902 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3902.severity = warning + +# Title : Types should be defined in named namespaces +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3903 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +# Comment : Doesn't work with file-scoped namespaces, so disabling for now. +dotnet_diagnostic.S3903.severity = none + +# Title : Assemblies should have version information +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3904 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3904.severity = warning + +# Title : Event Handlers should have the correct signature +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3906 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3906.severity = warning + +# Title : Generic event handlers should be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3908 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3908.severity = warning + +# Title : Collections should implement the generic interface +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3909 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3909.severity = none + +# Title : All branches in a conditional structure should not have exactly the same implementation +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3923 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3923.severity = warning + +# Title : "ISerializable" should be implemented correctly +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3925 +# Tags : C#, MainSourceScope, SonarWay +# Comment : TODO - is ISerializable still relevant? +dotnet_diagnostic.S3925.severity = none + +# Title : Deserialization methods should be provided for "OptionalField" members +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3926 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3926.severity = warning + +# Title : Serialization event handlers should be implemented correctly +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3927 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3927.severity = warning + +# Title : Parameter names used into ArgumentException constructors should match an existing one +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3928 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3928.severity = warning + +# Title : Number patterns should be regular +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3937 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3937.severity = warning + +# Title : Calculations should not overflow +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3949 +# Tags : C#, MainSourceScope, TestSourceScope, Unnecessary +dotnet_diagnostic.S3949.severity = suggestion + +# Title : "Generic.List" instances should not be part of public APIs +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3956 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3956.severity = warning + +# Title : "static readonly" constants should be "const" instead +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3962 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3962.severity = none + +# Title : "static" fields should be initialized inline +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3963 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3963.severity = none + +# Title : Objects should not be disposed more than once +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3966 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3966.severity = warning + +# Title : Multidimensional arrays should not be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3967 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3967.severity = warning + +# Title : "GC.SuppressFinalize" should not be called +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3971 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3971.severity = warning + +# Title : Conditionals should start on new lines +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3972 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3972.severity = warning + +# Title : A conditionally executed single line should be denoted by indentation +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3973 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3973.severity = warning + +# Title : Collection sizes and array length comparisons should make sense +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3981 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3981.severity = warning + +# Title : Exceptions should not be created without being thrown +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3984 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3984.severity = warning + +# Title : Assemblies should be marked as CLS compliant +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3990 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3990.severity = none + +# Title : Assemblies should explicitly specify COM visibility +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3992 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3992.severity = none + +# Title : Custom attributes should be marked with "System.AttributeUsageAttribute" +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3993 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3993.severity = none + +# Title : URI Parameters should not be strings +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3994 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3994.severity = none + +# Title : URI return values should not be strings +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3995 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3995.severity = warning + +# Title : URI properties should not be strings +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3996 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3996.severity = warning + +# Title : String URI overloads should call "System.Uri" overloads +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3997 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3997.severity = warning + +# Title : Threads should not lock on objects with weak identity +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3998 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3998.severity = warning + +# Title : Pointers to unmanaged memory should not be visible +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4000 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4000.severity = warning + +# Title : Disposable types should declare finalizers +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4002 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4002.severity = warning + +# Title : Collection properties should be readonly +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4004 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4004.severity = none + +# Title : "System.Uri" arguments should be used instead of strings +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4005 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4005.severity = none + +# Title : Inherited member visibility should not be decreased +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4015 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4015.severity = warning + +# Title : Enumeration members should not be named "Reserved" +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4016 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4016.severity = warning + +# Title : Method signatures should not contain nested generic types +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4017 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4017.severity = none + +# Title : All type parameters should be used in the parameter list to enable type inference +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4018 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4018.severity = none + +# Title : Base class methods should not be hidden +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4019 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4019.severity = warning + +# Title : Enumerations should have "Int32" storage +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4022 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4022.severity = warning + +# Title : Interfaces should not be empty +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4023 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4023.severity = warning + +# Title : Child class fields should not differ from parent class fields only by capitalization +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4025 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4025.severity = warning + +# Title : Assemblies should be marked with "NeutralResourcesLanguageAttribute" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4026 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4026.severity = warning + +# Title : Exceptions should provide standard constructors +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4027 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4027.severity = none + +# Title : Classes implementing "IEquatable" should be sealed +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4035 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4035.severity = warning + +# Title : Searching OS commands in PATH is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4036 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4036.severity = warning + +# Title : Interface methods should be callable by derived types +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4039 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4039.severity = warning + +# Title : Strings should be normalized to uppercase +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4040 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4040.severity = none + +# Title : Type names should not match namespaces +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4041 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4041.severity = warning + +# Title : Generics should be used when appropriate +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4047 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4047.severity = warning + +# Title : Properties should be preferred +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4049 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4049.severity = warning + +# Title : Operators should be overloaded consistently +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4050 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4050.severity = warning + +# Title : Types should not extend outdated base types +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4052 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4052.severity = warning + +# Title : Literals should not be passed as localized parameters +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4055 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4055.severity = none + +# Title : Overloads with a "CultureInfo" or an "IFormatProvider" parameter should be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4056 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4056.severity = warning + +# Title : Locales should be set for data types +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4057 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4057.severity = warning + +# Title : Overloads with a "StringComparison" parameter should be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4058 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4058.severity = none + +# Title : Property names should not match get methods +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4059 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4059.severity = warning + +# Title : Non-abstract attributes should be sealed +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4060 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4060.severity = none + +# Title : "params" should be used instead of "varargs" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4061 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4061.severity = warning + +# Title : Operator overloads should have named alternatives +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4069 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4069.severity = none + +# Title : Non-flags enums should not be marked with "FlagsAttribute" +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4070 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4070.severity = none + +# Title : Method overloads should be grouped together +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4136 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4136.severity = warning + +# Title : Duplicate values should not be passed as arguments +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4142 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4142.severity = warning + +# Title : Collection elements should not be replaced unconditionally +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4143 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4143.severity = warning + +# Title : Methods should not have identical implementations +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4144 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4144.severity = warning + +# Title : Empty collections should not be accessed or iterated +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4158 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4158.severity = warning + +# Title : Classes should implement their "ExportAttribute" interfaces +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4159 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4159.severity = warning + +# Title : Native methods should be wrapped +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4200 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4200.severity = warning + +# Title : Null checks should not be used with "is" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4201 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4201.severity = warning + +# Title : Windows Forms entry points should be marked with STAThread +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4210 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4210.severity = warning + +# Title : Members should not have conflicting transparency annotations +# Category : Major Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4211 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4211.severity = warning + +# Title : Serialization constructors should be secured +# Category : Major Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4212 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4212.severity = warning + +# Title : "P/Invoke" methods should not be visible +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4214 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4214.severity = warning + +# Title : Events should have proper arguments +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4220 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4220.severity = warning + +# Title : Extension methods should not extend "object" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4225 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4225.severity = warning + +# Title : Extensions should be in separate namespaces +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4226 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4226.severity = none + +# Title : "ConstructorArgument" parameters should exist in constructors +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4260 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4260.severity = warning + +# Title : Methods should be named according to their synchronicities +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4261 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4261.severity = none + +# Title : Getters and setters should access the expected fields +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4275 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4275.severity = warning + +# Title : "Shared" parts should not be created with "new" +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4277 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4277.severity = warning + +# Title : Weak SSL/TLS protocols should not be used +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4423 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4423.severity = warning + +# Title : Cryptographic keys should be robust +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4426 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4426.severity = warning + +# Title : "PartCreationPolicyAttribute" should be used with "ExportAttribute" +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4428 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4428.severity = warning + +# Title : AES encryption algorithm should be used with secured mode +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4432 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4432.severity = warning + +# Title : LDAP connections should be authenticated +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4433 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4433.severity = warning + +# Title : Parameter validation in yielding methods should be wrapped +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4456 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4456.severity = warning + +# Title : Parameter validation in "async"/"await" methods should be wrapped +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4457 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4457.severity = warning + +# Title : Calls to "async" methods should not be blocking +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4462 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4462.severity = none + +# Title : Unread "private" fields should be removed +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4487 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay, Unnecessary +dotnet_diagnostic.S4487.severity = none + +# Title : Disabling CSRF protections is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4502 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4502.severity = warning + +# Title : Delivering code in production with debug features activated is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4507 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4507.severity = warning + +# Title : "default" clauses should be first or last +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4524 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4524.severity = warning + +# Title : ASP.NET HTTP request validation feature should not be disabled +# Category : Major Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4564 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4564.severity = warning + +# Title : "new Guid()" should not be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4581 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4581.severity = warning + +# Title : Calls to delegate's method "BeginInvoke" should be paired with calls to "EndInvoke" +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4583 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4583.severity = warning + +# Title : Non-async "Task/Task" methods should not return null +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4586 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4586.severity = warning + +# Title : String offset-based methods should be preferred for finding substrings from offsets +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4635 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4635.severity = warning + +# Title : Using regular expressions is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4784 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4784.severity = warning + +# Title : Encrypting data is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4787 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4787.severity = warning + +# Title : Using weak hashing algorithms is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4790 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4790.severity = warning + +# Title : Configuring loggers is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4792 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4792.severity = warning + +# Title : Using Sockets is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4818 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4818.severity = warning + +# Title : Using command line arguments is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4823 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4823.severity = warning + +# Title : Reading the Standard Input is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4829 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4829.severity = warning + +# Title : Server certificates should be verified during SSL/TLS connections +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4830 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4830.severity = none + +# Title : Controlling permissions is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4834 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4834.severity = warning + +# Title : "ValueTask" should be consumed correctly +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5034 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S5034.severity = warning + +# Title : Expanding archive files without controlling resource consumption is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5042 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5042.severity = warning + +# Title : Having a permissive Cross-Origin Resource Sharing policy is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5122 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5122.severity = warning + +# Title : Using clear-text protocols is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5332 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5332.severity = warning + +# Title : Using publicly writable directories is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5443 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5443.severity = warning + +# Title : Insecure temporary file creation methods should not be used +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5445 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5445.severity = warning + +# Title : Encryption algorithms should be used with secure mode and padding scheme +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5542 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5542.severity = warning + +# Title : Cipher algorithms should be robust +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5547 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5547.severity = warning + +# Title : JWT should be signed and verified with strong cipher algorithms +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5659 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5659.severity = warning + +# Title : Allowing requests with excessive content length is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5693 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S5693.severity = warning + +# Title : Disabling ASP.NET "Request Validation" feature is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5753 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5753.severity = warning + +# Title : Deserializing objects without performing data validation is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5766 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S5766.severity = warning + +# Title : Types allowed to be deserialized should be restricted +# Category : Major Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5773 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5773.severity = warning + +# Title : Use a testable date/time provider +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6354 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S6354.severity = none + +# Title : Azure Functions should be stateless +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6419 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S6419.severity = warning + +# Title : Client instances should not be recreated on each Azure Function invocation +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6420 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S6420.severity = warning + +# Title : Azure Functions should use Structured Error Handling +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6421 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S6421.severity = warning + +# Title : Calls to "async" methods should not be blocking in Azure Functions +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6422 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S6422.severity = warning + +# Title : Azure Functions should log all failures +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6423 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S6423.severity = warning + +# Title : Interfaces for durable entities should satisfy the restrictions +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6424 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S6424.severity = warning + +# Title : Not specifying a timeout for regular expressions is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6444 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S6444.severity = warning + +# Title : Literal suffixes should be upper case +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-818 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S818.severity = warning + +# Title : Increment (++) and decrement (--) operators should not be used in a method call or mixed with other operators in an expression +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-881 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S881.severity = suggestion + +# Title : "goto" statement should not be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-907 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S907.severity = warning + +# Title : Parameter names should match base declaration and other partial definitions +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-927 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S927.severity = none + +# Title : Copy-paste token calculator +# Category : +# Tags : MainSourceScope, TestSourceScope, Utility, NotConfigurable +dotnet_diagnostic.S9999-cpd.severity = warning + +# Title : Log generator +# Category : +# Tags : MainSourceScope, TestSourceScope, Utility, NotConfigurable +dotnet_diagnostic.S9999-log.severity = none + +# Title : File metadata generator +# Category : +# Tags : MainSourceScope, TestSourceScope, Utility, NotConfigurable +dotnet_diagnostic.S9999-metadata.severity = warning + +# Title : Metrics calculator +# Category : +# Tags : MainSourceScope, TestSourceScope, Utility, NotConfigurable +dotnet_diagnostic.S9999-metrics.severity = warning + +# Title : Symbol reference calculator +# Category : +# Tags : MainSourceScope, TestSourceScope, Utility, NotConfigurable +dotnet_diagnostic.S9999-symbolRef.severity = warning + +# Title : Token type calculator +# Category : +# Tags : MainSourceScope, TestSourceScope, Utility, NotConfigurable +dotnet_diagnostic.S9999-token-type.severity = warning + +# Title : XML comment analysis disabled +# Category : StyleCop.CSharp.SpecialRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA0001.md +dotnet_diagnostic.SA0001.severity = none + +# Title : Invalid settings file +# Category : StyleCop.CSharp.SpecialRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA0002.md +dotnet_diagnostic.SA0002.severity = warning + +# Title : Keywords should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1000.md +dotnet_diagnostic.SA1000.severity = none + +# Title : Commas should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1001.md +dotnet_diagnostic.SA1001.severity = none + +# Title : Semicolons should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1002.md +dotnet_diagnostic.SA1002.severity = none + +# Title : Symbols should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1003.md +dotnet_diagnostic.SA1003.severity = none + +# Title : Documentation lines should begin with single space +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1004.md +dotnet_diagnostic.SA1004.severity = warning + +# Title : Single line comments should begin with single space +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1005.md +dotnet_diagnostic.SA1005.severity = warning + +# Title : Preprocessor keywords should not be preceded by space +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1006.md +dotnet_diagnostic.SA1006.severity = warning + +# Title : Operator keyword should be followed by space +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1007.md +dotnet_diagnostic.SA1007.severity = none + +# Title : Opening parenthesis should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1008.md +dotnet_diagnostic.SA1008.severity = none + +# Title : Closing parenthesis should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1009.md +dotnet_diagnostic.SA1009.severity = none + +# Title : Opening square brackets should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1010.md +dotnet_diagnostic.SA1010.severity = none + +# Title : Closing square brackets should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1011.md +dotnet_diagnostic.SA1011.severity = none + +# Title : Opening braces should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1012.md +dotnet_diagnostic.SA1012.severity = none + +# Title : Closing braces should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1013.md +dotnet_diagnostic.SA1013.severity = none + +# Title : Opening generic brackets should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1014.md +dotnet_diagnostic.SA1014.severity = none + +# Title : Closing generic brackets should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1015.md +dotnet_diagnostic.SA1015.severity = none + +# Title : Opening attribute brackets should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1016.md +dotnet_diagnostic.SA1016.severity = none + +# Title : Closing attribute brackets should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1017.md +dotnet_diagnostic.SA1017.severity = none + +# Title : Nullable type symbols should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1018.md +dotnet_diagnostic.SA1018.severity = none + +# Title : Member access symbols should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1019.md +dotnet_diagnostic.SA1019.severity = none + +# Title : Increment decrement symbols should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1020.md +dotnet_diagnostic.SA1020.severity = none + +# Title : Negative signs should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1021.md +dotnet_diagnostic.SA1021.severity = none + +# Title : Positive signs should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1022.md +dotnet_diagnostic.SA1022.severity = none + +# Title : Dereference and access of symbols should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1023.md +dotnet_diagnostic.SA1023.severity = none + +# Title : Colons Should Be Spaced Correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1024.md +dotnet_diagnostic.SA1024.severity = none + +# Title : Code should not contain multiple whitespace in a row +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1025.md +dotnet_diagnostic.SA1025.severity = none + +# Title : Code should not contain space after new or stackalloc keyword in implicitly typed array allocation +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1026.md +dotnet_diagnostic.SA1026.severity = none + +# Title : Use tabs correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1027.md +dotnet_diagnostic.SA1027.severity = warning + +# Title : Code should not contain trailing whitespace +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1028.md +# Tags : Unnecessary +dotnet_diagnostic.SA1028.severity = warning + +# Title : Do not prefix calls with base unless local implementation exists +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1100.md +dotnet_diagnostic.SA1100.severity = warning + +# Title : Prefix local calls with this +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1101.md +dotnet_diagnostic.SA1101.severity = none + +# Title : Query clause should follow previous clause +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1102.md +dotnet_diagnostic.SA1102.severity = warning + +# Title : Query clauses should be on separate lines or all on one line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1103.md +dotnet_diagnostic.SA1103.severity = warning + +# Title : Query clause should begin on new line when previous clause spans multiple lines +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1104.md +dotnet_diagnostic.SA1104.severity = warning + +# Title : Query clauses spanning multiple lines should begin on own line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1105.md +dotnet_diagnostic.SA1105.severity = warning + +# Title : Code should not contain empty statements +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1106.md +# Tags : Unnecessary +dotnet_diagnostic.SA1106.severity = warning + +# Title : Code should not contain multiple statements on one line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1107.md +dotnet_diagnostic.SA1107.severity = none + +# Title : Block statements should not contain embedded comments +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1108.md +dotnet_diagnostic.SA1108.severity = warning + +# Title : Block statements should not contain embedded regions +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1109.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1109.severity = warning + +# Title : Opening parenthesis or bracket should be on declaration line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1110.md +dotnet_diagnostic.SA1110.severity = warning + +# Title : Closing parenthesis should be on line of last parameter +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1111.md +dotnet_diagnostic.SA1111.severity = warning + +# Title : Closing parenthesis should be on line of opening parenthesis +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1112.md +dotnet_diagnostic.SA1112.severity = warning + +# Title : Comma should be on the same line as previous parameter +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1113.md +dotnet_diagnostic.SA1113.severity = warning + +# Title : Parameter list should follow declaration +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1114.md +dotnet_diagnostic.SA1114.severity = warning + +# Title : Parameter should follow comma +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1115.md +dotnet_diagnostic.SA1115.severity = none + +# Title : Split parameters should start on line after declaration +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1116.md +dotnet_diagnostic.SA1116.severity = none + +# Title : Parameters should be on same line or separate lines +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1117.md +dotnet_diagnostic.SA1117.severity = none + +# Title : Parameter should not span multiple lines +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1118.md +dotnet_diagnostic.SA1118.severity = warning + +# Title : Statement should not use unnecessary parenthesis +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1119.md +dotnet_diagnostic.SA1119.severity = warning + +# Title : Statement should not use unnecessary parenthesis +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1119.md +# Tags : Unnecessary, NotConfigurable +dotnet_diagnostic.SA1119_p.severity = none + +# Title : Comments should contain text +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1120.md +dotnet_diagnostic.SA1120.severity = warning + +# Title : Use built-in type alias +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1121.md +# Tags : Unnecessary +dotnet_diagnostic.SA1121.severity = warning + +# Title : Use string.Empty for empty strings +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1122.md +dotnet_diagnostic.SA1122.severity = warning + +# Title : Do not place regions within elements +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1123.md +dotnet_diagnostic.SA1123.severity = warning + +# Title : Do not use regions +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1124.md +dotnet_diagnostic.SA1124.severity = none + +# Title : Use shorthand for nullable types +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1125.md +dotnet_diagnostic.SA1125.severity = warning + +# Title : Prefix calls correctly +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1126.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1126.severity = none + +# Title : Generic type constraints should be on their own line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1127.md +dotnet_diagnostic.SA1127.severity = warning + +# Title : Put constructor initializers on their own line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1128.md +dotnet_diagnostic.SA1128.severity = warning + +# Title : Do not use default value type constructor +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1129.md +dotnet_diagnostic.SA1129.severity = warning + +# Title : Use lambda syntax +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1130.md +dotnet_diagnostic.SA1130.severity = warning + +# Title : Use readable conditions +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1131.md +dotnet_diagnostic.SA1131.severity = warning + +# Title : Do not combine fields +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1132.md +# Comment : S1169 handles fields and variables +dotnet_diagnostic.SA1132.severity = none + +# Title : Do not combine attributes +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1133.md +dotnet_diagnostic.SA1133.severity = warning + +# Title : Attributes should not share line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1134.md +dotnet_diagnostic.SA1134.severity = none + +# Title : Using directives should be qualified +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1135.md +dotnet_diagnostic.SA1135.severity = warning + +# Title : Enum values should be on separate lines +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1136.md +dotnet_diagnostic.SA1136.severity = warning + +# Title : Elements should have the same indentation +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1137.md +# Comment : Doesn't work with file-scoped namespaces +dotnet_diagnostic.SA1137.severity = none + +# Title : Use literal suffix notation instead of casting +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1139.md +dotnet_diagnostic.SA1139.severity = warning + +# Title : Use tuple syntax +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1141.md +dotnet_diagnostic.SA1141.severity = warning + +# Title : Refer to tuple fields by name +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1142.md +dotnet_diagnostic.SA1142.severity = none + +# Title : Using directives should be placed correctly +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md +dotnet_diagnostic.SA1200.severity = none + +# Title : Elements should appear in the correct order +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1201.md +dotnet_diagnostic.SA1201.severity = none + +# Title : Elements should be ordered by access +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1202.md +dotnet_diagnostic.SA1202.severity = warning + +# Title : Constants should appear before fields +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1203.md +dotnet_diagnostic.SA1203.severity = warning + +# Title : Static elements should appear before instance elements +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1204.md +dotnet_diagnostic.SA1204.severity = warning + +# Title : Partial elements should declare access +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1205.md +dotnet_diagnostic.SA1205.severity = warning + +# Title : Declaration keywords should follow order +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1206.md +dotnet_diagnostic.SA1206.severity = warning + +# Title : Protected should come before internal +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1207.md +dotnet_diagnostic.SA1207.severity = warning + +# Title : System using directives should be placed before other using directives +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1208.md +dotnet_diagnostic.SA1208.severity = warning + +# Title : Using alias directives should be placed after other using directives +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1209.md +dotnet_diagnostic.SA1209.severity = warning + +# Title : Using directives should be ordered alphabetically by namespace +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1210.md +dotnet_diagnostic.SA1210.severity = warning + +# Title : Using alias directives should be ordered alphabetically by alias name +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1211.md +dotnet_diagnostic.SA1211.severity = warning + +# Title : Property accessors should follow order +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1212.md +dotnet_diagnostic.SA1212.severity = warning + +# Title : Event accessors should follow order +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1213.md +dotnet_diagnostic.SA1213.severity = warning + +# Title : Readonly fields should appear before non-readonly fields +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1214.md +dotnet_diagnostic.SA1214.severity = warning + +# Title : Using static directives should be placed at the correct location +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1216.md +dotnet_diagnostic.SA1216.severity = warning + +# Title : Using static directives should be ordered alphabetically +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1217.md +dotnet_diagnostic.SA1217.severity = warning + +# Title : Element should begin with upper-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md +dotnet_diagnostic.SA1300.severity = none + +# Title : Element should begin with lower-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1301.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1301.severity = none + +# Title : Interface names should begin with I +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1302.md +dotnet_diagnostic.SA1302.severity = none + +# Title : Const field names should begin with upper-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md +dotnet_diagnostic.SA1303.severity = none + +# Title : Non-private readonly fields should begin with upper-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1304.md +dotnet_diagnostic.SA1304.severity = none + +# Title : Field names should not use Hungarian notation +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1305.md +dotnet_diagnostic.SA1305.severity = none + +# Title : Field names should begin with lower-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1306.md +dotnet_diagnostic.SA1306.severity = none + +# Title : Accessible fields should begin with upper-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1307.md +dotnet_diagnostic.SA1307.severity = none + +# Title : Variable names should not be prefixed +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1308.md +dotnet_diagnostic.SA1308.severity = none + +# Title : Field names should not begin with underscore +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1309.md +dotnet_diagnostic.SA1309.severity = none + +# Title : Field names should not contain underscore +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1310.md +dotnet_diagnostic.SA1310.severity = warning + +# Title : Static readonly fields should begin with upper-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md +dotnet_diagnostic.SA1311.severity = none + +# Title : Variable names should begin with lower-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md +dotnet_diagnostic.SA1312.severity = none + +# Title : Parameter names should begin with lower-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1313.md +dotnet_diagnostic.SA1313.severity = none + +# Title : Type parameter names should begin with T +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1314.md +dotnet_diagnostic.SA1314.severity = none + +# Title : Tuple element names should use correct casing +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1316.md +dotnet_diagnostic.SA1316.severity = warning + +# Title : Access modifier should be declared +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1400.md +dotnet_diagnostic.SA1400.severity = warning + +# Title : Fields should be private +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md +dotnet_diagnostic.SA1401.severity = none + +# Title : File may only contain a single type +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1402.md +dotnet_diagnostic.SA1402.severity = warning + +# Title : File may only contain a single namespace +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1403.md +dotnet_diagnostic.SA1403.severity = warning + +# Title : Code analysis suppression should have justification +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1404.md +dotnet_diagnostic.SA1404.severity = warning + +# Title : Debug.Assert should provide message text +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1405.md +dotnet_diagnostic.SA1405.severity = warning + +# Title : Debug.Fail should provide message text +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1406.md +dotnet_diagnostic.SA1406.severity = warning + +# Title : Arithmetic expressions should declare precedence +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1407.md +dotnet_diagnostic.SA1407.severity = warning + +# Title : Conditional expressions should declare precedence +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1408.md +dotnet_diagnostic.SA1408.severity = warning + +# Title : Remove unnecessary code +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1409.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1409.severity = warning + +# Title : Remove delegate parenthesis when possible +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1410.md +# Tags : Unnecessary +dotnet_diagnostic.SA1410.severity = warning + +# Title : Attribute constructor should not use unnecessary parenthesis +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1411.md +# Tags : Unnecessary +dotnet_diagnostic.SA1411.severity = warning + +# Title : Store files as UTF-8 with byte order mark +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1412.md +# Comment : Pedantic +dotnet_diagnostic.SA1412.severity = none + +# Title : Use trailing comma in multi-line initializers +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1413.md +dotnet_diagnostic.SA1413.severity = none + +# Title : Tuple types in signatures should have element names +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1414.md +dotnet_diagnostic.SA1414.severity = warning + +# Title : Braces for multi-line statements should not share line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1500.md +dotnet_diagnostic.SA1500.severity = warning + +# Title : Statement should not be on a single line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1501.md +dotnet_diagnostic.SA1501.severity = warning + +# Title : Element should not be on a single line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1502.md +dotnet_diagnostic.SA1502.severity = warning + +# Title : Braces should not be omitted +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1503.md +dotnet_diagnostic.SA1503.severity = none + +# Title : All accessors should be single-line or multi-line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1504.md +dotnet_diagnostic.SA1504.severity = warning + +# Title : Opening braces should not be followed by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1505.md +dotnet_diagnostic.SA1505.severity = warning + +# Title : Element documentation headers should not be followed by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1506.md +dotnet_diagnostic.SA1506.severity = warning + +# Title : Code should not contain multiple blank lines in a row +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1507.md +dotnet_diagnostic.SA1507.severity = none + +# Title : Closing braces should not be preceded by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1508.md +dotnet_diagnostic.SA1508.severity = none + +# Title : Opening braces should not be preceded by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1509.md +dotnet_diagnostic.SA1509.severity = warning + +# Title : Chained statement blocks should not be preceded by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1510.md +dotnet_diagnostic.SA1510.severity = warning + +# Title : While-do footer should not be preceded by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1511.md +dotnet_diagnostic.SA1511.severity = warning + +# Title : Single-line comments should not be followed by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1512.md +dotnet_diagnostic.SA1512.severity = none + +# Title : Closing brace should be followed by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1513.md +dotnet_diagnostic.SA1513.severity = warning + +# Title : Element documentation header should be preceded by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1514.md +dotnet_diagnostic.SA1514.severity = warning + +# Title : Single-line comment should be preceded by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1515.md +dotnet_diagnostic.SA1515.severity = warning + +# Title : Elements should be separated by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1516.md +dotnet_diagnostic.SA1516.severity = none + +# Title : Code should not contain blank lines at start of file +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1517.md +dotnet_diagnostic.SA1517.severity = warning + +# Title : Use line endings correctly at end of file +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1518.md +dotnet_diagnostic.SA1518.severity = none + +# Title : Braces should not be omitted from multi-line child statement +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1519.md +dotnet_diagnostic.SA1519.severity = warning + +# Title : Use braces consistently +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1520.md +dotnet_diagnostic.SA1520.severity = warning + +# Title : Elements should be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1600.md +dotnet_diagnostic.SA1600.severity = warning + +# Title : Partial elements should be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1601.md +dotnet_diagnostic.SA1601.severity = none + +# Title : Enumeration items should be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1602.md +dotnet_diagnostic.SA1602.severity = warning + +# Title : Documentation should contain valid XML +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1603.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1603.severity = warning + +# Title : Element documentation should have summary +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1604.md +dotnet_diagnostic.SA1604.severity = warning + +# Title : Partial element documentation should have summary +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1605.md +dotnet_diagnostic.SA1605.severity = warning + +# Title : Element documentation should have summary text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1606.md +dotnet_diagnostic.SA1606.severity = warning + +# Title : Partial element documentation should have summary text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1607.md +dotnet_diagnostic.SA1607.severity = warning + +# Title : Element documentation should not have default summary +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1608.md +dotnet_diagnostic.SA1608.severity = warning + +# Title : Property documentation should have value +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1609.md +dotnet_diagnostic.SA1609.severity = none + +# Title : Property documentation should have value text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1610.md +dotnet_diagnostic.SA1610.severity = none + +# Title : Element parameters should be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1611.md +dotnet_diagnostic.SA1611.severity = none + +# Title : Element parameter documentation should match element parameters +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1612.md +dotnet_diagnostic.SA1612.severity = warning + +# Title : Element parameter documentation should declare parameter name +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1613.md +dotnet_diagnostic.SA1613.severity = warning + +# Title : Element parameter documentation should have text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1614.md +dotnet_diagnostic.SA1614.severity = warning + +# Title : Element return value should be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1615.md +dotnet_diagnostic.SA1615.severity = warning + +# Title : Element return value documentation should have text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1616.md +dotnet_diagnostic.SA1616.severity = warning + +# Title : Void return value should not be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1617.md +dotnet_diagnostic.SA1617.severity = warning + +# Title : Generic type parameters should be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1618.md +dotnet_diagnostic.SA1618.severity = warning + +# Title : Generic type parameters should be documented partial class +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1619.md +dotnet_diagnostic.SA1619.severity = warning + +# Title : Generic type parameter documentation should match type parameters +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1620.md +dotnet_diagnostic.SA1620.severity = warning + +# Title : Generic type parameter documentation should declare parameter name +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1621.md +dotnet_diagnostic.SA1621.severity = warning + +# Title : Generic type parameter documentation should have text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1622.md +dotnet_diagnostic.SA1622.severity = warning + +# Title : Property summary documentation should match accessors +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1623.md +dotnet_diagnostic.SA1623.severity = warning + +# Title : Property summary documentation should omit accessor with restricted access +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1624.md +dotnet_diagnostic.SA1624.severity = warning + +# Title : Element documentation should not be copied and pasted +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1625.md +dotnet_diagnostic.SA1625.severity = warning + +# Title : Single-line comments should not use documentation style slashes +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1626.md +dotnet_diagnostic.SA1626.severity = warning + +# Title : Documentation text should not be empty +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1627.md +dotnet_diagnostic.SA1627.severity = warning + +# Title : Documentation text should begin with a capital letter +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1628.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1628.severity = warning + +# Title : Documentation text should end with a period +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1629.md +dotnet_diagnostic.SA1629.severity = warning + +# Title : Documentation text should contain whitespace +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1630.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1630.severity = warning + +# Title : Documentation should meet character percentage +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1631.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1631.severity = warning + +# Title : Documentation text should meet minimum character length +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1632.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1632.severity = warning + +# Title : File should have header +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1633.md +dotnet_diagnostic.SA1633.severity = none + +# Title : File header should show copyright +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1634.md +dotnet_diagnostic.SA1634.severity = none + +# Title : File header should have copyright text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1635.md +dotnet_diagnostic.SA1635.severity = none + +# Title : File header copyright text should match +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1636.md +dotnet_diagnostic.SA1636.severity = none + +# Title : File header should contain file name +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1637.md +dotnet_diagnostic.SA1637.severity = none + +# Title : File header file name documentation should match file name +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1638.md +dotnet_diagnostic.SA1638.severity = none + +# Title : File header should have summary +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1639.md +dotnet_diagnostic.SA1639.severity = none + +# Title : File header should have valid company text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1640.md +dotnet_diagnostic.SA1640.severity = none + +# Title : File header company name text should match +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1641.md +dotnet_diagnostic.SA1641.severity = none + +# Title : Constructor summary documentation should begin with standard text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1642.md +dotnet_diagnostic.SA1642.severity = warning + +# Title : Destructor summary documentation should begin with standard text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1643.md +dotnet_diagnostic.SA1643.severity = warning + +# Title : Documentation headers should not contain blank lines +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1644.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1644.severity = warning + +# Title : Included documentation file does not exist +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1645.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1645.severity = warning + +# Title : Included documentation XPath does not exist +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1646.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1646.severity = warning + +# Title : Include node does not contain valid file and path +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1647.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1647.severity = warning + +# Title : inheritdoc should be used with inheriting class +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1648.md +dotnet_diagnostic.SA1648.severity = warning + +# Title : File name should match first type name +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1649.md +dotnet_diagnostic.SA1649.severity = warning + +# Title : Element documentation should be spelled correctly +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1650.md +# Tags : NotConfigurable +# Comment : Deprecated +dotnet_diagnostic.SA1650.severity = none + +# Title : Do not use placeholder elements +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1651.md +dotnet_diagnostic.SA1651.severity = warning + +# Title : Do not prefix local calls with 'this.' +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SX1101.md +# Tags : Unnecessary +dotnet_diagnostic.SX1101.severity = warning + +# Title : Field names should begin with underscore +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SX1309.md +dotnet_diagnostic.SX1309.severity = none + +# Title : Static field names should begin with underscore +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SX1309S.md +dotnet_diagnostic.SX1309S.severity = none + +# Title : Avoid legacy thread switching APIs +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD001.md +dotnet_diagnostic.VSTHRD001.severity = warning + +# Title : Avoid problematic synchronous waits +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD002.md +dotnet_diagnostic.VSTHRD002.severity = warning + +# Title : Avoid awaiting foreign Tasks +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD003.md +dotnet_diagnostic.VSTHRD003.severity = warning + +# Title : Await SwitchToMainThreadAsync +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD004.md +dotnet_diagnostic.VSTHRD004.severity = error + +# Title : Invoke single-threaded types on Main thread +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD010.md +# Tags : CompilationEnd +dotnet_diagnostic.VSTHRD010.severity = warning + +# Title : Use AsyncLazy +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD011.md +dotnet_diagnostic.VSTHRD011.severity = error + +# Title : Provide JoinableTaskFactory where allowed +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD012.md +dotnet_diagnostic.VSTHRD012.severity = warning + +# Title : Avoid async void methods +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD100.md +dotnet_diagnostic.VSTHRD100.severity = error + +# Title : Avoid unsupported async delegates +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD101.md +dotnet_diagnostic.VSTHRD101.severity = error + +# Title : Implement internal logic asynchronously +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD102.md +dotnet_diagnostic.VSTHRD102.severity = suggestion + +# Title : Call async methods when in an async method +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD103.md +dotnet_diagnostic.VSTHRD103.severity = warning + +# Title : Offer async methods +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD104.md +dotnet_diagnostic.VSTHRD104.severity = warning + +# Title : Avoid method overloads that assume TaskScheduler.Current +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD105.md +dotnet_diagnostic.VSTHRD105.severity = warning + +# Title : Use InvokeAsync to raise async events +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD106.md +dotnet_diagnostic.VSTHRD106.severity = warning + +# Title : Await Task within using expression +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD107.md +dotnet_diagnostic.VSTHRD107.severity = error + +# Title : Assert thread affinity unconditionally +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD108.md +dotnet_diagnostic.VSTHRD108.severity = warning + +# Title : Switch instead of assert in async methods +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD109.md +dotnet_diagnostic.VSTHRD109.severity = error + +# Title : Observe result of async calls +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD110.md +dotnet_diagnostic.VSTHRD110.severity = none + +# Title : Use ConfigureAwait(bool) +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD111.md +dotnet_diagnostic.VSTHRD111.severity = none + +# Title : Implement System.IAsyncDisposable +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD112.md +dotnet_diagnostic.VSTHRD112.severity = suggestion + +# Title : Check for System.IAsyncDisposable +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD113.md +dotnet_diagnostic.VSTHRD113.severity = suggestion + +# Title : Avoid returning a null Task +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD114.md +dotnet_diagnostic.VSTHRD114.severity = warning + +# Title : Avoid returning a null Task +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD114.md +dotnet_diagnostic.VSTHRD114.severity = error + +# Title : Use "Async" suffix for async methods +# Category : Style +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD200.md +dotnet_diagnostic.VSTHRD200.severity = warning + diff --git a/src/Analyzers/Directory.Build.props b/src/Analyzers/Directory.Build.props new file mode 100644 index 0000000000..2fae3093a1 --- /dev/null +++ b/src/Analyzers/Directory.Build.props @@ -0,0 +1,9 @@ + + + + + netstandard2.0 + n/a + true + + diff --git a/src/Analyzers/Directory.Build.targets b/src/Analyzers/Directory.Build.targets new file mode 100644 index 0000000000..88cbdae86d --- /dev/null +++ b/src/Analyzers/Directory.Build.targets @@ -0,0 +1,8 @@ + + + + + false + false + + diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/AsyncCallInsideUsingBlockAnalyzer.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/AsyncCallInsideUsingBlockAnalyzer.cs new file mode 100644 index 0000000000..77ee5197f4 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/AsyncCallInsideUsingBlockAnalyzer.cs @@ -0,0 +1,222 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.Extensions.ExtraAnalyzers.Utilities; + +namespace Microsoft.Extensions.ExtraAnalyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class AsyncCallInsideUsingBlockAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + DiagDescriptors.AsyncCallInsideUsingBlock); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationContext => + { + // Stryker disable all : no reasonable means to test this + // Get the target Task / Task / ValueTask / ValueTask types. + var taskType = compilationContext.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task"); + var taskOfTType = compilationContext.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1"); + var valueTaskType = compilationContext.Compilation.GetTypeByMetadataName("System.Threading.Tasks.ValueTask"); + var valueTaskOfTType = compilationContext.Compilation.GetTypeByMetadataName("System.Threading.Tasks.ValueTask`1"); + + // If they don't exist, nothing more to do. + if (taskType == null && + taskOfTType == null && + valueTaskType == null && + valueTaskOfTType == null) + { + return; + } + + // Stryker restore all + + compilationContext.RegisterOperationAction(analysisContext => + { + var operation = (IUsingDeclarationOperation)analysisContext.Operation; + var disposable = GetDisposableSymbol(operation.DeclarationGroup); + + if (operation.Parent == null) + { + return; + } + + ValidateDisposable(analysisContext, disposable, operation.Parent); + }, OperationKind.UsingDeclaration); + + compilationContext.RegisterOperationAction(analysisContext => + { + var operation = (IUsingOperation)analysisContext.Operation; + + // Declaration introduced or resource held by the using + if (operation.Resources is not IVariableDeclarationGroupOperation declarationGroup) + { + return; + } + + var disposable = GetDisposableSymbol(declarationGroup); + ValidateDisposable(analysisContext, disposable, operation.Body); + }, OperationKind.Using); + + void ValidateDisposable(OperationAnalysisContext analysisContext, + ILocalSymbol disposable, + IOperation block) + { + // Find all the invocations that return Task with the disposable object passed via arguments + var invocations = block.Descendants() + .OfType() + .Where(IsReturnTypeTask) + .Where(operation => SymbolInArguments(operation, disposable)); + + foreach (var invocation in invocations) + { + if (invocation.Ancestors(block) + .Any(operation => + { + switch (operation.Kind) + { + // check if the returned task is awaited inline + case OperationKind.Await: + return true; + + // check if the task.Wait is used + case OperationKind.Invocation: + return TaskWaitInvoked((operation as IInvocationOperation)!); + + // check if the task.Result is used + case OperationKind.PropertyReference: + return TaskResultInvoked((operation as IPropertyReferenceOperation)!); + + // check if the returned task is assigned to a declared variable and then awaited (async or sync) + case OperationKind.VariableDeclarator: + return IsTaskAwaited(block, (operation as IVariableDeclaratorOperation)!.Symbol); + + // check if the returned task is assigned to a variable declared previously and then awaited + case OperationKind.SimpleAssignment: + { + var assignmentTarget = ((IAssignmentOperation)operation).Target as ILocalReferenceOperation; + return assignmentTarget != null && IsTaskAwaited(block, assignmentTarget.Local); + } + + // check if the invocation result is passed to lambda - ignore such cases for now + case OperationKind.AnonymousFunction: + return true; + } + + return false; + })) + { + continue; + } + + var diagnostic = + Diagnostic.Create(DiagDescriptors.AsyncCallInsideUsingBlock, invocation.Syntax.GetLocation()); + analysisContext.ReportDiagnostic(diagnostic); + } + } + + bool IsReturnTypeTask(IInvocationOperation operation) + { + var returnType = operation.Type?.OriginalDefinition; + if (returnType == null) + { + // Stryker disable once boolean : no means to test this + return false; + } + + return SymbolEqualityComparer.Default.Equals(returnType, taskType) || + SymbolEqualityComparer.Default.Equals(returnType, taskOfTType) || + SymbolEqualityComparer.Default.Equals(returnType, valueTaskType) || + SymbolEqualityComparer.Default.Equals(returnType, valueTaskOfTType); + } + }); + } + + private static ILocalSymbol GetDisposableSymbol(IVariableDeclarationGroupOperation declarationGroup) + { + // All `IVariableDeclarationGroupOperation` will have at least 1 `IVariableDeclarationOperation`, + // even if the declaration group only declares 1 variable. + // In C#, this will always be a single declaration + return declarationGroup.Declarations[0].Declarators[0].Symbol; + } + + private static bool IsTaskAwaited(IOperation block, ILocalSymbol taskSymbol) + { + if (block.Descendants() + .OfType() + .SelectMany(operation => operation.Descendants()) + .Any(operation => ReferencesSymbol(operation, taskSymbol))) + { + return true; + } + + if (block.Descendants() + .OfType() + .Where(operation => ReferencesSymbol(operation.Instance, taskSymbol)) + .Any(TaskWaitInvoked)) + { + return true; + } + + if (block.Descendants() + .OfType() + .Where(operation => ReferencesSymbol(operation.Instance, taskSymbol)) + .Any(TaskResultInvoked)) + { + return true; + } + + return false; + } + + private static bool ReferencesSymbol(IOperation? operation, ILocalSymbol symbol) + { + if (operation == null) + { + return false; + } + + if (operation is not ILocalReferenceOperation localReference) + { + return false; + } + + return SymbolEqualityComparer.Default.Equals(localReference.Local, symbol); + } + + private static bool TaskWaitInvoked(IInvocationOperation invocation) + { + return invocation.TargetMethod.Name is "Wait" or "GetAwaiter"; + } + + private static bool TaskResultInvoked(IPropertyReferenceOperation operation) + { + return operation.Property.Name is "Result"; + } + + private static bool SymbolInArguments(IInvocationOperation invocation, ILocalSymbol symbol) + { + foreach (var argument in invocation.Arguments) + { + if (argument + .Value + .Children + .Any(operation => ReferencesSymbol(operation, symbol))) + { + return true; + } + } + + return false; + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/AsyncMethodWithoutCancellation.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/AsyncMethodWithoutCancellation.cs new file mode 100644 index 0000000000..0815c0ca1d --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/AsyncMethodWithoutCancellation.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.Extensions.ExtraAnalyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class AsyncMethodWithoutCancellation : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + DiagDescriptors.AsyncMethodWithoutCancellation); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationContext => + { + // Stryker disable all : no reasonable means to test this + // Get the target types. + var taskType = compilationContext.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task"); + var taskOfTType = compilationContext.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1"); + var valueTaskType = compilationContext.Compilation.GetTypeByMetadataName("System.Threading.Tasks.ValueTask"); + var valueTaskOfTType = compilationContext.Compilation.GetTypeByMetadataName("System.Threading.Tasks.ValueTask`1"); + + // If task types don't exist, nothing more to do. + if (taskType == null && + taskOfTType == null && + valueTaskType == null && + valueTaskOfTType == null) + { + return; + } + + var cancellationTokenType = compilationContext.Compilation.GetTypeByMetadataName("System.Threading.CancellationToken"); + var httpContextType = compilationContext.Compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Http.HttpContext"); + var connectionContextType = + compilationContext.Compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Connections.ConnectionContext"); + var obsoleteAttribute = + compilationContext.Compilation.GetTypeByMetadataName("System.ObsoleteAttribute"); + + var knownTypes = new HashSet(SymbolEqualityComparer.Default); + if (cancellationTokenType != null) + { + _ = knownTypes.Add(cancellationTokenType); + } + + if (httpContextType != null) + { + _ = knownTypes.Add(httpContextType); + } + + if (connectionContextType != null) + { + _ = knownTypes.Add(connectionContextType); + } + + if (knownTypes.Count == 0) + { + return; + } + + // Stryker restore all + compilationContext.RegisterSyntaxNodeAction(analysisContext => + { + var methodSymbol = (IMethodSymbol)analysisContext.ContainingSymbol!; + + // ignore overrides + if (methodSymbol.IsOverride) + { + return; + } + + // ignore obsoleted methods + if (IsObsoleteSymbol(methodSymbol)) + { + return; + } + + // ignore obsoleted types + if (IsObsoleteSymbol(methodSymbol.ContainingType)) + { + return; + } + + if (!IsReturnTypeTask(methodSymbol)) + { + return; + } + + if (MethodParametersContainKnownTypes(methodSymbol, knownTypes)) + { + return; + } + + // ignore interface implementations + if (IsImplementationOfInterface(methodSymbol)) + { + return; + } + + var diagnostic = + Diagnostic.Create(DiagDescriptors.AsyncMethodWithoutCancellation, analysisContext.Node.GetLocation()); + analysisContext.ReportDiagnostic(diagnostic); + }, SyntaxKind.MethodDeclaration); + + bool IsReturnTypeTask(IMethodSymbol method) + { + var returnType = method.ReturnType.OriginalDefinition; + return SymbolEqualityComparer.Default.Equals(returnType, taskType) || + SymbolEqualityComparer.Default.Equals(returnType, taskOfTType) || + SymbolEqualityComparer.Default.Equals(returnType, valueTaskType) || + SymbolEqualityComparer.Default.Equals(returnType, valueTaskOfTType); + } + + bool IsObsoleteSymbol(ISymbol symbol) + { + if (obsoleteAttribute == null) + { + return false; + } + + return symbol + .GetAttributes() + .Any(data => + SymbolEqualityComparer.Default.Equals(data.AttributeClass, obsoleteAttribute)); + } + }); + } + + private static bool IsImplementationOfInterface(IMethodSymbol method) + { + var containingType = method.ContainingType; + foreach (var @interface in containingType.AllInterfaces) + { + if (@interface.GetMembers().OfType() + .Select(interfaceSymbol => containingType.FindImplementationForInterfaceMember(interfaceSymbol)) + .Any(implementation => SymbolEqualityComparer.Default.Equals(implementation, method))) + { + return true; + } + } + + return false; + } + + private static bool MethodParametersContainKnownTypes(IMethodSymbol method, HashSet typeSymbols) + { + foreach (var argument in method.Parameters) + { + if (typeSymbols.Contains(argument.Type)) + { + return true; + } + } + + return false; + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/Arrays.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/Arrays.cs new file mode 100644 index 0000000000..5975cd7a35 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/Arrays.cs @@ -0,0 +1,129 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.ExtraAnalyzers.CallAnalysis; + +/// +/// Recommends replacing dictionaries and sets indexed by [enum|byte|sbyte] with simple arrays instead. +/// +internal sealed class Arrays +{ + private static readonly string[] _collectionTypes = new[] + { + "System.Collections.Generic.Dictionary`2", + "System.Collections.Generic.HashSet`1", + "System.Collections.Generic.SortedDictionary`2", + "System.Collections.Generic.SortedSet`1", + "System.Collections.Immutable.ImmutableDictionary`2", + "System.Collections.Immutable.ImmutableHashSet`1", + "System.Collections.Immutable.ImmutableSortedDictionary`2", + "System.Collections.Immutable.ImmutableSortedSet`1", + "System.Collections.Frozen.FrozenDictionary`2", + "System.Collections.Frozen.FrozenSet`1", + }; + + private static readonly Dictionary _collectionFactories = new() + { + ["System.Collections.Immutable.ImmutableDictionary"] = new[] + { + "Create", + "CreateRange", + }, + + ["System.Collections.Immutable.ImmutableHashSet"] = new[] + { + "Create", + "CreateRange", + }, + + ["System.Collections.Immutable.ImmutableSortedDictionary"] = new[] + { + "Create", + "CreateRange", + }, + + ["System.Collections.Immutable.ImmutableSortedSet"] = new[] + { + "Create", + "CreateRange", + }, + + ["System.Collections.Immutable.ImmutableDictionary`2+Builder"] = new[] + { + "ToImmutable", + }, + + ["System.Collections.Immutable.ImmutableHashSet`1+Builder"] = new[] + { + "ToImmutable", + }, + + ["System.Collections.Immutable.ImmutableSortedDictionary`2+Builder"] = new[] + { + "ToImmutable", + }, + + ["System.Collections.Immutable.ImmutableSortedSet`1+Builder"] = new[] + { + "ToImmutable", + }, + }; + + public Arrays(CallAnalyzer.Registrar reg) + { + reg.RegisterConstructors(_collectionTypes, HandleConstructor); + reg.RegisterMethods(_collectionFactories, HandleMethod); + + var freezer = reg.Compilation.GetTypeByMetadataName("Microsoft.Extensions.Collections.Frozen.Freezer"); + if (freezer != null) + { + foreach (var method in freezer.GetMembers("ToFrozenDictionary").OfType().Where(m => m.TypeParameters.Length == 2)) + { + reg.RegisterMethod(method, HandleMethod); + } + + foreach (var method in freezer.GetMembers("ToFrozenSet").OfType().Where(m => m.TypeParameters.Length == 1)) + { + reg.RegisterMethod(method, HandleMethod); + } + } + + static void HandleMethod(OperationAnalysisContext context, IInvocationOperation op) => HandleSuspectType(context, (INamedTypeSymbol)op.TargetMethod.ReturnType, op.Syntax.GetLocation()); + + static void HandleConstructor(OperationAnalysisContext context, IObjectCreationOperation op) => HandleSuspectType(context, (INamedTypeSymbol)op.Type!, op.Syntax.GetLocation()); + + static void HandleSuspectType(OperationAnalysisContext context, INamedTypeSymbol type, Location loc) + { + var keyType = type.TypeArguments[0]; + if (keyType.TypeKind == TypeKind.Enum + || keyType.SpecialType == SpecialType.System_Byte + || keyType.SpecialType == SpecialType.System_SByte) + { + if (keyType.TypeKind == TypeKind.Enum) + { + var flagsAttr = context.Compilation.GetTypeByMetadataName("System.FlagsAttribute"); + if (keyType.GetAttributes().Any(a => a.AttributeClass != null && SymbolEqualityComparer.Default.Equals(a.AttributeClass, flagsAttr))) + { + // not for [Flags] enums + return; + } + } + + var valueType = keyType; + if (type.TypeArguments.Length == 2) + { + valueType = type.TypeArguments[1]; + } + + var diagnostic = Diagnostic.Create(DiagDescriptors.Arrays, loc, valueType.ToDisplayString(), type.ToDisplayString()); + context.ReportDiagnostic(diagnostic); + } + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/CallAnalyzer.Handlers.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/CallAnalyzer.Handlers.cs new file mode 100644 index 0000000000..7766ad39d0 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/CallAnalyzer.Handlers.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.ExtraAnalyzers.CallAnalysis; + +public partial class CallAnalyzer +{ + private sealed class Handlers + { + private readonly State _state; + + public Handlers(State state) + { + _state = state; + } + + public void HandleInvocation(OperationAnalysisContext context) + { + var op = (IInvocationOperation)context.Operation; + var target = op.TargetMethod; + + if (target != null) + { + if (_state.Methods.TryGetValue(target.OriginalDefinition, out var handlers)) + { + if (op.Arguments.Length == target.Parameters.Length) + { + foreach (var handler in handlers) + { + handler(context, op); + } + } + } + + if (_state.InterfaceMethodNames.Contains(target.Name)) + { + var type = target.ContainingType; + if (type.TypeKind == TypeKind.Interface) + { + if (_state.Interfaces.TryGetValue(type, out var l)) + { + foreach (var h in l) + { + if (SymbolEqualityComparer.Default.Equals(target, h.Method)) + { + foreach (var action in h.Actions) + { + action(context, op); + } + } + } + } + } + else + { + foreach (var iface in type.AllInterfaces) + { + if (_state.Interfaces.TryGetValue(iface, out var l)) + { + foreach (var h in l) + { + var impl = type.FindImplementationForInterfaceMember(h.Method); + if (SymbolEqualityComparer.Default.Equals(target, impl)) + { + foreach (var action in h.Actions) + { + action(context, op); + } + } + } + } + } + } + } + } + } + + public void HandleObjectCreation(OperationAnalysisContext context) + { + var op = (IObjectCreationOperation)context.Operation; + if (op.Constructor != null) + { + if (_state.Ctors.TryGetValue(op.Constructor.OriginalDefinition, out var handlers)) + { + if (op.Arguments.Length == op.Constructor.Parameters.Length) + { + foreach (var handler in handlers) + { + handler(context, op); + } + } + } + } + } + + public void HandlePropertyReference(OperationAnalysisContext context) + { + var op = (IPropertyReferenceOperation)context.Operation; + if (_state.Props.TryGetValue(op.Property, out var handlers)) + { + foreach (var handler in handlers) + { + handler(context, op); + } + } + } + + public void HandleThrow(OperationAnalysisContext context) + { + var op = (IThrowOperation)context.Operation; + + if (op.Exception is IConversionOperation convOp) + { + if (convOp.Operand is IObjectCreationOperation creationOp) + { + if (creationOp.Type != null) + { + if (_state.ExceptionTypes.TryGetValue(creationOp.Type, out var handlers)) + { + foreach (var handler in handlers) + { + handler(context, op); + } + } + } + } + } + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/CallAnalyzer.Registrar.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/CallAnalyzer.Registrar.cs new file mode 100644 index 0000000000..61b0b7bf1b --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/CallAnalyzer.Registrar.cs @@ -0,0 +1,241 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.ExtraAnalyzers.CallAnalysis; + +public partial class CallAnalyzer +{ + /// + /// Enables call analysis classes to register callbacks. + /// + internal sealed class Registrar + { + private readonly State _state; + + internal Registrar(State state, Compilation compilation) + { + _state = state; + Compilation = compilation; + } + + /// + /// Registers a callback to be invoked whenever the given method is invoked directly in code. + /// + /// + /// Note that this is not designed for use with interface methods. + /// + public void RegisterMethod(IMethodSymbol method, Action action) + { + if (!_state.Methods.TryGetValue(method, out var l)) + { + l = new(); + _state.Methods.Add(method, l); + } + + l.Add(action); + } + + /// + /// Registers a callback to be invoked whenever the given method overloads are invoked directly in code. + /// + /// + /// Note that this is not designed for use with interface methods. + /// + public void RegisterMethods(string typeName, string methodName, Action action) + { + var dict = new Dictionary + { + { typeName, new[] { methodName } }, + }; + + RegisterMethods(dict, action); + } + + /// + /// Registers a callback to be invoked whenever any of the specified methods are invoked. + /// + /// + /// The input dictionary has type names as keys, and arrays of method names as values. + /// + public void RegisterMethods(Dictionary methods, Action action) + { + foreach (var pair in methods) + { + var type = Compilation.GetTypeByMetadataName(pair.Key); + if (type != null) + { + foreach (var m in pair.Value) + { + foreach (var method in type.GetMembers(m).OfType()) + { + RegisterMethod(method, action); + } + } + } + } + } + + /// + /// Registers a callback to be invoked whenever the specified constructor is invoked. + /// + public void RegisterConstructor(IMethodSymbol ctor, Action action) + { + if (!_state.Ctors.TryGetValue(ctor, out var l)) + { + l = new(); + _state.Ctors.Add(ctor, l); + } + + l.Add(action); + } + + /// + /// Registers a callback to be invoked whenever constructors for the given type are invoked. + /// + public void RegisterConstructors(string typeName, Action action) + { + RegisterConstructors(new[] { typeName }, action); + } + + /// + /// Registers a callback to be invoked whenever constructors for any of the given types are invoked. + /// + public void RegisterConstructors(string[] typeNames, Action action) + { + foreach (var typeName in typeNames) + { + var type = Compilation.GetTypeByMetadataName(typeName); + if (type != null) + { + foreach (var ctor in type.Constructors) + { + RegisterConstructor(ctor, action); + } + } + } + } + + /// + /// Registers a callback to be invoked whenever the given property is invoked (set or get). + /// + public void RegisterProperty(IPropertySymbol prop, Action action) + { + if (!_state.Props.TryGetValue(prop, out var l)) + { + l = new(); + _state.Props.Add(prop, l); + } + + l.Add(action); + } + + /// + /// Registers a callback to be invoked whenever any of the given properties are invoked (set or get). + /// + /// + /// The input dictionary has type names as keys, and arrays of method names as values. + /// + public void RegisterProperties(Dictionary props, Action action) + { + foreach (var pair in props) + { + var type = Compilation.GetTypeByMetadataName(pair.Key); + if (type != null) + { + foreach (var m in pair.Value) + { + foreach (var prop in type.GetMembers(m).OfType()) + { + RegisterProperty(prop, action); + } + } + } + } + } + + /// + /// Registers a callback to be invoked whenever the given interface method is invoked. + /// + public void RegisterInterfaceMethod(IMethodSymbol method, Action action) + { + if (!_state.Interfaces.TryGetValue(method.ContainingType, out var handlers)) + { + handlers = new(); + _state.Interfaces.Add(method.ContainingType, handlers); + } + + bool found = false; + foreach (var h in handlers) + { + if (SymbolEqualityComparer.Default.Equals(h.Method, method)) + { + h.Actions.Add(action); + found = true; + break; + } + } + + if (!found) + { + var h = new MethodHandlers(method); + h.Actions.Add(action); + handlers.Add(h); + } + + _ = _state.InterfaceMethodNames.Add(method.Name); + } + + /// + /// Registers a callback to be invoked whenever any of the given interface methods are invoked. + /// + /// + /// The input dictionary has type names as keys, and arrays of method names as values. + /// + public void RegisterInterfaceMethods(Dictionary methods, Action action) + { + foreach (var pair in methods) + { + var type = Compilation.GetTypeByMetadataName(pair.Key); + if (type != null) + { + foreach (var m in pair.Value) + { + foreach (var method in type.GetMembers(m).OfType()) + { + RegisterInterfaceMethod(method, action); + } + } + } + } + } + + /// + /// Registers a callback to be invoked whenever any of the given exception types are thrown. + /// + public void RegisterExceptionTypes(string[] exceptionTypes, Action action) + { + foreach (var et in exceptionTypes) + { + var type = Compilation.GetTypeByMetadataName(et); + if (type != null) + { + if (!_state.ExceptionTypes.TryGetValue(type, out var l)) + { + l = new(); + _state.ExceptionTypes.Add(type, l); + } + + l.Add(action); + } + } + } + + public Compilation Compilation { get; } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/CallAnalyzer.State.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/CallAnalyzer.State.cs new file mode 100644 index 0000000000..6db42e7f34 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/CallAnalyzer.State.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.ExtraAnalyzers.CallAnalysis; + +public partial class CallAnalyzer +{ + internal sealed class State + { + public readonly Dictionary>> Methods = new(SymbolEqualityComparer.Default); + public readonly Dictionary>> Ctors = new(SymbolEqualityComparer.Default); + public readonly Dictionary>> Props = new(SymbolEqualityComparer.Default); + public readonly Dictionary>> ExceptionTypes = new(SymbolEqualityComparer.Default); + public readonly Dictionary> Interfaces = new(SymbolEqualityComparer.Default); + public readonly HashSet InterfaceMethodNames = new(); + } + + internal sealed class MethodHandlers + { + public MethodHandlers(IMethodSymbol method) + { + Method = method; + } + + public IMethodSymbol Method { get; } + public List> Actions { get; } = new(); + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/CallAnalyzer.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/CallAnalyzer.cs new file mode 100644 index 0000000000..5b93ff3d59 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/CallAnalyzer.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.Extensions.ExtraAnalyzers.CallAnalysis; + +/// +/// Composite analyzer that efficiently inspects various types of method/ctor/property calls. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed partial class CallAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + DiagDescriptors.StartsEndsWith, + DiagDescriptors.LegacyLogging, + DiagDescriptors.StaticTime, + DiagDescriptors.StringFormat, + DiagDescriptors.EnumStrings, + DiagDescriptors.ValueTuple, + DiagDescriptors.Arrays, + DiagDescriptors.NullCheck, + DiagDescriptors.LegacyCollection, + DiagDescriptors.Split); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationStartContext => + { + var state = new State(); + + var reg = new Registrar(state, compilationStartContext.Compilation); + + _ = new Arrays(reg); + _ = new EnumStrings(reg); + _ = new LegacyLogging(reg); + _ = new StartsEndsWith(reg); + _ = new StaticTime(reg); + _ = new ValueTuple(reg); + _ = new StringFormat(reg); + _ = new LegacyCollection(reg); + _ = new Split(reg); + + if (compilationStartContext.Compilation.Options.NullableContextOptions.WarningsEnabled()) + { + _ = new NullChecks(reg); + } + + var handlers = new Handlers(state); + compilationStartContext.RegisterOperationAction(handlers.HandleInvocation, OperationKind.Invocation); + compilationStartContext.RegisterOperationAction(handlers.HandleObjectCreation, OperationKind.ObjectCreation); + compilationStartContext.RegisterOperationAction(handlers.HandlePropertyReference, OperationKind.PropertyReference); + compilationStartContext.RegisterOperationAction(handlers.HandleThrow, OperationKind.Throw); + }); + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/EnumStrings.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/EnumStrings.cs new file mode 100644 index 0000000000..24ac8a9e83 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/EnumStrings.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.ExtraAnalyzers.CallAnalysis; + +/// +/// Recommends switch to faster alternatives to Enum.GetName and Enum.ToString. +/// +internal sealed class EnumStrings +{ + public EnumStrings(CallAnalyzer.Registrar reg) + { + reg.RegisterMethods("System.Enum", "GetName", HandleGetName); + reg.RegisterMethods("System.Enum", "ToString", HandleToString); + + static void HandleToString(OperationAnalysisContext context, IInvocationOperation op) + { + var inst = op.Instance; + if (inst != null && inst.Kind == OperationKind.FieldReference) + { + var fieldRef = (IFieldReferenceOperation)inst; + if (fieldRef.Field.Type.TypeKind == TypeKind.Enum) + { + var d = Diagnostic.Create(DiagDescriptors.EnumStrings, op.Syntax.GetLocation(), "'nameof'", "Enum.ToString"); + context.ReportDiagnostic(d); + return; + } + } + + var diagnostic = Diagnostic.Create(DiagDescriptors.EnumStrings, op.Syntax.GetLocation(), "the '[EnumStrings]' code generator", "Enum.ToString"); + context.ReportDiagnostic(diagnostic); + } + + static void HandleGetName(OperationAnalysisContext context, IInvocationOperation op) + { + var diagnostic = Diagnostic.Create(DiagDescriptors.EnumStrings, op.Syntax.GetLocation(), "the '[EnumStrings] code generator", "Enum.GetName"); + context.ReportDiagnostic(diagnostic); + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/Fixers/LegacyLoggingFixer.FixDetails.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/Fixers/LegacyLoggingFixer.FixDetails.cs new file mode 100644 index 0000000000..f58afcee03 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/Fixers/LegacyLoggingFixer.FixDetails.cs @@ -0,0 +1,345 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.ExtraAnalyzers; + +public sealed partial class LegacyLoggingFixer +{ + /// + /// Tracks a bunch of metadata about a potential fix to apply. + /// + internal sealed class FixDetails + { + public int MessageParamIndex { get; } + public int ExceptionParamIndex { get; } + public int EventIdParamIndex { get; } + public int LogLevelParamIndex { get; } + public int ArgsParamIndex { get; } + public string Message { get; } = string.Empty; + public string Level { get; } = string.Empty; + public string TargetFilename { get; } + public string TargetNamespace { get; } + public string TargetClassName { get; } + public string TargetMethodName { get; } + public IReadOnlyList MessageArgs { get; } + public IReadOnlyList? InterpolationArgs { get; } + + public FixDetails( + IMethodSymbol method, + IInvocationOperation invocationOp, + string? defaultNamespace, + IEnumerable docs) + { + (MessageParamIndex, ExceptionParamIndex, EventIdParamIndex, LogLevelParamIndex, ArgsParamIndex) = IdentifyParameters(method); + + if (MessageParamIndex >= 0) + { + var op = invocationOp.Arguments[MessageParamIndex]; + var children = op.Children.ToArray(); + if (children.Length == 1) + { + if (children[0].ConstantValue.HasValue) + { + Message = children[0].ConstantValue.Value as string ?? string.Empty; + } + else if (children[0] is IInterpolatedStringOperation inter) + { + var interpolationArgs = new List(); + var messageArgs = new List(); + InterpolationArgs = interpolationArgs; + MessageArgs = messageArgs; + + var sb = new StringBuilder(); + int argCount = 0; + foreach (var o in children[0].Children) + { + switch (o) + { + case IInterpolatedStringTextOperation stringOp: + _ = sb.Append(stringOp.Children.First().ConstantValue.Value as string); + break; + + case IInterpolationOperation intOp: + var operation = intOp.Children.First(); + var argName = operation.Syntax.GetLastToken().Text; + + if (argName.Length == 0) + { + argName = $"_arg{argCount++}"; + } + else + { + char firstChar = argName[0]; + if (firstChar >= 'A' && firstChar <= 'Z') + { + argName = (char)(firstChar + ('a' - 'A')) + argName.Substring(1); + } + else if (firstChar < 'a' || firstChar > 'z') + { + argName = $"_arg{argCount++}"; + } + } + + _ = sb.Append("{" + argName + "}"); + messageArgs.Add(argName); + interpolationArgs.Add(intOp.Children.First()); + break; + } + } + + Message = sb.ToString(); + } + } + } + + if (LogLevelParamIndex > 0) + { + object? value = null; + + var op = invocationOp.Arguments[LogLevelParamIndex].Descendants().SingleOrDefault(x => x.Kind == OperationKind.Literal || x.Kind == OperationKind.FieldReference); + switch (op) + { + case ILiteralOperation lit: + value = lit.ConstantValue.Value; + break; + + case IFieldReferenceOperation fieldRef: + value = fieldRef.ConstantValue.Value; + break; + } + + if (value is int) + { + Level = GetLogLevelName((LogLevel)value); + } + } + else + { + Level = method.Name.Substring("Log".Length); + } + + TargetFilename = FindUniqueFilename(docs); + TargetNamespace = defaultNamespace ?? string.Empty; + TargetClassName = "Log"; + TargetMethodName = DeriveName(Message); + MessageArgs ??= ExtractTemplateArgs(Message); + } + + public string FullTargetClassName + { + get + { + if (string.IsNullOrEmpty(TargetNamespace)) + { + return TargetClassName; + } + + return $"{TargetNamespace}.{TargetClassName}"; + } + } + + internal static string GetLogLevelName(LogLevel value) + { + return value switch + { + LogLevel.Trace => "Trace", + LogLevel.Debug => "Debug", + LogLevel.Information => "Information", + LogLevel.Warning => "Warning", + LogLevel.Error => "Error", + LogLevel.Critical => "Critical", + _ => string.Empty, + }; + } + + private static string FindUniqueFilename(IEnumerable docs) + { + var targetName = "Log.cs"; + int count = 2; + bool duplicate; + do + { + duplicate = false; + foreach (var doc in docs) + { + if (string.Equals(doc.Name, targetName, StringComparison.OrdinalIgnoreCase)) + { + duplicate = true; + targetName = $"Log{count}.cs"; + count++; + break; + } + } + } + while (duplicate); + + return targetName; + } + + /// + /// Finds the position of the well-known parameters of legacy logging methods. + /// + /// -1 for any parameter not present in the given overload. + private static (int message, int exception, int eventId, int logLevel, int args) IdentifyParameters(IMethodSymbol method) + { + var message = -1; + var exception = -1; + var eventId = -1; + var logLevel = -1; + var args = -1; + + int index = 0; + foreach (var p in method.Parameters) + { + switch (p.Name) + { + case "message": + message = index; + break; + case "exception": + exception = index; + break; + case "eventId": + eventId = index; + break; + case "logLevel": + logLevel = index; + break; + case "args": + args = index; + break; + } + + index++; + } + + return (message, exception, eventId, logLevel, args); + } + + /// + /// Given a logging message with template args, generate a reasonable logging method name. + /// + private static string DeriveName(string message) + { + var sb = new StringBuilder(); + bool capitalizeNext = true; + foreach (var ch in message) + { + if (char.IsLetter(ch) || (char.IsLetterOrDigit(ch) && sb.Length > 1)) + { + if (capitalizeNext) + { + _ = sb.Append(char.ToUpperInvariant(ch)); + capitalizeNext = false; + } + else + { + _ = sb.Append(ch); + } + } + else + { + capitalizeNext = true; + } + } + + return sb.ToString(); + } + + private static readonly char[] _formatDelimiters = { ',', ':' }; + + /// + /// Finds the template arguments contained in the message string. + /// + private static List ExtractTemplateArgs(string message) + { + var args = new List(); + var scanIndex = 0; + var endIndex = message.Length; + + while (scanIndex < endIndex) + { + var openBraceIndex = FindBraceIndex(message, '{', scanIndex, endIndex); + var closeBraceIndex = FindBraceIndex(message, '}', openBraceIndex, endIndex); + + if (closeBraceIndex == endIndex) + { + scanIndex = endIndex; + } + else + { + // Format item syntax : { index[,alignment][ :formatString] }. + var formatDelimiterIndex = FindIndexOfAny(message, _formatDelimiters, openBraceIndex, closeBraceIndex); + + args.Add(message.Substring(openBraceIndex + 1, formatDelimiterIndex - openBraceIndex - 1)); + scanIndex = closeBraceIndex + 1; + } + } + + return args; + } + + private static int FindBraceIndex(string message, char brace, int startIndex, int endIndex) + { + // Example: {{prefix{{{Argument}}}suffix}}. + var braceIndex = endIndex; + var scanIndex = startIndex; + var braceOccurrenceCount = 0; + + while (scanIndex < endIndex) + { + if (braceOccurrenceCount > 0 && message[scanIndex] != brace) + { +#pragma warning disable S109 // Magic numbers should not be used + if (braceOccurrenceCount % 2 == 0) +#pragma warning restore S109 // Magic numbers should not be used + { + // Even number of '{' or '}' found. Proceed search with next occurrence of '{' or '}'. + braceOccurrenceCount = 0; + braceIndex = endIndex; + } + else + { + // An unescaped '{' or '}' found. + break; + } + } + else if (message[scanIndex] == brace) + { + if (brace == '}') + { + if (braceOccurrenceCount == 0) + { + // For '}' pick the first occurrence. + braceIndex = scanIndex; + } + } + else + { + // For '{' pick the last occurrence. + braceIndex = scanIndex; + } + + braceOccurrenceCount++; + } + + scanIndex++; + } + + return braceIndex; + } + + private static int FindIndexOfAny(string message, char[] chars, int startIndex, int endIndex) + { + var findIndex = message.IndexOfAny(chars, startIndex, endIndex - startIndex); + return findIndex == -1 ? endIndex : findIndex; + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/Fixers/LegacyLoggingFixer.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/Fixers/LegacyLoggingFixer.cs new file mode 100644 index 0000000000..3de8a6afbf --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/Fixers/LegacyLoggingFixer.cs @@ -0,0 +1,640 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.Extensions.ExtraAnalyzers; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LegacyLoggingFixer))] +[Shared] +public sealed partial class LegacyLoggingFixer : CodeFixProvider +{ + // mimics the definition from Microsoft.Extensions.Logging.Abstractions + internal enum LogLevel + { + Trace = 0, + Debug = 1, + Information = 2, + Warning = 3, + Error = 4, + Critical = 5, + None = 6, + } + + // function pointers that can be patched by test code to exercise obscure failure paths + internal Func> GetSyntaxRootAsync = (d, t) => d.GetSyntaxRootAsync(t); + internal Func> GetSemanticModelAsync = (d, t) => d.GetSemanticModelAsync(t); + internal Func GetOperation = (sm, sn, t) => sm.GetOperation(sn, t); + internal Func GetTypeByMetadataName1 = (c, n) => c.GetTypeByMetadataName(n); + internal Func GetTypeByMetadataName2 = (c, n) => c.GetTypeByMetadataName(n); + internal Func GetTypeByMetadataName3 = (c, n) => c.GetTypeByMetadataName(n); + internal Func GetDeclaredSymbol = (sm, m, t) => sm.GetDeclaredSymbol(m, t); + + private const string LogMethodAttribute = "Microsoft.Extensions.Telemetry.Logging.LogMethodAttribute"; + private const int LogMethodAttrEventIdArg = 0; + private const int LogMethodAttrLevelArg = 1; + private const int LogMethodAttrMessageArg = 2; + + /// + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DiagDescriptors.LegacyLogging.Id); + + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var (invocationExpression, details) = await CheckIfCanFixAsync(context.Document, context.Span, context.CancellationToken).ConfigureAwait(false); + if (invocationExpression != null && details != null) + { + context.RegisterCodeFix( + CodeAction.Create( + title: Resources.GenerateStronglyTypedLoggingMethod, + createChangedSolution: cancellationToken => ApplyFixAsync(context.Document, invocationExpression, details, cancellationToken), + equivalenceKey: nameof(Resources.GenerateStronglyTypedLoggingMethod)), + context.Diagnostics); + } + } + + internal async Task<(ExpressionSyntax? invocationExpression, FixDetails? details)> + CheckIfCanFixAsync(Document invocationDoc, TextSpan span, CancellationToken cancellationToken) + { + var root = await GetSyntaxRootAsync(invocationDoc, cancellationToken).ConfigureAwait(false); + if (root?.FindNode(span) is not ExpressionSyntax invocationExpression) + { + // shouldn't happen, we only get called for invocations + return (null, null); + } + + var sm = await GetSemanticModelAsync(invocationDoc, cancellationToken).ConfigureAwait(false); + if (sm == null) + { + // shouldn't happen + return (null, null); + } + + var comp = sm.Compilation; + + var loggerExtensions = GetTypeByMetadataName1(comp, "Microsoft.Extensions.Logging.LoggerExtensions"); + if (loggerExtensions == null) + { + // shouldn't happen, we only get called for methods on this type + return (null, null); + } + + var invocationOp = GetOperation(sm, invocationExpression, cancellationToken) as IInvocationOperation; + if (invocationOp == null) + { + // shouldn't happen, we're dealing with an invocation expression + return (null, null); + } + + var method = invocationOp.TargetMethod; + + var details = new FixDetails(method, invocationOp, invocationDoc.Project.DefaultNamespace, invocationDoc.Project.Documents); + + if (string.IsNullOrWhiteSpace(details.Message)) + { + // can't auto-generate without a valid message string + return (null, null); + } + + if (details.EventIdParamIndex >= 0) + { + // can't auto-generate the variants using event id + return (null, null); + } + + if (string.IsNullOrWhiteSpace(details.Level)) + { + // can't auto-generate without a valid level + return (null, null); + } + + return (invocationExpression, details); + } + + /// + /// Get the final name of the target method. If there's an existing method with the right + /// message, level, and argument types, we just use that. Otherwise, we create a new method. + /// + internal async Task<(string methodName, bool existing)> GetFinalTargetMethodNameAsync( + Document targetDoc, + ClassDeclarationSyntax targetClass, + Document invocationDoc, + ExpressionSyntax invocationExpression, + FixDetails details, + CancellationToken cancellationToken) + { + var invocationSM = (await invocationDoc.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false))!; + var invocationOp = (invocationSM.GetOperation(invocationExpression, cancellationToken) as IInvocationOperation)!; + + var docEditor = await DocumentEditor.CreateAsync(targetDoc, cancellationToken).ConfigureAwait(false); + var sm = docEditor.SemanticModel; + var comp = sm.Compilation; + + var logMethodAttribute = GetTypeByMetadataName2(comp, LogMethodAttribute); + if (logMethodAttribute is null) + { + // strange that we can't find the attribute, but supply a potential useful value instead + return (details.TargetMethodName, false); + } + + var invocationArgList = MakeArgumentList(details, invocationOp); + + var conflict = false; + var count = 2; + string methodName; + do + { + methodName = details.TargetMethodName; + if (conflict) + { + methodName = $"{methodName}{count}"; + count++; + conflict = false; + } + + foreach (var method in targetClass.Members.Where(m => m.IsKind(SyntaxKind.MethodDeclaration)).OfType()) + { + var methodSymbol = GetDeclaredSymbol(sm, method, cancellationToken); + if (methodSymbol == null) + { + // hmmm, this shouldn't happen should it? + continue; + } + + var matchName = method.Identifier.ToString() == methodName; + + var matchParams = invocationArgList.Count == methodSymbol.Parameters.Length; + if (matchParams) + { + for (int i = 0; i < invocationArgList.Count; i++) + { + matchParams = invocationArgList[i].Equals(methodSymbol.Parameters[i].Type, SymbolEqualityComparer.Default); + if (!matchParams) + { + break; + } + } + } + + if (matchName && matchParams) + { + conflict = true; + } + + foreach (var mal in method.AttributeLists) + { + foreach (var ma in mal.Attributes) + { + var mattrSymbolInfo = sm.GetSymbolInfo(ma, cancellationToken); + if (mattrSymbolInfo.Symbol is IMethodSymbol ms) + { + if (logMethodAttribute.Equals(ms.ContainingType, SymbolEqualityComparer.Default)) + { + var arg = ma.ArgumentList!.Arguments[LogMethodAttrLevelArg]; + var level = (LogLevel)sm.GetConstantValue(arg.Expression, cancellationToken).Value!; + + arg = ma.ArgumentList.Arguments[LogMethodAttrMessageArg]; + var message = sm.GetConstantValue(arg.Expression, cancellationToken).ToString(); + + var matchMessage = message == details.Message; + var matchLevel = FixDetails.GetLogLevelName(level) == details.Level; + + if (matchLevel && matchMessage && matchParams) + { + // found a match, use this one + return (method.Identifier.ToString(), true); + } + + break; + } + } + } + } + } + } + while (conflict); + + return (methodName, false); + } + + /// + /// Finds the class into which to create the logging method signature, or creates it if it doesn't exist. + /// + private static async Task<(Solution solution, ClassDeclarationSyntax declarationSyntax, Document document)> + GetOrMakeTargetClassAsync(Project proj, FixDetails details, CancellationToken cancellationToken) + { + while (true) + { + var comp = (await proj.GetCompilationAsync(cancellationToken).ConfigureAwait(false))!; + var allNodes = comp.SyntaxTrees.SelectMany(s => s.GetRoot().DescendantNodes()); + var allClasses = allNodes.Where(d => d.IsKind(SyntaxKind.ClassDeclaration)).OfType(); + foreach (var cl in allClasses) + { + var nspace = GetNamespace(cl); + if (nspace != details.TargetNamespace) + { + continue; + } + + if (cl.Identifier.Text == details.TargetClassName) + { + return (proj.Solution, cl, proj.GetDocument(cl.SyntaxTree)!); + } + } + + var text = $@" +#pragma warning disable CS8019 +using Microsoft.Extensions.Logging; +using System; +#pragma warning restore CS8019 + +static partial class {details.TargetClassName} +{{ +}} +"; + + if (!string.IsNullOrEmpty(details.TargetNamespace)) + { + text = $@" +namespace {details.TargetNamespace} +{{ +#pragma warning disable CS8019 + using Microsoft.Extensions.Logging; + using System; +#pragma warning restore CS8019 + + static partial class {details.TargetClassName} + {{ + }} +}} +"; + } + + proj = proj.AddDocument(details.TargetFilename, text).Project; + } + } + + /// + /// Remaps an invocation expression to a new doc. + /// + private static async Task<(Document document, ExpressionSyntax expressionSyntax)> + RemapAsync(Solution sol, DocumentId docId, ExpressionSyntax invocationExpression) + { + var doc = sol.GetDocument(docId)!; + var root = await doc.GetSyntaxRootAsync().ConfigureAwait(false); + + return (doc, (root!.FindNode(invocationExpression.Span) as ExpressionSyntax)!); + } + + private static string GetNamespace(ClassDeclarationSyntax cl) + { +#if ROSLYN_4_0_OR_GREATER + var ns = cl.Parent as BaseNamespaceDeclarationSyntax; +#else + var ns = cl.Parent as NamespaceDeclarationSyntax; +#endif + if (ns == null) + { + if (cl.Parent is not CompilationUnitSyntax) + { + // nested type, we don't do those + return "<+Invalid Namespace+>"; + } + + return string.Empty; + } + + var nspace = ns.Name.ToString(); + while (true) + { +#if ROSLYN_4_0_OR_GREATER + ns = ns.Parent as BaseNamespaceDeclarationSyntax; +#else + ns = ns.Parent as NamespaceDeclarationSyntax; +#endif + + if (ns == null) + { + break; + } + + nspace = $"{ns.Name}.{nspace}"; + } + + return nspace; + } + + /// + /// Given a LoggerExtensions method invocation, produce a parameter list for the corresponding generated logging method. + /// + private static IReadOnlyList MakeParameterList( + FixDetails details, + IInvocationOperation invocationOp, + SyntaxGenerator gen) + { + var t = invocationOp.Arguments[0].Value.Type!; + var loggerType = gen.TypeExpression(t); + if (invocationOp.Parent?.Kind == OperationKind.ConditionalAccess) + { + loggerType = gen.TypeExpression(t.WithNullableAnnotation(NullableAnnotation.Annotated)); + } + + var loggerParam = gen.ParameterDeclaration("logger", loggerType); + if (loggerParam is ParameterSyntax parameterSyntax) + { + loggerParam = parameterSyntax.WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.ThisKeyword))); + } + + var parameters = new List + { + loggerParam + }; + + if (details.ExceptionParamIndex >= 0) + { + parameters.Add(gen.ParameterDeclaration("exception", gen.TypeExpression(invocationOp.Arguments[details.ExceptionParamIndex].Value.Type))); + } + + var index = 0; + if (details.InterpolationArgs != null) + { + foreach (var o in details.InterpolationArgs) + { + parameters.Add(gen.ParameterDeclaration(details.MessageArgs[index++], gen.TypeExpression(o.Type))); + } + } + + var paramsArg = invocationOp.Arguments[details.ArgsParamIndex]; + if (paramsArg != null) + { + var arrayCreation = (IArrayCreationOperation)paramsArg.Value; + foreach (var e in arrayCreation.Initializer!.ElementValues) + { + var type = e.SemanticModel?.GetTypeInfo(e.Syntax).Type; + + string name; + if (index < details.MessageArgs.Count) + { + name = details.MessageArgs[index]; + } + else + { + name = $"arg{index}"; + } + + parameters.Add(gen.ParameterDeclaration(name, gen.TypeExpression(type))); + index++; + } + } + + return parameters; + } + + /// + /// Given a LoggerExtensions method invocation, produce an argument list in the shape of a corresponding generated logging method. + /// + private static IReadOnlyList MakeArgumentList(FixDetails details, IInvocationOperation invocationOp) + { + var args = new List + { + invocationOp.Arguments[0].Value.Type! + }; + + if (details.ExceptionParamIndex >= 0) + { + args.Add(invocationOp.Arguments[details.ExceptionParamIndex].Value.Type!); + } + + if (details.InterpolationArgs != null) + { + foreach (var a in details.InterpolationArgs) + { + args.Add(a.Type!); + } + } + + var paramsArg = invocationOp.Arguments[details.ArgsParamIndex]; + if (paramsArg != null) + { + var arrayCreation = (IArrayCreationOperation)paramsArg.Value; + foreach (var e in arrayCreation.Initializer!.ElementValues) + { + foreach (var d in e.Descendants()) + { + args.Add(d.Type!); + } + } + } + + return args; + } + + private static async Task RewriteLoggingCallAsync( + Document doc, + ExpressionSyntax invocationExpression, + FixDetails details, + string methodName, + CancellationToken cancellationToken) + { + var solEditor = new SolutionEditor(doc.Project.Solution); + var docEditor = await solEditor.GetDocumentEditorAsync(doc.Id, cancellationToken).ConfigureAwait(false); + var sm = docEditor.SemanticModel; + var comp = sm.Compilation; + var gen = docEditor.Generator; + var invocation = sm.GetOperation(invocationExpression, cancellationToken) as IInvocationOperation; + var argList = new List(); + + int index = 0; + SyntaxNode loggerSyntaxNode = null!; + foreach (var arg in invocation!.Arguments) + { + if ((index == details.MessageParamIndex) || (index == details.LogLevelParamIndex)) + { + index++; + continue; + } + + index++; + + if (index == 1) + { + loggerSyntaxNode = arg.Syntax; + } + else + { + if (arg.ArgumentKind == ArgumentKind.ParamArray) + { + if (details.InterpolationArgs != null) + { + foreach (var a in details.InterpolationArgs) + { + argList.Add(a.Syntax.WithoutTrivia()); + } + } + + var arrayCreation = (IArrayCreationOperation)arg.Value; + foreach (var e in arrayCreation.Initializer!.ElementValues) + { + argList.Add(e.Syntax.WithoutTrivia()); + } + } + else + { + argList.Add(arg.Syntax.WithoutTrivia()); + } + } + } + + var memberAccessExpression = gen.MemberAccessExpression(loggerSyntaxNode!, methodName); + var call = gen.InvocationExpression(memberAccessExpression, argList).WithTriviaFrom(invocationExpression); + + if (invocationExpression.Parent!.IsKind(SyntaxKind.ConditionalAccessExpression)) + { + invocationExpression = (ExpressionSyntax)invocationExpression.Parent; + } + + docEditor.ReplaceNode(invocationExpression, call); + + return solEditor.GetChangedSolution(); + } + + /// + /// Orchestrate all the work needed to fix an issue. + /// + private async Task ApplyFixAsync(Document invocationDoc, ExpressionSyntax invocationExpression, FixDetails details, CancellationToken cancellationToken) + { + ClassDeclarationSyntax targetClass; + Document targetDoc; + Solution sol; + + // stable id surviving across solution generations + var invocationDocId = invocationDoc.Id; + + // get a reference to the class where to insert the logging method, creating it if necessary + (sol, targetClass, targetDoc) = await GetOrMakeTargetClassAsync(invocationDoc.Project, details, cancellationToken).ConfigureAwait(false); + + // find the doc and invocation in the current solution + (invocationDoc, invocationExpression) = await RemapAsync(sol, invocationDocId, invocationExpression).ConfigureAwait(false); + + // determine the final name of the logging method and whether we need to generate it or not + var (methodName, existing) = await GetFinalTargetMethodNameAsync(targetDoc, targetClass, invocationDoc, invocationExpression, details, cancellationToken).ConfigureAwait(false); + + // if the target method doesn't already exist, go make it + if (!existing) + { + // generate the logging method signature in the target class + sol = await InsertLoggingMethodSignatureAsync(targetDoc, targetClass, invocationDoc, invocationExpression, details, cancellationToken).ConfigureAwait(false); + + // find the doc and invocation in the current solution + (invocationDoc, invocationExpression) = await RemapAsync(sol, invocationDocId, invocationExpression).ConfigureAwait(false); + } + + // rewrite the call site to invoke the generated logging method + sol = await RewriteLoggingCallAsync(invocationDoc, invocationExpression, details, methodName, cancellationToken).ConfigureAwait(false); + + return sol; + } + + private async Task InsertLoggingMethodSignatureAsync( + Document targetDoc, + ClassDeclarationSyntax targetClass, + Document invocationDoc, + ExpressionSyntax invocationExpression, + FixDetails details, + CancellationToken cancellationToken) + { + var invocationSM = (await invocationDoc.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false))!; + var invocationOp = (invocationSM.GetOperation(invocationExpression, cancellationToken) as IInvocationOperation)!; + + var solEditor = new SolutionEditor(targetDoc.Project.Solution); + var docEditor = await solEditor.GetDocumentEditorAsync(targetDoc.Id, cancellationToken).ConfigureAwait(false); + var sm = docEditor.SemanticModel; + var comp = sm.Compilation; + var gen = docEditor.Generator; + + var logMethod = gen.MethodDeclaration( + details.TargetMethodName, + MakeParameterList(details, invocationOp, gen), + accessibility: Accessibility.Internal, + modifiers: DeclarationModifiers.Partial | DeclarationModifiers.Static); + + var attrArgs = new[] + { + gen.LiteralExpression(CalcEventId(comp, targetClass, cancellationToken)), + gen.MemberAccessExpression(gen.TypeExpression(comp.GetTypeByMetadataName("Microsoft.Extensions.Logging.LogLevel")), details.Level), + gen.LiteralExpression(details.Message), + }; + + var attr = gen.Attribute(LogMethodAttribute, attrArgs); + + logMethod = gen.AddAttributes(logMethod, attr); + + var line = SyntaxFactory.ParseLeadingTrivia($@" +"); + logMethod = logMethod.WithLeadingTrivia(line); + + docEditor.AddMember(targetClass, logMethod); + + return solEditor.GetChangedSolution(); + } + + /// + /// Iterate through the existing methods in the target class + /// and look at any method annotated with [LogMethod], + /// get their event ids, and then return 1 larger than any event id + /// found. + /// + private int CalcEventId(Compilation comp, ClassDeclarationSyntax targetClass, CancellationToken cancellationToken) + { + var logMethodAttribute = GetTypeByMetadataName3(comp, LogMethodAttribute); + if (logMethodAttribute is null) + { + // strange we can't find the attribute, but supply a potential useful value instead + return targetClass.Members.Count + 1; + } + + var max = 0; + foreach (var method in targetClass.Members.Where(m => m.IsKind(SyntaxKind.MethodDeclaration)).OfType()) + { + foreach (var mal in method.AttributeLists) + { + foreach (var ma in mal.Attributes) + { + var sm = comp.GetSemanticModel(ma.SyntaxTree); + var mattrSymbol = sm.GetSymbolInfo(ma, cancellationToken); + if (mattrSymbol.Symbol is IMethodSymbol ms) + { + if (logMethodAttribute.Equals(ms.ContainingType, SymbolEqualityComparer.Default)) + { + var arg = ma.ArgumentList!.Arguments[LogMethodAttrEventIdArg]; + var eventId = (int)(sm.GetConstantValue(arg.Expression, cancellationToken).Value!); + if (eventId >= max) + { + max = eventId + 1; + } + } + } + } + } + } + + return max; + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/LegacyCollection.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/LegacyCollection.cs new file mode 100644 index 0000000000..d95534d9ee --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/LegacyCollection.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.ExtraAnalyzers.CallAnalysis; + +/// +/// Recommends replacing legacy collections with generic ones. +/// +internal sealed class LegacyCollection +{ + private static readonly string[] _collectionTypes = new[] + { + "System.Collections.ArrayList", + "System.Collections.Hashtable", + "System.Collections.Queue", + "System.Collections.Stack", + "System.Collections.SortedList", + "System.Collections.Specialized.HybridDictionary", + "System.Collections.Specialized.ListDictionary", + "System.Collections.Specialized.OrderedDictionary", + }; + + public LegacyCollection(CallAnalyzer.Registrar reg) + { + reg.RegisterConstructors(_collectionTypes, HandleConstructor); + + static void HandleConstructor(OperationAnalysisContext context, IObjectCreationOperation op) + { + var diagnostic = Diagnostic.Create(DiagDescriptors.LegacyCollection, op.Syntax.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/LegacyLogging.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/LegacyLogging.cs new file mode 100644 index 0000000000..d64ef61d2e --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/LegacyLogging.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.ExtraAnalyzers.CallAnalysis; + +/// +/// Recommends replacing legacy logging calls with R9 logging calls. +/// +internal sealed class LegacyLogging +{ + public LegacyLogging(CallAnalyzer.Registrar reg) + { + var loggerExtensions = reg.Compilation.GetTypeByMetadataName("Microsoft.Extensions.Logging.LoggerExtensions"); + if (loggerExtensions != null) + { + var legacyMethods = new List(); + legacyMethods.AddRange(loggerExtensions.GetMembers("LogTrace").OfType()); + legacyMethods.AddRange(loggerExtensions.GetMembers("LogDebug").OfType()); + legacyMethods.AddRange(loggerExtensions.GetMembers("LogInformation").OfType()); + legacyMethods.AddRange(loggerExtensions.GetMembers("LogWarning").OfType()); + legacyMethods.AddRange(loggerExtensions.GetMembers("LogError").OfType()); + legacyMethods.AddRange(loggerExtensions.GetMembers("LogCritical").OfType()); + legacyMethods.AddRange(loggerExtensions.GetMembers("Log").OfType()); + + foreach (var method in legacyMethods) + { + reg.RegisterMethod(method, Handle); + } + } + + static void Handle(OperationAnalysisContext context, IInvocationOperation op) + { + var diagnostic = Diagnostic.Create(DiagDescriptors.LegacyLogging, op.Syntax.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/NullChecks.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/NullChecks.cs new file mode 100644 index 0000000000..fa60c9b322 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/NullChecks.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.Extensions.ExtraAnalyzers.Utilities; + +namespace Microsoft.Extensions.ExtraAnalyzers.CallAnalysis; + +/// +/// Recommends removing null checks from internal-facing functions. +/// +internal sealed class NullChecks +{ + private static readonly Dictionary _nullCheckMethods = new() + { + ["Microsoft.Extensions.Diagnostics.Throws"] = new[] + { + "IfNull", + }, + }; + + public NullChecks(CallAnalyzer.Registrar reg) + { + reg.RegisterMethods(_nullCheckMethods, HandleMethod); + reg.RegisterExceptionTypes(new[] { "System.ArgumentNullException" }, HandleException); + + static void HandleMethod(OperationAnalysisContext context, IInvocationOperation op) => HandleNullCheck(context, op); + + static void HandleException(OperationAnalysisContext context, IThrowOperation op) => HandleNullCheck(context, op); + + static void HandleNullCheck(OperationAnalysisContext context, IOperation op) + { + var method = op.SemanticModel?.GetEnclosingSymbol(op.Syntax.GetLocation().SourceSpan.Start) as IMethodSymbol; + if (method != null) + { + // externally visible methods can have null checks + if (method.IsExternallyVisible()) + { + return; + } + + // see if the method implements any part of any public interface + if (method.ImplementsPublicInterface()) + { + return; + } + + var diagnostic = Diagnostic.Create(DiagDescriptors.NullCheck, op.Syntax.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/Split.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/Split.cs new file mode 100644 index 0000000000..6e83a1df74 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/Split.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.ExtraAnalyzers.CallAnalysis; + +/// +/// Recommends R9's string split functionality instead of String.Split. +/// +internal sealed class Split +{ + public Split(CallAnalyzer.Registrar reg) + { + reg.RegisterMethods("System.String", "Split", Handle); + + static void Handle(OperationAnalysisContext context, IInvocationOperation op) + { + var diagnostic = Diagnostic.Create(DiagDescriptors.Split, op.Syntax.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/StartsEndsWith.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/StartsEndsWith.cs new file mode 100644 index 0000000000..1c51445292 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/StartsEndsWith.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.ExtraAnalyzers.CallAnalysis; + +/// +/// Recommends char (instead of string) versions of String.StartsWith and String.EndsWith when possible. +/// +internal sealed class StartsEndsWith +{ + public StartsEndsWith(CallAnalyzer.Registrar reg) + { + var stringType = reg.Compilation.GetSpecialType(SpecialType.System_String); + var stringCompType = reg.Compilation.GetTypeByMetadataName("System.StringComparison"); + + var startsWith = stringType.GetMembers("StartsWith").OfType() + .Where(m => SymbolEqualityComparer.Default.Equals(m.Parameters[0].Type, stringType)) + .Where(m => + (m.Parameters.Length == 1) || + (m.Parameters.Length == 2 && SymbolEqualityComparer.Default.Equals(m.Parameters[1].Type, stringCompType))); + + var endsWith = stringType.GetMembers("EndsWith").OfType() + .Where(m => SymbolEqualityComparer.Default.Equals(m.Parameters[0].Type, stringType)) + .Where(m => + (m.Parameters.Length == 1) || + (m.Parameters.Length == 2 && SymbolEqualityComparer.Default.Equals(m.Parameters[1].Type, stringCompType))); + + foreach (var m in startsWith) + { + reg.RegisterMethod(m, Handle); + } + + foreach (var m in endsWith) + { + reg.RegisterMethod(m, Handle); + } + + static void Handle(OperationAnalysisContext context, IInvocationOperation op) + { + var s = op.Arguments[0].Value.ConstantValue.Value as string; + + if (s != null && s.Length == 1) + { + if (op.Arguments.Length > 1 && op.Arguments[1].Value.ConstantValue.HasValue) + { + var comp = (StringComparison)op.Arguments[1].Value.ConstantValue.Value!; + if (comp != StringComparison.Ordinal) + { + return; + } + } + + var diagnostic = Diagnostic.Create(DiagDescriptors.StartsEndsWith, op.Syntax.GetLocation(), op.TargetMethod.Name); + context.ReportDiagnostic(diagnostic); + } + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/StaticTime.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/StaticTime.cs new file mode 100644 index 0000000000..156fdba4d2 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/StaticTime.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.ExtraAnalyzers.CallAnalysis; + +/// +/// Recommends using the System.TimeProvider abstraction. +/// +internal sealed class StaticTime +{ + private static readonly Dictionary _timeMethods = new() + { + ["System.Threading.Tasks.Task"] = new[] + { + "Delay", + }, + + ["System.Threading.Thread"] = new[] + { + "Sleep", + }, + }; + + private static readonly Dictionary _timeProperties = new() + { + ["System.DateTime"] = new[] + { + "Now", + "Today", + "UtcNow", + }, + + ["System.DateTimeOffset"] = new[] + { + "Now", + "UtcNow", + }, + }; + + public StaticTime(CallAnalyzer.Registrar reg) + { + reg.RegisterMethods(_timeMethods, HandleMethod); + reg.RegisterProperties(_timeProperties, HandleProperty); + + static void HandleMethod(OperationAnalysisContext context, IInvocationOperation op) + { + var diagnostic = Diagnostic.Create(DiagDescriptors.StaticTime, op.Syntax.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + + static void HandleProperty(OperationAnalysisContext context, IPropertyReferenceOperation op) + { + var diagnostic = Diagnostic.Create(DiagDescriptors.StaticTime, op.Syntax.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/StringFormat.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/StringFormat.cs new file mode 100644 index 0000000000..60f175a02a --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/StringFormat.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.ExtraAnalyzers.CallAnalysis; + +/// +/// Recommends using R9 composite text formatting functionality. +/// +internal sealed class StringFormat +{ + public StringFormat(CallAnalyzer.Registrar reg) + { + foreach (var method in reg.Compilation.GetSpecialType(SpecialType.System_String).GetMembers("Format").OfType()) + { + reg.RegisterMethod(method, Handle); + } + + var type = reg.Compilation.GetTypeByMetadataName("System.Text.StringBuilder"); + if (type != null) + { + foreach (var method in type.GetMembers("AppendFormat").OfType()) + { + reg.RegisterMethod(method, Handle); + } + } + + static void Handle(OperationAnalysisContext context, IInvocationOperation op) + { + var format = GetFormatArgument(op); + if (format.ChildNodes().First().IsKind(SyntaxKind.StringLiteralExpression)) + { + var properties = new Dictionary(); + if (op.TargetMethod.Name == "Format") + { + properties.Add("StringFormat", null); + } + + var diagnostic = Diagnostic.Create(DiagDescriptors.StringFormat, op.Syntax.GetLocation(), properties.ToImmutableDictionary()); + context.ReportDiagnostic(diagnostic); + } + + static SyntaxNode GetFormatArgument(IInvocationOperation invocation) + { + var sm = invocation.SemanticModel!; + var arguments = invocation.Arguments; + var typeInfo = sm.GetTypeInfo(arguments[0].Syntax.ChildNodes().First()); + + // This check is needed to identify exactly which argument of string.Format is the format argument + // The format might be passed as first or second argument + // if there are more than 1 arguments and first argument is IFormatProvider then format is second argument otherwise it is first + if (arguments.Length > 1 && typeInfo.Type != null && typeInfo.Type.AllInterfaces.Any(i => i.MetadataName == "IFormatProvider")) + { + return arguments[1].Syntax; + } + + return arguments[0].Syntax; + } + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/ValueTuple.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/ValueTuple.cs new file mode 100644 index 0000000000..ce7d5191c0 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CallAnalysis/ValueTuple.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.ExtraAnalyzers.CallAnalysis; + +/// +/// Recommends using value tuples, instead of reference tuples. +/// +internal sealed class ValueTuple +{ + private readonly string[] _tupleTypes = new[] + { + "System.Tuple`1", + "System.Tuple`2", + "System.Tuple`3", + "System.Tuple`4", + "System.Tuple`5", + "System.Tuple`6", + "System.Tuple`7", + "System.Tuple`8", + }; + + public ValueTuple(CallAnalyzer.Registrar reg) + { + var type = reg.Compilation.GetTypeByMetadataName("System.Tuple"); + if (type != null) + { + foreach (var method in type.GetMembers("Create").OfType()) + { + reg.RegisterMethod(method, HandleMethod); + } + + reg.RegisterConstructors(_tupleTypes, HandleConstructor); + } + + static void HandleMethod(OperationAnalysisContext context, IInvocationOperation op) + { + var diagnostic = Diagnostic.Create(DiagDescriptors.ValueTuple, op.Syntax.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + + static void HandleConstructor(OperationAnalysisContext context, IObjectCreationOperation op) + { + var diagnostic = Diagnostic.Create(DiagDescriptors.ValueTuple, op.Syntax.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CoalesceAnalyzer.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CoalesceAnalyzer.cs new file mode 100644 index 0000000000..58f3f22656 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/CoalesceAnalyzer.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.Extensions.ExtraAnalyzers.Utilities; + +namespace Microsoft.Extensions.ExtraAnalyzers; + +/// +/// C# analyzer that recommends removing superfluous uses of ?? and ??=. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class CoalesceAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagDescriptors.CoalesceAssignment, DiagDescriptors.Coalesce); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationStartContext => + { + // only report diagnostics on .NET 6, since previous runtimes don't have good enough nullability annotations + if (compilationStartContext.Compilation.IsNet6OrGreater()) + { + compilationStartContext.RegisterOperationAction(operationAnalysisContext => + { + var op = (ICoalesceAssignmentOperation)operationAnalysisContext.Operation; + + var type = op.Target.Type; + if (type != null + && type.NullableAnnotation == NullableAnnotation.NotAnnotated + && type.OriginalDefinition.SpecialType != SpecialType.System_Nullable_T) + { + if (op.Target.Kind == OperationKind.ParameterReference) + { + var pr = (IParameterReferenceOperation)op.Target; + var method = pr.Parameter.ContainingSymbol as IMethodSymbol; + + if (pr.Parameter.ContainingSymbol.IsExternallyVisible() + || (method != null && method.ImplementsPublicInterface())) + { + // this is a ??= applied to a parameter of a public method, let it slide... + return; + } + } + + var diagnostic = Diagnostic.Create(DiagDescriptors.CoalesceAssignment, op.Syntax.GetLocation()); + operationAnalysisContext.ReportDiagnostic(diagnostic); + } + }, OperationKind.CoalesceAssignment); + + compilationStartContext.RegisterOperationAction(operationAnalysisContext => + { + var op = (ICoalesceOperation)operationAnalysisContext.Operation; + + var type = op.Value.Type; + if (type != null + && type.NullableAnnotation == NullableAnnotation.NotAnnotated + && type.OriginalDefinition.SpecialType != SpecialType.System_Nullable_T) + { + if (op.Value.Kind == OperationKind.ParameterReference) + { + var pr = (IParameterReferenceOperation)op.Value; + var method = pr.Parameter.ContainingSymbol as IMethodSymbol; + + if (pr.Parameter.ContainingSymbol.IsExternallyVisible() + || (method != null && method.ImplementsPublicInterface())) + { + // this is a ?? applied to a parameter of a public method, let it slide... + return; + } + } + + var diagnostic = Diagnostic.Create(DiagDescriptors.Coalesce, op.Syntax.GetLocation()); + operationAnalysisContext.ReportDiagnostic(diagnostic); + } + }, OperationKind.Coalesce); + } + }); + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/ConditionalAccessAnalyzer.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/ConditionalAccessAnalyzer.cs new file mode 100644 index 0000000000..ffc33eebb7 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/ConditionalAccessAnalyzer.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.Extensions.ExtraAnalyzers.Utilities; + +namespace Microsoft.Extensions.ExtraAnalyzers; + +/// +/// C# analyzer that recommends removing superfluous uses of ?. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ConditionalAccessAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagDescriptors.ConditionalAccess); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationStartContext => + { + // only report diagnostics on .NET 6 or above since previous runtimes don't have good enough nullability annotations + if (compilationStartContext.Compilation.IsNet6OrGreater()) + { + var maybeNull = compilationStartContext.Compilation.GetTypeByMetadataName("System.Diagnostics.CodeAnalysis.MaybeNullAttribute"); + + compilationStartContext.RegisterOperationAction(operationAnalysisContext => + { + var op = (IConditionalAccessOperation)operationAnalysisContext.Operation; + + ITypeSymbol? type; + switch (op.Operation.Kind) + { + case OperationKind.PropertyReference: + { + var propRef = (IPropertyReferenceOperation)op.Operation; + if (MaybeNull(propRef.Property.GetAttributes())) + { + // property can be null, independent of its type signature + return; + } + + type = propRef.Property.Type; + break; + } + + case OperationKind.FieldReference: + { + var fieldRef = (IFieldReferenceOperation)op.Operation; + if (MaybeNull(fieldRef.Field.GetAttributes())) + { + return; + } + + type = fieldRef.Field.Type; + break; + } + + case OperationKind.Invocation: + { + var invocation = (IInvocationOperation)op.Operation; + if (MaybeNull(invocation.TargetMethod.GetReturnTypeAttributes())) + { + return; + } + + type = op.Operation.Type; + break; + } + + default: + { + type = op.Operation.Type; + break; + } + } + + if (type != null) + { + if (type is ITypeParameterSymbol tp) + { + if (!tp.HasNotNullConstraint) + { + // a generic type without a notnull constraint can potentially hold null values + return; + } + } + + // if the type of the operand is not nullable, then we have a candidate + if (type.NullableAnnotation == NullableAnnotation.NotAnnotated) + { + // if the operand is a parameter on a public method or interface method, then don't report it + if (op.Operation.Kind == OperationKind.ParameterReference) + { + var pr = (IParameterReferenceOperation)op.Operation; + var method = pr.Parameter.ContainingSymbol as IMethodSymbol; + + if (pr.Parameter.ContainingSymbol.IsExternallyVisible() + || (method != null && method.ImplementsPublicInterface())) + { + // this is a ? applied to a parameter of a public method, let it slide... + return; + } + } + + var diagnostic = Diagnostic.Create(DiagDescriptors.ConditionalAccess, op.Syntax.GetLocation()); + operationAnalysisContext.ReportDiagnostic(diagnostic); + } + } + }, OperationKind.ConditionalAccess); + + bool MaybeNull(ImmutableArray attrs) + { + foreach (var attr in attrs) + { + if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, maybeNull)) + { + return true; + } + } + + return false; + } + } + }); + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/DataClassificationStaticAnalysisCommon.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/DataClassificationStaticAnalysisCommon.cs new file mode 100644 index 0000000000..4b908c0d90 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/DataClassificationStaticAnalysisCommon.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.ExtraAnalyzers; + +internal static class DataClassificationStaticAnalysisCommon +{ + internal const string DataClassifierNS = "Microsoft.Extensions.Data.Classification"; + internal const string BaseClassifierAttributeName = "DataClassificationAttribute"; + internal const string DataClassificationInterfaceName = "IClassifiedData"; + + internal static ITypeSymbol? GetFieldOrPropertyType(this ISymbol symbol) + { + if (symbol is IFieldSymbol fieldSymbol) + { + return fieldSymbol.Type; + } + else if (symbol is IPropertySymbol propertySymbol) + { + return propertySymbol.Type; + } + else + { + return null; + } + } + + internal static string GetDataClassifierName(this ISymbol symbol) + { + if (symbol != null) + { + var classifier = symbol + .GetAttributes() + .Where(a => a.IsDataClassifier()) + .SingleOrDefault(); + + if (classifier != default) + { + var classifierFullName = classifier!.AttributeClass!.Name; + return classifierFullName.Substring(0, classifierFullName.Length - "Attribute".Length); + } + + var symbolType = symbol.GetFieldOrPropertyType(); + + if (symbolType != null) + { + if (symbolType.IsClassifiedData()) + { + return symbolType.Name; + } + } + } + + return string.Empty; + } + + internal static bool IsAnnotatedWithDataClassifier(this ISymbol symbol) + => symbol.GetAttributes().Any(a => a.IsDataClassifier()); + + internal static bool IsClassifiedData(this ITypeSymbol type) + => type.Interfaces.Any(i => string.Equals(i.Name, DataClassificationInterfaceName, StringComparison.Ordinal)); + + internal static bool HasDataClassificationTypes(this Compilation comp) + => CompilationUsesType(comp, $"{DataClassifierNS}.{DataClassificationInterfaceName}"); + + internal static bool HasDataClassifiers(this Compilation comp) + => CompilationUsesType(comp, $"{DataClassifierNS}.{BaseClassifierAttributeName}"); + + internal static bool IsDataClassifier(this AttributeData attribute) + { + if (attribute != null && attribute.AttributeClass != null) + { + var attrNS = attribute.AttributeClass.ContainingNamespace.ToString(); + var attrBaseName = attribute.AttributeClass.BaseType!.Name; + + return string.Equals(attrNS, DataClassifierNS, StringComparison.Ordinal) + && string.Equals(attrBaseName, BaseClassifierAttributeName, StringComparison.Ordinal); + } + + return false; + } + + internal static ImmutableArray GetDataClassifierNamesAsImmutableArray(this ISymbol symbol) + { + return symbol.GetAttributes() + .Where(a => a.IsDataClassifier()) + .Select(a => a.AttributeClass!.ToDisplayString()) + .Select(s => s.Substring(0, s.Length - "Attribute".Length)) + .ToImmutableArray(); + } + + private static bool CompilationUsesType(Compilation comp, string fullyQualifiedTypeName) + => comp!.GetTypeByMetadataName(fullyQualifiedTypeName) != null; +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/DiagDescriptors.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/DiagDescriptors.cs new file mode 100644 index 0000000000..b4b0726be9 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/DiagDescriptors.cs @@ -0,0 +1,368 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +[assembly: System.Resources.NeutralResourcesLanguage("en-us")] + +namespace Microsoft.Extensions.ExtraAnalyzers; + +internal static class DiagDescriptors +{ + /// + /// Category for analyzers that will improve performance of the application. + /// + private const string Performance = nameof(Performance); + + /// + /// Category for analyzers that will make code more readable. + /// + private const string Readability = nameof(Readability); + + /// + /// Category for analyzers that will improve reliability of the application. + /// + private const string Reliability = nameof(Reliability); + + /// + /// Category for analyzers that will improve resiliency of the application. + /// + private const string Resilience = nameof(Resilience); + + /// + /// Category for analyzers that will make code more correct. + /// + private const string Correctness = nameof(Correctness); + + /// + /// Category for analyzers that will improve the privacy posture of code. + /// + private const string Privacy = nameof(Privacy); + + public static DiagnosticDescriptor LegacyLogging { get; } = new( + id: "R9A000", + messageFormat: Resources.LegacyLoggingMessage, + title: Resources.LegacyLoggingTitle, + category: Performance, + description: Resources.LegacyLoggingDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a000", + isEnabledByDefault: true); + + // R9A001..R9A013 are retired + + public static DiagnosticDescriptor ThrowsExpression { get; } = new( + id: "R9A014", + messageFormat: Resources.ThrowsExpressionMessage, + title: Resources.ThrowsExpressionTitle, + category: Performance, + description: Resources.ThrowsExpressionDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a014", + isEnabledByDefault: true); + + public static DiagnosticDescriptor ThrowsStatement { get; } = new( + id: "R9A015", + messageFormat: Resources.ThrowsStatementMessage, + title: Resources.ThrowsStatementTitle, + category: Performance, + description: Resources.ThrowsStatementDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a015", + isEnabledByDefault: true); + + // R9A016..R9A017 has been retired + + public static DiagnosticDescriptor StringFormat { get; } = new( + id: "R9A018", + messageFormat: Resources.StringFormatMessage, + title: Resources.StringFormatTitle, + category: Performance, + description: Resources.StringFormatDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a018", + isEnabledByDefault: true); + + public static DiagnosticDescriptor UsingExcessiveDictionaryLookup { get; } = new( + id: "R9A019", + messageFormat: Resources.UsingExcessiveDictionaryLookupMessage, + title: Resources.UsingExcessiveDictionaryLookupTitle, + category: Performance, + description: Resources.UsingExcessiveDictionaryLookupDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a019", + isEnabledByDefault: true); + + public static DiagnosticDescriptor UsingExcessiveSetLookup { get; } = new( + id: "R9A020", + messageFormat: Resources.UsingExcessiveSetLookupMessage, + title: Resources.UsingExcessiveSetLookupTitle, + category: Performance, + description: Resources.UsingExcessiveSetLookupDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a020", + isEnabledByDefault: true); + + public static DiagnosticDescriptor UsingToStringInLoggers { get; } = new( + id: "R9A021", + messageFormat: Resources.UsingToStringInLoggersMessage, + title: Resources.UsingToStringInLoggersTitle, + category: "Performance", + description: Resources.UsingToStringInLoggersDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a021", + isEnabledByDefault: true); + + public static DiagnosticDescriptor StaticTime { get; } = new( + id: "R9A022", + messageFormat: Resources.StaticTimeMessage, + title: Resources.StaticTimeTitle, + category: Reliability, + description: Resources.StaticTimeDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a022", + isEnabledByDefault: true); + + // R9A023..R9A028 retired + + public static DiagnosticDescriptor UsingExperimentalApi { get; } = new( + id: "R9A029", + messageFormat: Resources.UsingExperimentalApiMessage, + title: Resources.UsingExperimentalApiTitle, + category: Reliability, + description: Resources.UsingExperimentalApiDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a029", + isEnabledByDefault: true); + + public static DiagnosticDescriptor StartsEndsWith { get; } = new( + id: "R9A030", + messageFormat: Resources.StartsEndsWithMessage, + title: Resources.StartsEndsWithTitle, + category: Performance, + description: Resources.StartsEndsWithDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a030", + isEnabledByDefault: true); + + public static DiagnosticDescriptor MakeExeTypesInternal { get; } = new( + id: "R9A031", + messageFormat: Resources.MakeExeTypesInternalMessage, + title: Resources.MakeExeTypesInternalTitle, + category: Performance, + description: Resources.MakeExeTypesInternalDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a031", + isEnabledByDefault: true); + + public static DiagnosticDescriptor Arrays { get; } = new( + id: "R9A032", + messageFormat: Resources.ArraysMessage, + title: Resources.ArraysTitle, + category: Performance, + description: Resources.ArraysDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a032", + isEnabledByDefault: true); + + public static DiagnosticDescriptor EnumStrings { get; } = new( + id: "R9A033", + messageFormat: Resources.EnumStringsMessage, + title: Resources.EnumStringsTitle, + category: Performance, + description: Resources.EnumStringsDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a033", + isEnabledByDefault: true); + + // R9A034 deprecated + // R9A035 deprecated + + public static DiagnosticDescriptor ToInvariantString { get; } = new( + id: "R9A036", + messageFormat: Resources.ToInvariantStringMessage, + title: Resources.ToInvariantStringTitle, + category: Performance, + description: Resources.ToInvariantStringDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a036", + isEnabledByDefault: true); + + public static DiagnosticDescriptor ValueTuple { get; } = new( + id: "R9A037", + messageFormat: Resources.ValueTupleMessage, + title: Resources.ValueTupleTitle, + category: Performance, + description: Resources.ValueTupleDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a037", + isEnabledByDefault: true); + + // R9A038 retired + + public static DiagnosticDescriptor NullCheck { get; } = new( + id: "R9A039", + messageFormat: Resources.NullCheckMessage, + title: Resources.NullCheckTitle, + category: Performance, + description: Resources.NullCheckDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a039", + isEnabledByDefault: true); + + public static DiagnosticDescriptor LegacyCollection { get; } = new( + id: "R9A040", + messageFormat: Resources.LegacyCollectionMessage, + title: Resources.LegacyCollectionTitle, + category: Performance, + description: Resources.LegacyCollectionDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a040", + isEnabledByDefault: true); + + // R9A041..R9A042 retired + + public static DiagnosticDescriptor Split { get; } = new( + id: "R9A043", + messageFormat: Resources.SplitMessage, + title: Resources.SplitTitle, + category: Performance, + description: Resources.SplitDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a043", + isEnabledByDefault: true); + + public static DiagnosticDescriptor MakeArrayStatic { get; } = new( + id: "R9A044", + messageFormat: Resources.MakeArrayStaticMessage, + title: Resources.MakeArrayStaticTitle, + category: Performance, + description: Resources.MakeArrayStaticDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a044", + isEnabledByDefault: true); + + // R9A045..R9A048 retired + + public static DiagnosticDescriptor AnExperimentalApiIsNotAnnotated { get; } = new( + id: "R9A049", + messageFormat: Resources.AnExperimentalApiIsNotAnnotatedMessage, + title: Resources.AnExperimentalApiIsNotAnnotatedTitle, + category: Correctness, + description: Resources.AnExperimentalApiIsNotAnnotatedDescription, + defaultSeverity: DiagnosticSeverity.Hidden, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a049", + isEnabledByDefault: true); + + public static DiagnosticDescriptor AnExperimentalApiWasMarkedAsObsolete { get; } = new( + id: "R9A050", + messageFormat: Resources.AnExperimentalApiWasMarkedAsObsoleteMessage, + title: Resources.AnExperimentalApiWasMarkedAsObsoleteTitle, + category: Correctness, + description: Resources.AnExperimentalApiWasMarkedAsObsoleteDescription, + defaultSeverity: DiagnosticSeverity.Hidden, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a050", + isEnabledByDefault: true); + + public static DiagnosticDescriptor AStableApiWasMarkedAsExperimental { get; } = new( + id: "R9A051", + messageFormat: Resources.AStableApiWasMarkedAsExperimentalMessage, + title: Resources.AStableApiWasMarkedAsExperimentalTitle, + category: Correctness, + description: Resources.AStableApiWasMarkedAsExperimentalDescription, + defaultSeverity: DiagnosticSeverity.Hidden, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a051", + isEnabledByDefault: true); + + public static DiagnosticDescriptor AStableApiWasDeletedOutsideTheDeprecationPeriod { get; } = new( + id: "R9A052", + messageFormat: Resources.AStableApiWasDeletedOutsideTheDeprecationPeriodMessage, + title: Resources.AStableApiWasDeletedOutsideTheDeprecationPeriodTitle, + category: Correctness, + description: Resources.AStableApiWasDeletedOutsideTheDeprecationPeriodDescription, + defaultSeverity: DiagnosticSeverity.Hidden, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a052", + isEnabledByDefault: true); + + public static DiagnosticDescriptor ADeprecatedApiIsNotAnnotatedWithObsoleteAttribute { get; } = new( + id: "R9A053", + messageFormat: Resources.ADeprecatedApiIsNotAnnotatedWithObsoleteAttributeMessage, + title: Resources.ADeprecatedApiIsNotAnnotatedWithObsoleteAttributeTitle, + category: Correctness, + description: Resources.ADeprecatedApiIsNotAnnotatedWithObsoleteAttributeDescription, + defaultSeverity: DiagnosticSeverity.Hidden, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a053", + isEnabledByDefault: true); + + public static DiagnosticDescriptor ADeprecatedApiIsMarkedAsExperimental { get; } = new( + id: "R9A054", + messageFormat: Resources.ADeprecatedApiIsMarkedAsExperimentalMessage, + title: Resources.ADeprecatedApiIsMarkedAsExperimentalTitle, + category: Correctness, + description: Resources.ADeprecatedApiIsMarkedAsExperimentalDescription, + defaultSeverity: DiagnosticSeverity.Hidden, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a054", + isEnabledByDefault: true); + + public static DiagnosticDescriptor TheSignatureOfAStableApiHasChanged { get; } = new( + id: "R9A055", + messageFormat: Resources.TheSignatureOfAStableApiHasChangedMessage, + title: Resources.TheSignatureOfAStableApiHasChangedTitle, + category: Correctness, + description: Resources.TheSignatureOfAStableApiHasChangedDescription, + defaultSeverity: DiagnosticSeverity.Hidden, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a055", + isEnabledByDefault: true); + + public static DiagnosticDescriptor AsyncCallInsideUsingBlock { get; } = new( + id: "R9A056", + messageFormat: Resources.AsyncCallInsideUsingBlockMessage, + title: Resources.AsyncCallInsideUsingBlockTitle, + category: Correctness, + description: Resources.AsyncCallInsideUsingBlockDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a056", + isEnabledByDefault: true); + + // R9A057 retired + + public static DiagnosticDescriptor ConditionalAccess { get; } = new( + id: "R9A058", + messageFormat: Resources.ConditionalAccessMessage, + title: Resources.ConditionalAccessTitle, + category: Performance, + description: Resources.ConditionalAccessDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a058", + isEnabledByDefault: true); + + public static DiagnosticDescriptor CoalesceAssignment { get; } = new( + id: "R9A059", + messageFormat: Resources.CoalesceAssignmentMessage, + title: Resources.CoalesceAssignmentTitle, + category: Performance, + description: Resources.CoalesceAssignmentDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a059", + isEnabledByDefault: true); + + public static DiagnosticDescriptor Coalesce { get; } = new( + id: "R9A060", + messageFormat: Resources.CoalesceMessage, + title: Resources.CoalesceTitle, + category: Performance, + description: Resources.CoalesceDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a060", + isEnabledByDefault: true); + + public static DiagnosticDescriptor AsyncMethodWithoutCancellation { get; } = new( + id: "R9A061", + messageFormat: Resources.AsyncMethodWithoutCancellationMessage, + title: Resources.AsyncMethodWithoutCancellationTitle, + category: Resilience, + description: Resources.AsyncMethodWithoutCancellationDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a061", + isEnabledByDefault: true); +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/FixAllProviders/ISequentialFixer.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/FixAllProviders/ISequentialFixer.cs new file mode 100644 index 0000000000..f458a34227 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/FixAllProviders/ISequentialFixer.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.ExtraAnalyzers.FixAllProviders; + +public interface ISequentialFixer +{ + public SyntaxNode GetFixableSyntaxNodeFromDiagnostic(SyntaxNode documentRoot, Diagnostic diagnostic); + public SyntaxNode ApplyDiagnosticFixToSyntaxNode(SyntaxNode nodeToFix, Diagnostic diagnostic); +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/FixAllProviders/SequentialFixAllCodeAction.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/FixAllProviders/SequentialFixAllCodeAction.cs new file mode 100644 index 0000000000..b51863ed6d --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/FixAllProviders/SequentialFixAllCodeAction.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; + +namespace Microsoft.Extensions.ExtraAnalyzers.FixAllProviders; + +public sealed class SequentialFixAllCodeAction : CodeAction +{ + public override string Title { get; } + + public SequentialFixAllCodeAction( + string fixAllTitle, + FixAllContext context, + ConcurrentDictionary> diagsToFixGroupedByDocId, + ImmutableArray inScopeDocumentIds) + { + Title = fixAllTitle; + _context = context; + _sequentialFixer = (context.CodeFixProvider as ISequentialFixer)!; + _diagsToFixGroupedByDocId = diagsToFixGroupedByDocId; + _solution = context.Solution; + _inScopeDocumentIds = inScopeDocumentIds; + } + + protected override async Task GetChangedSolutionAsync(CancellationToken cancellationToken) + { + do + { + // Apply CodeFix for all in scope documents and update solution with changed documents + var fixDiagTasks = _diagsToFixGroupedByDocId.Select(diagsInDoc => + { + var docId = diagsInDoc.Key; + return ApplyDiagnosticFixesAndGetDocumentRootAsync(docId) + .ContinueWith(completeFixDiagTask => + { + lock (_solution) + { + _solution = _solution.WithDocumentSyntaxRoot(docId, completeFixDiagTask.Result); + } + }, TaskScheduler.Default); + }); + await Task.WhenAll(fixDiagTasks).ConfigureAwait(continueOnCapturedContext: false); + + // Clear the internal map of document grouped diagnostics + _diagsToFixGroupedByDocId.Clear(); + + // Recompute diagnostics from all in scope documents + var recomputeDiagTasks = _inScopeDocumentIds.Select(docId => + { + var document = _solution.GetDocument(docId); + return _context + .GetDocumentDiagnosticsAsync(document!) + .ContinueWith(completeRecomputeTask => + { + var newDiagnostics = completeRecomputeTask.Result; + if (newDiagnostics.Any()) + { + _ = _diagsToFixGroupedByDocId.TryAdd(docId, newDiagnostics); + } + }, TaskScheduler.Default); + }); + await Task.WhenAll(recomputeDiagTasks).ConfigureAwait(continueOnCapturedContext: false); + } + while (!_diagsToFixGroupedByDocId.IsEmpty); + + return _solution; + } + + private readonly FixAllContext _context; + private readonly ISequentialFixer _sequentialFixer; + private readonly ConcurrentDictionary> _diagsToFixGroupedByDocId; + private Solution _solution; + private ImmutableArray _inScopeDocumentIds; + + private async Task ApplyDiagnosticFixesAndGetDocumentRootAsync(DocumentId docIdToFix) + { + var document = _solution.GetDocument(docIdToFix); + var docRoot = await document!.GetSyntaxRootAsync().ConfigureAwait(continueOnCapturedContext: false); + var diagnostics = _diagsToFixGroupedByDocId[docIdToFix]; + var nodeToDiagsMap = new Dictionary(); + + foreach (var d in diagnostics) + { + nodeToDiagsMap.Add(_sequentialFixer.GetFixableSyntaxNodeFromDiagnostic(docRoot!, d), d); + } + + return docRoot!.ReplaceNodes(nodeToDiagsMap.Keys.ToArray(), + (oldNode, _) => _sequentialFixer.ApplyDiagnosticFixToSyntaxNode(oldNode, nodeToDiagsMap[oldNode])); + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/FixAllProviders/SequentialFixAllProvider.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/FixAllProviders/SequentialFixAllProvider.cs new file mode 100644 index 0000000000..b6eaf013da --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/FixAllProviders/SequentialFixAllProvider.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; + +namespace Microsoft.Extensions.ExtraAnalyzers.FixAllProviders; + +public sealed class SequentialFixAllProvider : FixAllProvider +{ + public const string DocumentScopeStr = "document"; + public const string ProjectScopeStr = "project"; + + public static SequentialFixAllProvider GetInstance(ISequentialFixer _) => _instance; + + public override async Task GetFixAsync(FixAllContext fixAllContext) + { + (var fixAllTitle, var documentsToFix) = GetFixAllTitleAndDocumentsToFix(fixAllContext); + ConcurrentDictionary> diagsToFixGroupedByDocId = new(); + + var computeDiagTasks = documentsToFix.Select(docId => + fixAllContext.GetDocumentDiagnosticsAsync(fixAllContext.Solution.GetDocument(docId)!) + .ContinueWith((completeComputeDiagsTask) => + { + var diagsToFixInDocument = completeComputeDiagsTask.Result; + if (diagsToFixInDocument.Any()) + { + _ = diagsToFixGroupedByDocId.TryAdd(docId, diagsToFixInDocument); + } + }, TaskScheduler.Default)); + + await Task.WhenAll(computeDiagTasks).ConfigureAwait(continueOnCapturedContext: false); + + return new SequentialFixAllCodeAction( + fixAllTitle, + fixAllContext, + diagsToFixGroupedByDocId, + documentsToFix); + } + + private static readonly SequentialFixAllProvider _instance = new(); + + internal static ImmutableArray GetAllDocumentsInSolution(Solution solution) + { + var allDocsInSolution = ImmutableArray.Empty; + + foreach (var project in solution.Projects) + { + allDocsInSolution = allDocsInSolution.AddRange(project!.Documents.Select(d => d.Id)); + } + + return allDocsInSolution; + } + + internal static (string fixAllTitle, ImmutableArray documentsToFix) GetFixAllTitleAndDocumentsToFix(FixAllContext context) + { + var fixAllTitle = string.Empty; + var documentsToFix = ImmutableArray.Empty; + + switch (context.Scope) + { + case FixAllScope.Document: + fixAllTitle = string.Format( + CultureInfo.InvariantCulture, + Resources.SequentialFixAllFormat, + DocumentScopeStr, + context.Document!.Name); + documentsToFix = ImmutableArray.Create(context.Document!.Id); + break; + case FixAllScope.Project: + fixAllTitle = string.Format( + CultureInfo.InvariantCulture, + Resources.SequentialFixAllFormat, + ProjectScopeStr, + context.Project!.Name); + documentsToFix = context.Project.Documents.Select(d => d.Id).ToImmutableArray(); + break; + case FixAllScope.Solution: + fixAllTitle = Resources.SequentialFixAllInSolution; + documentsToFix = GetAllDocumentsInSolution(context.Solution); + break; + } + + return (fixAllTitle, documentsToFix); + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/MakeExeTypesInternalAnalyzer.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/MakeExeTypesInternalAnalyzer.cs new file mode 100644 index 0000000000..ef3e805bb3 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/MakeExeTypesInternalAnalyzer.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Extensions.ExtraAnalyzers.Utilities; + +namespace Microsoft.Extensions.ExtraAnalyzers; + +/// +/// C# analyzer that recommends making an executable's types internal. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class MakeExeTypesInternalAnalyzer : DiagnosticAnalyzer +{ + // if any member of the discovered public types are annotated with these attributes, then the type is known + // to need to be public, so we don't report the type + private static readonly string[] _disqualifyingMemberAttributes = new[] + { + "Xunit.FactAttribute", + "Xunit.TheoryAttribute", + "BenchmarkDotNet.Attributes.BenchmarkAttribute", + "Microsoft.AspNetCore.Mvc.HttpGetAttribute", + "System.Text.Json.Serialization.JsonConstructorAttribute", + "System.Text.Json.Serialization.JsonExtensionDataAttribute", + "System.Text.Json.Serialization.JsonIgnoreAttribute", + "System.Text.Json.Serialization.JsonIncludeAttribute", + "System.Text.Json.Serialization.JsonNumberHandlingAttribute", + "System.Text.Json.Serialization.JsonPropertyNameAttribute", + "System.Text.Json.Serialization.JsonPropertyOrderAttribute", + }; + + private static readonly string[] _disqualifyingTypeAttributes = new[] + { + "MessagePack.MessagePackObjectAttribute", + "Microsoft.AspNetCore.Mvc.ApiControllerAttribute", + }; + + // if any of the discovered public types derive from the given base classes, we know the use requires the types to + // be public, so we don't report the type + private static readonly string[] _disqualifyingBaseClasses = new[] + { + "Microsoft.AspNetCore.Mvc.ControllerBase", + "System.Web.Http.ApiController", + "System.Web.Mvc.Controller", + }; + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagDescriptors.MakeExeTypesInternal); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationStartContext => + { + var disqualifyingMemberAttributes = new HashSet(SymbolEqualityComparer.Default); + foreach (var name in _disqualifyingMemberAttributes) + { + var type = compilationStartContext.Compilation.GetTypeByMetadataName(name); + if (type != null) + { + _ = disqualifyingMemberAttributes.Add(type); + } + } + + var disqualifyingTypeAttributes = new HashSet(SymbolEqualityComparer.Default); + foreach (var name in _disqualifyingTypeAttributes) + { + var type = compilationStartContext.Compilation.GetTypeByMetadataName(name); + if (type != null) + { + _ = disqualifyingTypeAttributes.Add(type); + } + } + + var disqualifyingBaseClasses = new List(); + foreach (var name in _disqualifyingBaseClasses) + { + var type = compilationStartContext.Compilation.GetTypeByMetadataName(name); + if (type != null) + { + disqualifyingBaseClasses.Add(type); + } + } + + if (compilationStartContext.Compilation.Options.OutputKind == OutputKind.ConsoleApplication) + { + compilationStartContext.RegisterSymbolAction(symbolActionContext => + { + var type = (ITypeSymbol)symbolActionContext.Symbol; + if (type.DeclaredAccessibility == Accessibility.Public && type.ContainingType == null) + { + // see if the type is annotated with one of the disqualifying attributes + foreach (var attr in type.GetAttributes()) + { + if (attr.AttributeClass != null) + { + if (disqualifyingTypeAttributes.Contains(attr.AttributeClass)) + { + return; + } + } + } + + // see if the type derives from one of the disqualifying base types + foreach (var c in disqualifyingBaseClasses) + { + if (c.IsAncestorOf(type)) + { + return; + } + } + + // see if any members are annotated with disqualifying attributes + var members = type.GetMembers(); + foreach (var member in members) + { + var attrs = member.GetAttributes(); + foreach (var attr in attrs) + { + if (attr.AttributeClass != null) + { + if (disqualifyingMemberAttributes.Contains(attr.AttributeClass)) + { + return; + } + } + } + } + + var diagnostic = Diagnostic.Create(DiagDescriptors.MakeExeTypesInternal, type.Locations[0], type.Name); + symbolActionContext.ReportDiagnostic(diagnostic); + } + }, SymbolKind.NamedType); + } + }); + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/MakeExeTypesInternalFixer.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/MakeExeTypesInternalFixer.cs new file mode 100644 index 0000000000..18a8d37c9f --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/MakeExeTypesInternalFixer.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Editing; + +namespace Microsoft.Extensions.ExtraAnalyzers; + +/// +/// Replace explicit throw with static method. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MakeExeTypesInternalFixer))] +[Shared] +public sealed class MakeExeTypesInternalFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DiagDescriptors.MakeExeTypesInternal.Id); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var node = root?.FindNode(context.Span); + if (node != null) + { + var action = CodeAction.Create(Resources.MakeTypeInternal, c => MakeInternalAsync(context.Document, node, context.CancellationToken), nameof(MakeExeTypesInternalFixer)); + context.RegisterCodeFix(action, context.Diagnostics); + } + } + + private static async Task MakeInternalAsync(Document document, SyntaxNode decl, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + editor.SetAccessibility(decl, Accessibility.Internal); + return editor.GetChangedDocument(); + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/OptimizeArraysAnalyzer.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/OptimizeArraysAnalyzer.cs new file mode 100644 index 0000000000..a0338d00df --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/OptimizeArraysAnalyzer.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.ExtraAnalyzers; + +/// +/// C# analyzer that recommends using Array.Empty, or making arrays of literals into static readonly fields. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class OptimizeArraysAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagDescriptors.MakeArrayStatic); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterOperationAction(operationAnalysisContext => + { + var arrayCreation = (IArrayCreationOperation)operationAnalysisContext.Operation; + + if (arrayCreation.Syntax.AncestorsAndSelf().Any(x => x.IsKind(SyntaxKind.Attribute))) + { + return; + } + + var initializer = arrayCreation.Initializer; + var target = arrayCreation.Parent; + var type = ((IArrayTypeSymbol?)arrayCreation.Type)?.ElementType; + + var empty = initializer?.ElementValues.Length == 0; +#pragma warning disable S1067 // Expressions should not be too complex + if (initializer == null + && arrayCreation.DimensionSizes.Length == 1 + && arrayCreation.DimensionSizes[0] is ILiteralOperation lit + && lit.ConstantValue.HasValue + && lit.ConstantValue.Value is 0) + { + empty = true; + } +#pragma warning restore S1067 // Expressions should not be too complex + + if (empty) + { + // empty arrays, handled by CA1825 + return; + } + + if (initializer == null) + { + return; + } + + foreach (var value in initializer.ElementValues) + { + if (value.Kind == OperationKind.Literal) + { + continue; + } + + if (value.Kind == OperationKind.FieldReference) + { + var fieldRef = (IFieldReferenceOperation)value; + if (fieldRef.ConstantValue.HasValue) + { + continue; + } + } + + return; + } + + // found a candidate array initialization... + + if (target != null) + { + if (InitializesStaticFieldOrProp(target)) + { + return; + } + } + + var diagnostic = Diagnostic.Create(DiagDescriptors.MakeArrayStatic, arrayCreation.Syntax.GetLocation()); + operationAnalysisContext.ReportDiagnostic(diagnostic); + }, OperationKind.ArrayCreation); + } + + private static bool InitializesStaticFieldOrProp(IOperation op) + { + // if this array allocation is done to initialize a static field or property, then don't report it + switch (op.Kind) + { + case OperationKind.FieldInitializer: + { + var fieldRef = (IFieldInitializerOperation)op; + foreach (var field in fieldRef.InitializedFields) + { + if (field.IsStatic) + { + return true; + } + } + + break; + } + + case OperationKind.PropertyInitializer: + { + var propRef = (IPropertyInitializerOperation)op; + foreach (var prop in propRef.InitializedProperties) + { + if (prop.IsStatic) + { + return true; + } + } + + break; + } + + case OperationKind.Conversion: + case OperationKind.Argument: + case OperationKind.Invocation: + { + if (op.Parent != null) + { + return InitializesStaticFieldOrProp(op.Parent); + } + + break; + } + } + + return false; + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Resources.Designer.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Resources.Designer.cs new file mode 100644 index 0000000000..6a7f76f70e --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Resources.Designer.cs @@ -0,0 +1,963 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Extensions.ExtraAnalyzers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Extesions.ExtraAnalyzers.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Deprecated API cannot be marked as experimental. + /// + internal static string ADeprecatedApiIsMarkedAsExperimentalDescription { + get { + return ResourceManager.GetString("ADeprecatedApiIsMarkedAsExperimentalDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove experimental attribute from deprecated API. + /// + internal static string ADeprecatedApiIsMarkedAsExperimentalMessage { + get { + return ResourceManager.GetString("ADeprecatedApiIsMarkedAsExperimentalMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A deprecated API is marked as experimental. + /// + internal static string ADeprecatedApiIsMarkedAsExperimentalTitle { + get { + return ResourceManager.GetString("ADeprecatedApiIsMarkedAsExperimentalTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Deprecated API should have annotation that will guide customers regarding its replacement and the release in which it will be removed. + /// + internal static string ADeprecatedApiIsNotAnnotatedWithObsoleteAttributeDescription { + get { + return ResourceManager.GetString("ADeprecatedApiIsNotAnnotatedWithObsoleteAttributeDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Annotate deprecated API with obsolete attribute. + /// + internal static string ADeprecatedApiIsNotAnnotatedWithObsoleteAttributeMessage { + get { + return ResourceManager.GetString("ADeprecatedApiIsNotAnnotatedWithObsoleteAttributeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A deprecated API is not annotated with the obsolete attribute. + /// + internal static string ADeprecatedApiIsNotAnnotatedWithObsoleteAttributeTitle { + get { + return ResourceManager.GetString("ADeprecatedApiIsNotAnnotatedWithObsoleteAttributeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Newly added externally visible API is not marked as experimental. + /// + internal static string AnExperimentalApiIsNotAnnotatedDescription { + get { + return ResourceManager.GetString("AnExperimentalApiIsNotAnnotatedDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Newly added externally visible API must be annotated as experimental. + /// + internal static string AnExperimentalApiIsNotAnnotatedMessage { + get { + return ResourceManager.GetString("AnExperimentalApiIsNotAnnotatedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Newly added API must be annotated with experimental attribute. + /// + internal static string AnExperimentalApiIsNotAnnotatedTitle { + get { + return ResourceManager.GetString("AnExperimentalApiIsNotAnnotatedTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You can change experimental APIs at any time without deprecation period. + /// + internal static string AnExperimentalApiWasMarkedAsObsoleteDescription { + get { + return ResourceManager.GetString("AnExperimentalApiWasMarkedAsObsoleteDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove obsolete annotation from experimental API. + /// + internal static string AnExperimentalApiWasMarkedAsObsoleteMessage { + get { + return ResourceManager.GetString("AnExperimentalApiWasMarkedAsObsoleteMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An experimental API was marked as obsolete. + /// + internal static string AnExperimentalApiWasMarkedAsObsoleteTitle { + get { + return ResourceManager.GetString("AnExperimentalApiWasMarkedAsObsoleteTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Annotate experimental API. + /// + internal static string AnnotateExperimentalApi { + get { + return ResourceManager.GetString("AnnotateExperimentalApi", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Dictionaries and sets which use enums and bytes as keys can often be replaced with simple arrays for improved performance. + /// + internal static string ArraysDescription { + get { + return ResourceManager.GetString("ArraysDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Consider using '{0}?[]' instead of '{1}'. + /// + internal static string ArraysMessage { + get { + return ResourceManager.GetString("ArraysMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Consider using an array instead of a collection. + /// + internal static string ArraysTitle { + get { + return ResourceManager.GetString("ArraysTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stable APIs must follow deprecation policy and be marked as obsolete before it gets deleted.. + /// + internal static string AStableApiWasDeletedOutsideTheDeprecationPeriodDescription { + get { + return ResourceManager.GetString("AStableApiWasDeletedOutsideTheDeprecationPeriodDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Revert deletion of {0} API and mark it with obsolete attribute. + /// + internal static string AStableApiWasDeletedOutsideTheDeprecationPeriodMessage { + get { + return ResourceManager.GetString("AStableApiWasDeletedOutsideTheDeprecationPeriodMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A stable API was deleted outside the deprecation period. + /// + internal static string AStableApiWasDeletedOutsideTheDeprecationPeriodTitle { + get { + return ResourceManager.GetString("AStableApiWasDeletedOutsideTheDeprecationPeriodTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stable APIs should not be annotated as experimental. + /// + internal static string AStableApiWasMarkedAsExperimentalDescription { + get { + return ResourceManager.GetString("AStableApiWasMarkedAsExperimentalDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Don't mark stable API as experimental. + /// + internal static string AStableApiWasMarkedAsExperimentalMessage { + get { + return ResourceManager.GetString("AStableApiWasMarkedAsExperimentalMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A stable API was marked as experimental. + /// + internal static string AStableApiWasMarkedAsExperimentalTitle { + get { + return ResourceManager.GetString("AStableApiWasMarkedAsExperimentalTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to When skipping the await keyword for asynchronous operations inside a using block, then a disposable object could be disposed before the asynchronous invocation finishes. This might result in incorrect behavior and very often ends with a runtime exception notifying that the code is trying to operate on a disposed object.. + /// + internal static string AsyncCallInsideUsingBlockDescription { + get { + return ResourceManager.GetString("AsyncCallInsideUsingBlockDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Async call should be awaited before leaving the 'using' block. + /// + internal static string AsyncCallInsideUsingBlockMessage { + get { + return ResourceManager.GetString("AsyncCallInsideUsingBlockMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fire-and-forget async call inside a 'using' block. + /// + internal static string AsyncCallInsideUsingBlockTitle { + get { + return ResourceManager.GetString("AsyncCallInsideUsingBlockTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Accepting a CancellationToken as a parameter allows caller to express a loss of interest in the result enabling the method to save cycles by finishing early. + /// + internal static string AsyncMethodWithoutCancellationDescription { + get { + return ResourceManager.GetString("AsyncMethodWithoutCancellationDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add CancellationToken as the parameter of asynchronous method. + /// + internal static string AsyncMethodWithoutCancellationMessage { + get { + return ResourceManager.GetString("AsyncMethodWithoutCancellationMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The async method doesn't support cancellation. + /// + internal static string AsyncMethodWithoutCancellationTitle { + get { + return ResourceManager.GetString("AsyncMethodWithoutCancellationTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Using the null coalescing assignment operator (??=) with values which are statically known not to be null causes superfluous null checks to be performed at runtime. + /// + internal static string CoalesceAssignmentDescription { + get { + return ResourceManager.GetString("CoalesceAssignmentDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Consider removing unnecessary null coalescing assignment (??=) since the target value is statically known not to be null. + /// + internal static string CoalesceAssignmentMessage { + get { + return ResourceManager.GetString("CoalesceAssignmentMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Consider removing unnecessary null coalescing assignment (??=). + /// + internal static string CoalesceAssignmentTitle { + get { + return ResourceManager.GetString("CoalesceAssignmentTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Using the null coalescing operator (??) with values which are statically known to be null causes superfluous null checks to be performed at runtime. + /// + internal static string CoalesceDescription { + get { + return ResourceManager.GetString("CoalesceDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Consider removing unnecessary null coalescing (??) since the left-hand value is statically known not to be null. + /// + internal static string CoalesceMessage { + get { + return ResourceManager.GetString("CoalesceMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Consider removing unnecessary null coalescing operator (??). + /// + internal static string CoalesceTitle { + get { + return ResourceManager.GetString("CoalesceTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Using the conditional access operator (?) to access values which are statically known not to be null causes superfluous null checks to be performed at runtime. + /// + internal static string ConditionalAccessDescription { + get { + return ResourceManager.GetString("ConditionalAccessDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Consider removing unnecessary conditional access operator (?) since the value is statically known not to be null. + /// + internal static string ConditionalAccessMessage { + get { + return ResourceManager.GetString("ConditionalAccessMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Consider removing unnecessary conditional access operator (?). + /// + internal static string ConditionalAccessTitle { + get { + return ResourceManager.GetString("ConditionalAccessTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replace uses of 'Enum.GetName' and 'Enum.ToString' for improved performance. + /// + internal static string EnumStringsDescription { + get { + return ResourceManager.GetString("EnumStringsDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use {0} instead of '{1}' for improved performance. + /// + internal static string EnumStringsMessage { + get { + return ResourceManager.GetString("EnumStringsMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replace uses of 'Enum.GetName' and 'Enum.ToString' for improved performance. + /// + internal static string EnumStringsTitle { + get { + return ResourceManager.GetString("EnumStringsTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generate a strongly-typed logging method. + /// + internal static string GenerateStronglyTypedLoggingMethod { + get { + return ResourceManager.GetString("GenerateStronglyTypedLoggingMethod", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Using generic collections can avoid boxing overhead and provides strong typing. + /// + internal static string LegacyCollectionDescription { + get { + return ResourceManager.GetString("LegacyCollectionDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use generic collections instead of legacy collections for improved performance. + /// + internal static string LegacyCollectionMessage { + get { + return ResourceManager.GetString("LegacyCollectionMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use generic collections instead of legacy collections for improved performance. + /// + internal static string LegacyCollectionTitle { + get { + return ResourceManager.GetString("LegacyCollectionTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Identifies calls to legacy logging methods. + /// + internal static string LegacyLoggingDescription { + get { + return ResourceManager.GetString("LegacyLoggingDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use source generated logging methods for improved performance. + /// + internal static string LegacyLoggingMessage { + get { + return ResourceManager.GetString("LegacyLoggingMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use source generated logging methods for improved performance. + /// + internal static string LegacyLoggingTitle { + get { + return ResourceManager.GetString("LegacyLoggingTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Arrays of literal values should generally be assigned to static fields in order to avoid creating them redundantly over time. + /// + internal static string MakeArrayStaticDescription { + get { + return ResourceManager.GetString("MakeArrayStaticDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Assign array of literal values to a static field for improved performance. + /// + internal static string MakeArrayStaticMessage { + get { + return ResourceManager.GetString("MakeArrayStaticMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Assign array of literal values to a static field for improved performance. + /// + internal static string MakeArrayStaticTitle { + get { + return ResourceManager.GetString("MakeArrayStaticTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Making an executable's types internal enables dead code analysis along with other potential optimizations. + /// + internal static string MakeExeTypesInternalDescription { + get { + return ResourceManager.GetString("MakeExeTypesInternalDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Make type '{0}' internal since it is declared in an executable. + /// + internal static string MakeExeTypesInternalMessage { + get { + return ResourceManager.GetString("MakeExeTypesInternalMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Make types declared in an executable internal. + /// + internal static string MakeExeTypesInternalTitle { + get { + return ResourceManager.GetString("MakeExeTypesInternalTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Make the type internal. + /// + internal static string MakeTypeInternal { + get { + return ResourceManager.GetString("MakeTypeInternal", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to When compiling in a nullable context, the C# compiler performs null analysis at compile time so there is no need to also perform null checking at runtime. + /// + internal static string NullCheckDescription { + get { + return ResourceManager.GetString("NullCheckDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove superfluous null check when compiling in a nullable context. + /// + internal static string NullCheckMessage { + get { + return ResourceManager.GetString("NullCheckMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove superfluous null checks when compiling in a nullable context. + /// + internal static string NullCheckTitle { + get { + return ResourceManager.GetString("NullCheckTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replace explicit null check with call to 'Throws.IfNull' (needs a 'PackageReference' to 'Microsoft.R9.Extensions.Essentials'). + /// + internal static string ReplaceWithStaticNullCheckMethod { + get { + return ResourceManager.GetString("ReplaceWithStaticNullCheckMethod", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replace explicit throw with call to 'Throws' (needs a 'PackageReference' to 'Microsoft.R9.Extensions.Essentials'). + /// + internal static string ReplaceWithStaticThrowMethod { + get { + return ResourceManager.GetString("ReplaceWithStaticThrowMethod", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Apply code fix for all issues in '{0}' '{1}'. + /// + internal static string SequentialFixAllFormat { + get { + return ResourceManager.GetString("SequentialFixAllFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Apply code fix for all issues in current solution. + /// + internal static string SequentialFixAllInSolution { + get { + return ResourceManager.GetString("SequentialFixAllInSolution", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Text.StringSplitExtensions.TrySplit' for improved performance. + /// + internal static string SplitDescription { + get { + return ResourceManager.GetString("SplitDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Text.StringSplitExtensions.TrySplit' for improved performance. + /// + internal static string SplitMessage { + get { + return ResourceManager.GetString("SplitMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Text.StringSplitExtensions.TrySplit' for improved performance. + /// + internal static string SplitTitle { + get { + return ResourceManager.GetString("SplitTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to When checking for a single character, prefer the character overloads of 'String.StartsWith' and 'String.EndsWith' for improved performance. + /// + internal static string StartsEndsWithDescription { + get { + return ResourceManager.GetString("StartsEndsWithDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use the character-based overload of '{0}'. + /// + internal static string StartsEndsWithMessage { + get { + return ResourceManager.GetString("StartsEndsWithMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use the character-based overloads of 'String.StartsWith' or 'String.EndsWith'. + /// + internal static string StartsEndsWithTitle { + get { + return ResourceManager.GetString("StartsEndsWithTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Identifies uses of time dependent APIs that can lead to flaky tests. + /// + internal static string StaticTimeDescription { + get { + return ResourceManager.GetString("StaticTimeDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'System.TimeProvider' to make the code easier to test. + /// + internal static string StaticTimeMessage { + get { + return ResourceManager.GetString("StaticTimeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'System.TimeProvider' to make the code easier to test. + /// + internal static string StaticTimeTitle { + get { + return ResourceManager.GetString("StaticTimeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Identifies uses of 'String.Format' and 'StringBuilder.AppendFormat'. + /// + internal static string StringFormatDescription { + get { + return ResourceManager.GetString("StringFormatDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Text.CompositeFormat' instead of 'string.Format' for improved performance. + /// + internal static string StringFormatMessage { + get { + return ResourceManager.GetString("StringFormatMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Text.CompositeFormat' instead of 'string.Format' for improved performance. + /// + internal static string StringFormatTitle { + get { + return ResourceManager.GetString("StringFormatTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stable APIs cannot be changed in an incompatible way. + /// + internal static string TheSignatureOfAStableApiHasChangedDescription { + get { + return ResourceManager.GetString("TheSignatureOfAStableApiHasChangedDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The signature of a stable API misses '{1}'. + /// + internal static string TheSignatureOfAStableApiHasChangedMessage { + get { + return ResourceManager.GetString("TheSignatureOfAStableApiHasChangedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The signature of a stable API has changed. + /// + internal static string TheSignatureOfAStableApiHasChangedTitle { + get { + return ResourceManager.GetString("TheSignatureOfAStableApiHasChangedTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Recommends replacing explicit argument throwing with the more efficient 'Microsoft.R9.Extensions.Diagnostics.Throws' class. + /// + internal static string ThrowsExpressionDescription { + get { + return ResourceManager.GetString("ThrowsExpressionDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use '{0}' to throw the exception instead to improve performance. + /// + internal static string ThrowsExpressionMessage { + get { + return ResourceManager.GetString("ThrowsExpressionMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use the 'Microsoft.R9.Extensions.Diagnostics.Throws' class instead of explicitly throwing exception for improved performance. + /// + internal static string ThrowsExpressionTitle { + get { + return ResourceManager.GetString("ThrowsExpressionTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Recommends replacing explicit argument throwing with the more efficient 'Microsoft.R9.Extensions.Diagnostics.Throws' class. + /// + internal static string ThrowsStatementDescription { + get { + return ResourceManager.GetString("ThrowsStatementDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use '{0}' to throw the exception instead to improve performance. + /// + internal static string ThrowsStatementMessage { + get { + return ResourceManager.GetString("ThrowsStatementMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use the 'Microsoft.R9.Extensions.Diagnostics.Throws' class instead of explicitly throwing exception for improved performance. + /// + internal static string ThrowsStatementTitle { + get { + return ResourceManager.GetString("ThrowsStatementTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to 'Microsoft.R9.Extensions.Text.NumericExtensions.ToInvariantString' provides caching for common numeric values, avoiding the need to allocate new strings in many situations. + /// + internal static string ToInvariantStringDescription { + get { + return ResourceManager.GetString("ToInvariantStringDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Text.NumericExtensions.ToInvariantString' for improved performance. + /// + internal static string ToInvariantStringMessage { + get { + return ResourceManager.GetString("ToInvariantStringMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Text.NumericExtensions.ToInvariantString' for improved performance. + /// + internal static string ToInvariantStringTitle { + get { + return ResourceManager.GetString("ToInvariantStringTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Encourages optimal use of dictionary lookup. + /// + internal static string UsingExcessiveDictionaryLookupDescription { + get { + return ResourceManager.GetString("UsingExcessiveDictionaryLookupDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove excessive dictionary lookups. + /// + internal static string UsingExcessiveDictionaryLookupMessage { + get { + return ResourceManager.GetString("UsingExcessiveDictionaryLookupMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove unnecessary dictionary lookups. + /// + internal static string UsingExcessiveDictionaryLookupTitle { + get { + return ResourceManager.GetString("UsingExcessiveDictionaryLookupTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Encourages optimal use of set lookup. + /// + internal static string UsingExcessiveSetLookupDescription { + get { + return ResourceManager.GetString("UsingExcessiveSetLookupDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove excessive set lookups. + /// + internal static string UsingExcessiveSetLookupMessage { + get { + return ResourceManager.GetString("UsingExcessiveSetLookupMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove unnecessary set lookups. + /// + internal static string UsingExcessiveSetLookupTitle { + get { + return ResourceManager.GetString("UsingExcessiveSetLookupTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Indicates that code is depending on an experimental API. + /// + internal static string UsingExperimentalApiDescription { + get { + return ResourceManager.GetString("UsingExperimentalApiDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to '{0}' is experimental and is subject to change without notice. + /// + internal static string UsingExperimentalApiMessage { + get { + return ResourceManager.GetString("UsingExperimentalApiMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Using experimental API. + /// + internal static string UsingExperimentalApiTitle { + get { + return ResourceManager.GetString("UsingExperimentalApiTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Identifies calls to the 'ToString' method as arguments to an R9 logging method. + /// + internal static string UsingToStringInLoggersDescription { + get { + return ResourceManager.GetString("UsingToStringInLoggersDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Provide a logging method that accepts an instance of the object instead of a string. + /// + internal static string UsingToStringInLoggersMessage { + get { + return ResourceManager.GetString("UsingToStringInLoggersMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Perform message formatting in the body of the logging method. + /// + internal static string UsingToStringInLoggersTitle { + get { + return ResourceManager.GetString("UsingToStringInLoggersTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Using 'System.ValueTuple' avoids allocations and is generally more efficient than 'System.Tuple'. + /// + internal static string ValueTupleDescription { + get { + return ResourceManager.GetString("ValueTupleDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'System.ValueTuple' instead of 'System.Tuple' for improved performance. + /// + internal static string ValueTupleMessage { + get { + return ResourceManager.GetString("ValueTupleMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'System.ValueTuple' instead of 'System.Tuple' for improved performance. + /// + internal static string ValueTupleTitle { + get { + return ResourceManager.GetString("ValueTupleTitle", resourceCulture); + } + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Resources.resx b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Resources.resx new file mode 100644 index 0000000000..aa9f5ceba1 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Resources.resx @@ -0,0 +1,420 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Generate a strongly-typed logging method + + + Identifies calls to legacy logging methods + + + Use source generated logging methods for improved performance + + + Use source generated logging methods for improved performance + + + Recommends replacing explicit argument throwing with the more efficient 'Microsoft.R9.Extensions.Diagnostics.Throws' class + + + Use '{0}' to throw the exception instead to improve performance + + + Use the 'Microsoft.R9.Extensions.Diagnostics.Throws' class instead of explicitly throwing exception for improved performance + + + Recommends replacing explicit argument throwing with the more efficient 'Microsoft.R9.Extensions.Diagnostics.Throws' class + + + Use '{0}' to throw the exception instead to improve performance + + + Use the 'Microsoft.R9.Extensions.Diagnostics.Throws' class instead of explicitly throwing exception for improved performance + + + Replace explicit null check with call to 'Throws.IfNull' (needs a 'PackageReference' to 'Microsoft.R9.Extensions.Essentials') + + + Replace explicit throw with call to 'Throws' (needs a 'PackageReference' to 'Microsoft.R9.Extensions.Essentials') + + + Identifies uses of 'String.Format' and 'StringBuilder.AppendFormat' + + + Use 'Microsoft.R9.Extensions.Text.CompositeFormat' instead of 'string.Format' for improved performance + + + Use 'Microsoft.R9.Extensions.Text.CompositeFormat' instead of 'string.Format' for improved performance + + + Encourages optimal use of dictionary lookup + + + Remove excessive dictionary lookups + + + Remove unnecessary dictionary lookups + + + Encourages optimal use of set lookup + + + Remove excessive set lookups + + + Remove unnecessary set lookups + + + Identifies calls to the 'ToString' method as arguments to an R9 logging method + + + Provide a logging method that accepts an instance of the object instead of a string + + + Perform message formatting in the body of the logging method + + + Identifies uses of time dependent APIs that can lead to flaky tests + + + Use 'System.TimeProvider' to make the code easier to test + + + Use 'System.TimeProvider' to make the code easier to test + + + Apply code fix for all issues in '{0}' '{1}' + + + Apply code fix for all issues in current solution + + + Indicates that code is depending on an experimental API + + + Using experimental API + + + '{0}' is experimental and is subject to change without notice + + + When checking for a single character, prefer the character overloads of 'String.StartsWith' and 'String.EndsWith' for improved performance + + + Use the character-based overload of '{0}' + + + Use the character-based overloads of 'String.StartsWith' or 'String.EndsWith' + + + Making an executable's types internal enables dead code analysis along with other potential optimizations + + + Make type '{0}' internal since it is declared in an executable + + + Make types declared in an executable internal + + + Dictionaries and sets which use enums and bytes as keys can often be replaced with simple arrays for improved performance + + + Consider using '{0}?[]' instead of '{1}' + + + Consider using an array instead of a collection + + + Replace uses of 'Enum.GetName' and 'Enum.ToString' for improved performance + + + Replace uses of 'Enum.GetName' and 'Enum.ToString' for improved performance + + + Use {0} instead of '{1}' for improved performance + + + 'Microsoft.R9.Extensions.Text.NumericExtensions.ToInvariantString' provides caching for common numeric values, avoiding the need to allocate new strings in many situations + + + Use 'Microsoft.R9.Extensions.Text.NumericExtensions.ToInvariantString' for improved performance + + + Use 'Microsoft.R9.Extensions.Text.NumericExtensions.ToInvariantString' for improved performance + + + Using 'System.ValueTuple' avoids allocations and is generally more efficient than 'System.Tuple' + + + Use 'System.ValueTuple' instead of 'System.Tuple' for improved performance + + + Use 'System.ValueTuple' instead of 'System.Tuple' for improved performance + + + When compiling in a nullable context, the C# compiler performs null analysis at compile time so there is no need to also perform null checking at runtime + + + Remove superfluous null check when compiling in a nullable context + + + Remove superfluous null checks when compiling in a nullable context + + + Make the type internal + + + Using generic collections can avoid boxing overhead and provides strong typing + + + Use generic collections instead of legacy collections for improved performance + + + Use generic collections instead of legacy collections for improved performance + + + Use 'Microsoft.R9.Extensions.Text.StringSplitExtensions.TrySplit' for improved performance + + + Use 'Microsoft.R9.Extensions.Text.StringSplitExtensions.TrySplit' for improved performance + + + Use 'Microsoft.R9.Extensions.Text.StringSplitExtensions.TrySplit' for improved performance + + + Arrays of literal values should generally be assigned to static fields in order to avoid creating them redundantly over time + + + Assign array of literal values to a static field for improved performance + + + Assign array of literal values to a static field for improved performance + + + You can change experimental APIs at any time without deprecation period + + + Remove obsolete annotation from experimental API + + + An experimental API was marked as obsolete + + + Newly added externally visible API is not marked as experimental + + + Newly added externally visible API must be annotated as experimental + + + Newly added API must be annotated with experimental attribute + + + Stable APIs must follow deprecation policy and be marked as obsolete before it gets deleted. + + + Revert deletion of {0} API and mark it with obsolete attribute + + + A stable API was deleted outside the deprecation period + + + Stable APIs should not be annotated as experimental + + + Don't mark stable API as experimental + + + A stable API was marked as experimental + + + Deprecated API should have annotation that will guide customers regarding its replacement and the release in which it will be removed + + + Annotate deprecated API with obsolete attribute + + + A deprecated API is not annotated with the obsolete attribute + + + Annotate experimental API + + + Deprecated API cannot be marked as experimental + + + Remove experimental attribute from deprecated API + + + A deprecated API is marked as experimental + + + Stable APIs cannot be changed in an incompatible way + + + The signature of a stable API misses '{1}' + + + The signature of a stable API has changed + + + When skipping the await keyword for asynchronous operations inside a using block, then a disposable object could be disposed before the asynchronous invocation finishes. This might result in incorrect behavior and very often ends with a runtime exception notifying that the code is trying to operate on a disposed object. + + + Async call should be awaited before leaving the 'using' block + + + Fire-and-forget async call inside a 'using' block + + + Using the conditional access operator (?) to access values which are statically known not to be null causes superfluous null checks to be performed at runtime + + + Consider removing unnecessary conditional access operator (?) since the value is statically known not to be null + + + Consider removing unnecessary conditional access operator (?) + + + Using the null coalescing assignment operator (??=) with values which are statically known not to be null causes superfluous null checks to be performed at runtime + + + Consider removing unnecessary null coalescing assignment (??=) since the target value is statically known not to be null + + + Consider removing unnecessary null coalescing assignment (??=) + + + Using the null coalescing operator (??) with values which are statically known to be null causes superfluous null checks to be performed at runtime + + + Consider removing unnecessary null coalescing (??) since the left-hand value is statically known not to be null + + + Consider removing unnecessary null coalescing operator (??) + + + Accepting a CancellationToken as a parameter allows caller to express a loss of interest in the result enabling the method to save cycles by finishing early + + + Add CancellationToken as the parameter of asynchronous method + + + The async method doesn't support cancellation + + diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/StringFormatFixer.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/StringFormatFixer.cs new file mode 100644 index 0000000000..f01878b1ef --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/StringFormatFixer.cs @@ -0,0 +1,178 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.Extensions.ExtraAnalyzers.Utilities; + +namespace Microsoft.Extensions.ExtraAnalyzers; + +/// +/// Replace string.Format usage with Microsoft.Extensions.Text.CompositeFormat. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(StringFormatFixer))] +[Shared] +public sealed class StringFormatFixer : CodeFixProvider +{ + private const string TargetClass = "CompositeFormat"; + private const string TargetMethod = "Format"; + private const string VariableName = "_sf"; + private const int ArgumentsToSkip = 2; + private static readonly IdentifierNameSyntax _textNamespace = SyntaxFactory.IdentifierName("Microsoft.Extensions.Text"); + + /// + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DiagDescriptors.StringFormat.Id); + + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + /// + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostics = context.Diagnostics.First(); + context.RegisterCodeFix( + CodeAction.Create( + title: Resources.StringFormatTitle, + createChangedDocument: cancellationToken => ApplyFixAsync(context.Document, diagnostics.Location, diagnostics.Properties, cancellationToken), + equivalenceKey: nameof(Resources.StringFormatTitle)), + context.Diagnostics); + + return Task.CompletedTask; + } + + private static async Task ApplyFixAsync(Document document, Location diagnosticLocation, IReadOnlyDictionary properties, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + if (editor.OriginalRoot.FindNode(diagnosticLocation.SourceSpan) is InvocationExpressionSyntax expression) + { + var classDeclaration = GetTypeDeclaration(expression); + if (classDeclaration != null) + { + var (format, argList) = GetFormatAndArguments(editor, expression); + var formatKind = format.ChildNodes().First().Kind(); + + if (formatKind is SyntaxKind.StringLiteralExpression) + { + var (identifier, field) = GetFieldDeclaration(editor, classDeclaration, format); + var invocation = properties.ContainsKey("StringFormat") + ? CreateInvocationExpression(editor, identifier, argList.Arguments, expression) + : GetStringBuilderExpression(editor, identifier, argList.Arguments, expression); + ApplyChanges(editor, expression, invocation, classDeclaration, field); + } + } + } + + return editor.GetChangedDocument(); + } + + private static TypeDeclarationSyntax? GetTypeDeclaration(SyntaxNode node) + { + return node.FirstAncestorOrSelf(n => n.IsKind(SyntaxKind.ClassDeclaration) || n.IsKind(SyntaxKind.StructDeclaration)); + } + + private static (string identifier, FieldDeclarationSyntax? field) GetFieldDeclaration(SyntaxEditor editor, SyntaxNode classDeclaration, SyntaxNode format) + { + var members = classDeclaration.DescendantNodes().OfType(); + int numberOfMembers = 1; + + var strExp = format.ToString(); + + var arguments = SyntaxFactory.Argument(SyntaxFactory.ParseExpression(strExp)); + HashSet fields = new HashSet(); + + foreach (var member in members) + { + var fieldName = member.DescendantNodes().OfType().First().Identifier.ToString(); + _ = fields.Add(fieldName); + + if (member.Declaration.Type.ToString() == "CompositeFormat") + { + if (member.DescendantNodes().OfType().First().ArgumentList!.Arguments.First().ToString() == strExp) + { + return (member.DescendantNodes().OfType().First().Identifier.ToString(), null); + } + + numberOfMembers++; + } + } + + string variableName; + do + { + variableName = $"{VariableName}{numberOfMembers}"; + numberOfMembers++; + } + while (!IsFieldNameAvailable(fields, variableName)); + + return (variableName, editor.Generator.FieldDeclaration( + variableName, + SyntaxFactory.ParseTypeName(TargetClass), + Accessibility.Private, + DeclarationModifiers.Static | DeclarationModifiers.ReadOnly, + SyntaxFactory.ObjectCreationExpression( + SyntaxFactory.Token(SyntaxKind.NewKeyword), + SyntaxFactory.IdentifierName(TargetClass), + SyntaxFactory.ArgumentList().AddArguments(arguments), + null)) as FieldDeclarationSyntax); + } + + private static bool IsFieldNameAvailable(ICollection fields, string field) + { + return !fields.Contains(field); + } + + private static (ArgumentSyntax argument, ArgumentListSyntax argumentList) GetFormatAndArguments(DocumentEditor editor, InvocationExpressionSyntax invocation) + { + var arguments = invocation.ArgumentList.Arguments; + var first = arguments[0]; + var typeInfo = editor.SemanticModel.GetTypeInfo(first.ChildNodes().First()); + SeparatedSyntaxList separatedList; + if (arguments.Count > 1 && typeInfo.Type!.AllInterfaces.Any(i => i.MetadataName == "IFormatProvider")) + { + separatedList = SyntaxFactory.SingletonSeparatedList(first).AddRange(arguments.Skip(ArgumentsToSkip)); + return (arguments[1], SyntaxFactory.ArgumentList(separatedList)); + } + + var nullArgument = SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)); + separatedList = SyntaxFactory.SingletonSeparatedList(nullArgument).AddRange(arguments.Skip(1)); + return (first, SyntaxFactory.ArgumentList(separatedList)); + } + + private static SyntaxNode CreateInvocationExpression(SyntaxEditor editor, string identifierName, IEnumerable arguments, SyntaxNode invocation) + { + var gen = editor.Generator; + var identifier = gen.IdentifierName(identifierName); + var memberAccessExpression = gen.MemberAccessExpression(identifier, TargetMethod); + return gen.InvocationExpression(memberAccessExpression, arguments).WithTriviaFrom(invocation); + } + + private static void ApplyChanges(SyntaxEditor editor, SyntaxNode oldInvocation, SyntaxNode newInvocation, SyntaxNode classDeclaration, SyntaxNode? field) + { + if (field != null) + { + editor.AddMember(classDeclaration, field); + } + + editor.ReplaceNode(oldInvocation, newInvocation); + editor.TryAddUsingDirective(_textNamespace); + } + + private static SyntaxNode GetStringBuilderExpression(SyntaxEditor editor, string identifierName, IEnumerable arguments, InvocationExpressionSyntax invocation) + { + var gen = editor.Generator; + var identifier = gen.IdentifierName(identifierName); + var memberAccessExpression = gen.Argument(identifier); + var list = SyntaxFactory.SingletonSeparatedList(memberAccessExpression).AddRange(arguments); + return invocation.WithArgumentList(SyntaxFactory.ArgumentList(list)); + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExcessiveDictionaryLookupAnalyzer.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExcessiveDictionaryLookupAnalyzer.cs new file mode 100644 index 0000000000..473cd86b2b --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExcessiveDictionaryLookupAnalyzer.cs @@ -0,0 +1,321 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Extensions.ExtraAnalyzers.Utilities; + +namespace Microsoft.Extensions.ExtraAnalyzers; + +/// +/// C# analyzer that finds excessive dictionary lookups. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UsingExcessiveDictionaryLookupAnalyzer : DiagnosticAnalyzer +{ + private static readonly HashSet _containsKeyMethodFullNames = new(StringComparer.Ordinal) + { + "System.Collections.Generic.IDictionary.ContainsKey(TKey)", + "System.Collections.Generic.Dictionary.ContainsKey(TKey)", + "System.Collections.Generic.IReadOnlyDictionary.ContainsKey(TKey)" + }; + + private static readonly HashSet _addMethodFullNames = new(StringComparer.Ordinal) + { + "System.Collections.Generic.IDictionary.Add(TKey, TValue)", + "System.Collections.Generic.IDictionary.TryAdd(TKey, TValue)", + "System.Collections.Generic.Dictionary.TryAdd(TKey, TValue)", + "System.Collections.Generic.Dictionary.Add(TKey, TValue)" + }; + + private static readonly HashSet _otherMethodsFullNamesToCheck = new(_addMethodFullNames, StringComparer.Ordinal) + { + "System.Collections.Generic.IDictionary.Remove(TKey, out TValue)", + "System.Collections.Generic.IDictionary.Remove(TKey)", + "System.Collections.Generic.IDictionary.TryGetValue(TKey, out TValue)", + "System.Collections.Generic.Dictionary.Remove(TKey, out TValue)", + "System.Collections.Generic.Dictionary.Remove(TKey)", + "System.Collections.Generic.Dictionary.TryGetValue(TKey, out TValue)", + "System.Collections.Generic.IReadOnlyDictionary.TryGetValue(TKey, out TValue)" + }; + + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagDescriptors.UsingExcessiveDictionaryLookup); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(syntaxNodeContext => + { + var ifStatement = (IfStatementSyntax)syntaxNodeContext.Node; + var invocationExpression = GetInvocationExpression(ifStatement.Condition)!; + if (!invocationExpression.NodeHasSpecifiedMethod(syntaxNodeContext.SemanticModel, _containsKeyMethodFullNames)) + { + return; + } + + string? expectedDictionaryIdentifierNameText = + invocationExpression.Expression is MemberAccessExpressionSyntax memberAccessExpression && + memberAccessExpression.Expression is IdentifierNameSyntax identifierNameSyntax + ? identifierNameSyntax.Identifier.Text + : null; + + if (expectedDictionaryIdentifierNameText == null) + { + return; + } + + var expectedContainsMethodArgument = invocationExpression.ArgumentList.Arguments[0]; + if (expectedContainsMethodArgument!.DescendantNodesAndSelf().Any(w => w is InvocationExpressionSyntax)) + { + return; + } + + var expectedContainsMethodArgumentText = expectedContainsMethodArgument!.GetText().ToString(); + + if (ifStatement.Else != null) + { + string? valueAddedToDictionaryInElse = ProcessStatementSyntaxAndGetAssignedValue( + ifStatement.Else.Statement, + syntaxNodeContext.SemanticModel, + expectedDictionaryIdentifierNameText, + expectedContainsMethodArgumentText); + + if (!string.IsNullOrEmpty(valueAddedToDictionaryInElse) && + valueAddedToDictionaryInElse!.Equals( + ProcessStatementSyntaxAndGetAssignedValue( + ifStatement.Statement, + syntaxNodeContext.SemanticModel, + expectedDictionaryIdentifierNameText, + expectedContainsMethodArgumentText), + StringComparison.OrdinalIgnoreCase)) + { + CreateDiagnosticAndReport(invocationExpression.GetLocation(), syntaxNodeContext); + } + + return; + } + + bool isDictionaryUsedInIfBody = ifStatement.Statement.DescendantNodes() + .Any(w => w is IdentifierNameSyntax id && id.Identifier.Text == expectedDictionaryIdentifierNameText); + + if (isDictionaryUsedInIfBody) + { + if (ifStatement.Statement is BlockSyntax block) + { + if (block.Statements.Count > 1) + { + CheckMultilineBlockAndReportDiagnostic( + block, + syntaxNodeContext, + expectedDictionaryIdentifierNameText, + expectedContainsMethodArgumentText, + invocationExpression.GetLocation()); + } + else + { + CheckSingleLineBlockAndReportDiagnostic( + block.Statements[0], + syntaxNodeContext, + expectedDictionaryIdentifierNameText, + expectedContainsMethodArgumentText, + invocationExpression.GetLocation()); + } + } + else + { + CheckSingleLineBlockAndReportDiagnostic( + ifStatement.Statement, + syntaxNodeContext, + expectedDictionaryIdentifierNameText, + expectedContainsMethodArgumentText, + invocationExpression.GetLocation()); + } + } + else if (ifStatement.Parent is BlockSyntax parentBlock) + { + var next = parentBlock.Statements.SkipWhile(w => w != ifStatement).Skip(1).FirstOrDefault(); + CheckSingleLineBlockAndReportDiagnostic( + next, + syntaxNodeContext, + expectedDictionaryIdentifierNameText, + expectedContainsMethodArgumentText, + null); + } + }, SyntaxKind.IfStatement); + } + + private static string? ProcessStatementSyntaxAndGetAssignedValue( + StatementSyntax statement, + SemanticModel semanticModel, + string expectedDictionaryIdentifierName, + string expectedContainsMethodArgumentText) + { + if (statement is BlockSyntax block) + { + if (block.Statements.Count == 1 && block.Statements[0] is ExpressionStatementSyntax expressionStatement) + { + return ProcessExpressionStatementSyntaxAndGetAssignedValue(expressionStatement, semanticModel, expectedDictionaryIdentifierName, expectedContainsMethodArgumentText); + } + } + else if (statement is ExpressionStatementSyntax expressionStatement) + { + return ProcessExpressionStatementSyntaxAndGetAssignedValue(expressionStatement, semanticModel, expectedDictionaryIdentifierName, expectedContainsMethodArgumentText); + } + + return null; + } + + private static string? ProcessExpressionStatementSyntaxAndGetAssignedValue( + ExpressionStatementSyntax expressionStatement, + SemanticModel semanticModel, + string expectedDictionaryIdentifierName, + string expectedContainsMethodArgumentText) + { + if (expressionStatement.Expression.NodeHasSpecifiedMethod(semanticModel, _addMethodFullNames)) + { + var foundInvocation = (InvocationExpressionSyntax)expressionStatement.Expression; + if (CheckIndentifierNameAndFirstArgument( + (foundInvocation.Expression as MemberAccessExpressionSyntax)!.Expression, + foundInvocation.ArgumentList, + expectedDictionaryIdentifierName, + expectedContainsMethodArgumentText)) + { + return foundInvocation.ArgumentList.Arguments[1].Expression.GetText().ToString(); + } + } + + if (expressionStatement.Expression is AssignmentExpressionSyntax assignmentExpression) + { + if (assignmentExpression.Left is ElementAccessExpressionSyntax elementAccessExpr && + CheckIndentifierNameAndFirstArgument( + elementAccessExpr.Expression, + elementAccessExpr.ArgumentList, + expectedDictionaryIdentifierName, + expectedContainsMethodArgumentText)) + { + return assignmentExpression.Right.GetText().ToString(); + } + } + + return null; + } + + private static void CheckMultilineBlockAndReportDiagnostic( + SyntaxNode block, + SyntaxNodeAnalysisContext syntaxNodeContext, + string expectedDictionaryIdentifierNameText, + string expectedContainsMethodArgumentText, + Location invocationExpressionLocation) + { + var dictionaryItemAccessExpressions = + block + .DescendantNodes() + .Where(w => w is ElementAccessExpressionSyntax elementAccessExpr && + CheckIndentifierNameAndFirstArgument( + elementAccessExpr.Expression, + elementAccessExpr.ArgumentList, + expectedDictionaryIdentifierNameText, + expectedContainsMethodArgumentText)); + + bool isInefficientDictionaryUsageFound = dictionaryItemAccessExpressions.Any( + w => + { + var isRightPartOfAssignmentExpression = w.Parent is AssignmentExpressionSyntax a && a.Right == w; + var isRightPartOfEqualsValueClause = w.Parent is EqualsValueClauseSyntax e && e.Value == w; + var isUsedAsArgument = w.Parent is ArgumentSyntax arg && arg.Expression == w; + + return isRightPartOfAssignmentExpression || isRightPartOfEqualsValueClause || isUsedAsArgument; + }); + + if (isInefficientDictionaryUsageFound) + { + CreateDiagnosticAndReport(invocationExpressionLocation, syntaxNodeContext); + } + } + + private static InvocationExpressionSyntax? GetInvocationExpression(ExpressionSyntax expression) + { + if (expression is PrefixUnaryExpressionSyntax logicalExpr && logicalExpr.IsKind(SyntaxKind.LogicalNotExpression)) + { + return logicalExpr.Operand as InvocationExpressionSyntax; + } + + return expression as InvocationExpressionSyntax; + } + + private static bool CheckIndentifierNameAndFirstArgument(ExpressionSyntax expression, BaseArgumentListSyntax argumentListSyntax, string expectedIndetifierName, string expectedFirstArgument) + { + return expression.IdentifierNameEquals(expectedIndetifierName) && + argumentListSyntax.Arguments[0].GetText().ToString() == expectedFirstArgument; + } + + private static void CheckSingleLineBlockAndReportDiagnostic( + StatementSyntax statementSyntax, + SyntaxNodeAnalysisContext syntaxNodeContext, + string expectedDictionaryIdentifierName, + string expectedContainsMethodArgumentText, + Location? locationToReport) + { + if (statementSyntax is ExpressionStatementSyntax expressionStatement) + { + if (CheckSingleLineBlock(expressionStatement.Expression, syntaxNodeContext.SemanticModel, expectedDictionaryIdentifierName, expectedContainsMethodArgumentText)) + { + CreateDiagnosticAndReport(locationToReport ?? expressionStatement.Expression.GetLocation(), syntaxNodeContext); + return; + } + + if (expressionStatement.Expression is AssignmentExpressionSyntax assignmentExpression) + { + if (assignmentExpression.Left is ElementAccessExpressionSyntax elementAccessExpr && + CheckIndentifierNameAndFirstArgument(elementAccessExpr.Expression, elementAccessExpr.ArgumentList, expectedDictionaryIdentifierName, expectedContainsMethodArgumentText)) + { + CreateDiagnosticAndReport(locationToReport ?? assignmentExpression.Left.GetLocation(), syntaxNodeContext); + return; + } + + bool shouldReport = assignmentExpression.Right is ElementAccessExpressionSyntax rightElementAccessExpr && + CheckIndentifierNameAndFirstArgument( + rightElementAccessExpr.Expression, + rightElementAccessExpr.ArgumentList, + expectedDictionaryIdentifierName, + expectedContainsMethodArgumentText); + if (shouldReport || CheckSingleLineBlock(assignmentExpression.Right, syntaxNodeContext.SemanticModel, expectedDictionaryIdentifierName, expectedContainsMethodArgumentText)) + { + CreateDiagnosticAndReport(locationToReport ?? assignmentExpression.Right.GetLocation(), syntaxNodeContext); + } + } + } + } + + private static void CreateDiagnosticAndReport(Location locationToReport, SyntaxNodeAnalysisContext syntaxNodeContext) + { + var diagnostic = Diagnostic.Create(DiagDescriptors.UsingExcessiveDictionaryLookup, locationToReport); + syntaxNodeContext.ReportDiagnostic(diagnostic); + } + + private static bool CheckSingleLineBlock( + SyntaxNode node, + SemanticModel semanticModel, + string expectedDictionaryIdentifierName, + string expectedContainsMethodArgumentText) + { + if (node.NodeHasSpecifiedMethod(semanticModel, _otherMethodsFullNamesToCheck)) + { + var foundInvocation = (InvocationExpressionSyntax)node; + var memberAccessExpression = foundInvocation.Expression as MemberAccessExpressionSyntax; + return CheckIndentifierNameAndFirstArgument(memberAccessExpression!.Expression, foundInvocation.ArgumentList, expectedDictionaryIdentifierName, expectedContainsMethodArgumentText); + } + + return false; + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExcessiveDictionaryLookupFixer.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExcessiveDictionaryLookupFixer.cs new file mode 100644 index 0000000000..9ad135434a --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExcessiveDictionaryLookupFixer.cs @@ -0,0 +1,241 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.Extensions.ExtraAnalyzers.Utilities; + +namespace Microsoft.Extensions.ExtraAnalyzers; + +/// +/// Removes excessive dictionary lookups. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UsingExcessiveDictionaryLookupFixer))] +[Shared] +public sealed class UsingExcessiveDictionaryLookupFixer : CodeFixProvider +{ + private const string ContainsKeyMethodName = "ContainsKey"; + private static readonly SimpleNameSyntax _tryGetValueMethod = (SimpleNameSyntax)SyntaxFactory.ParseName("TryGetValue"); + private static readonly SimpleNameSyntax _tryAddMethod = (SimpleNameSyntax)SyntaxFactory.ParseName("TryAdd"); + private static readonly HashSet _removeAndTryGetMethodFullNames = new() + { + "System.Collections.Generic.IDictionary.Remove(TKey, out TValue)", + "System.Collections.Generic.IDictionary.Remove(TKey)", + "System.Collections.Generic.Dictionary.Remove(TKey)", + "System.Collections.Generic.Dictionary.Remove(TKey, out TValue)", + "System.Collections.Generic.IDictionary.TryGetValue(TKey, out TValue)", + "System.Collections.Generic.Dictionary.TryGetValue(TKey, out TValue)", + "System.Collections.Generic.IReadOnlyDictionary.TryGetValue(TKey, out TValue)" + }; + + private static readonly HashSet _methodsFullNamesToBeHanled = new(_removeAndTryGetMethodFullNames) + { + "System.Collections.Generic.IDictionary.Add(TKey, TValue)", + "System.Collections.Generic.IDictionary.TryAdd(TKey, TValue)", + "System.Collections.Generic.Dictionary.TryAdd(TKey, TValue)", + "System.Collections.Generic.Dictionary.Add(TKey, TValue)" + }; + + /// + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DiagDescriptors.UsingExcessiveDictionaryLookup.Id); + + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + /// + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + context.RegisterCodeFix( + CodeAction.Create( + title: Resources.UsingExcessiveDictionaryLookupTitle, + createChangedDocument: cancellationToken => ApplyFixAsync(context.Document, context.Diagnostics.First().Location, cancellationToken), + equivalenceKey: nameof(Resources.UsingExcessiveDictionaryLookupTitle)), + context.Diagnostics); + + return Task.CompletedTask; + } + + private static async Task ApplyFixAsync(Document document, Location diagnosticLocation, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + + var invocationExpression = editor.OriginalRoot.FindNode(diagnosticLocation.SourceSpan) as InvocationExpressionSyntax; + var memberAccessExpression = invocationExpression?.Expression as MemberAccessExpressionSyntax; + if (memberAccessExpression == null || memberAccessExpression.Name.GetText().ToString() != ContainsKeyMethodName) + { + return document; + } + + var expectedDictionaryIdentifier = (IdentifierNameSyntax)memberAccessExpression.Expression; + + var expectedKeyArgument = invocationExpression!.ArgumentList.Arguments[0]; + var ifStatementSyntax = (IfStatementSyntax)invocationExpression.GetFirstAncestorOfSyntaxKind(SyntaxKind.IfStatement)!; + + if (ifStatementSyntax.Else != null) + { + var separatedList = SyntaxFactory.SeparatedList(new[] { expectedKeyArgument }); + var bracketedArgumentList = SyntaxFactory.BracketedArgumentList(SyntaxFactory.ParseToken("["), separatedList, SyntaxFactory.ParseToken("]")); + var elementAccessExpression = SyntaxFactory.ElementAccessExpression(expectedDictionaryIdentifier, bracketedArgumentList); + + var expressionStatementSyntax = (ExpressionStatementSyntax)(ifStatementSyntax.Else.Statement is BlockSyntax blockInsideElse + ? blockInsideElse.Statements[0] + : ifStatementSyntax.Else.Statement); + + var assignedValue = expressionStatementSyntax.Expression is InvocationExpressionSyntax inv + ? inv.ArgumentList.Arguments[1].Expression + : ((AssignmentExpressionSyntax)expressionStatementSyntax.Expression).Right; + + var simpleAssignmentExpression = SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, elementAccessExpression, assignedValue); + + return CreateExpressionStatementAndReplaceOldNode(simpleAssignmentExpression, ifStatementSyntax, editor); + } + + var expectedDictionaryIdentifierNameText = expectedDictionaryIdentifier.Identifier.Text; + var expectedKeyText = expectedKeyArgument.GetText().ToString(); + if (ifStatementSyntax.Statement is BlockSyntax block) + { + if (block.Statements.Count > 1) + { + return ProcessIfWithMultilineBody(editor, ifStatementSyntax, invocationExpression, expectedDictionaryIdentifierNameText, expectedKeyText); + } + + return ProcessIfWithSingleLineBody(editor, ifStatementSyntax, invocationExpression, expectedDictionaryIdentifierNameText, block.Statements[0]); + } + + return ProcessIfWithSingleLineBody(editor, ifStatementSyntax, invocationExpression, expectedDictionaryIdentifierNameText, ifStatementSyntax.Statement); + } + + private static Document ProcessIfWithMultilineBody( + DocumentEditor editor, + IfStatementSyntax ifStatement, + InvocationExpressionSyntax invocationExpression, + string dictionaryName, + string expectedKeyText) + { + // replaces ContainsKey by TryGetValue + var newInvocationExpression = CreateTryGetValueInvocationExpression(invocationExpression); + var newIfStatement = ifStatement.ReplaceNode(invocationExpression, newInvocationExpression); + + // replaces dictionary item access expressions by 'retrievedValue' in 'if' body + var dictionaryItemAccessExpressionsToReplace = + newIfStatement.Statement + .DescendantNodes() + .Where(w => w is ElementAccessExpressionSyntax elementAccessExpr && + CheckIndentifierNameAndFirstArgument( + elementAccessExpr.Expression, + elementAccessExpr.ArgumentList, + dictionaryName, + expectedKeyText)); + + var newIdentifierName = SyntaxFactory.IdentifierName("retrievedValue"); + newIfStatement = newIfStatement.ReplaceNodes(dictionaryItemAccessExpressionsToReplace, (_, _) => newIdentifierName); + editor.ReplaceNode(ifStatement, newIfStatement); + + return editor.GetChangedDocument(); + } + + private static Document CreateExpressionStatementAndReplaceOldNode(ExpressionSyntax newExpressionSyntax, SyntaxNode oldSyntaxNode, DocumentEditor editor) + { + var newExpression = SyntaxFactory.ExpressionStatement(newExpressionSyntax, SyntaxFactory.ParseToken(";")).WithTriviaFrom(oldSyntaxNode); + editor.ReplaceNode(oldSyntaxNode, newExpression); + return editor.GetChangedDocument(); + } + + private static Document ProcessIfWithSingleLineBody( + DocumentEditor editor, + SyntaxNode ifStatement, + InvocationExpressionSyntax invocationExpression, + string dictionaryName, + StatementSyntax statementSyntax) + { + ExpressionSyntax expression = ((ExpressionStatementSyntax)statementSyntax).Expression; + + if (expression is AssignmentExpressionSyntax assignmentExpression) + { + if (assignmentExpression!.Left is ElementAccessExpressionSyntax elementAccessExpr && + elementAccessExpr.Expression.IdentifierNameEquals(dictionaryName)) + { + // replaces if with ContainsKey by TryAdd + ExpressionSyntax newExpr = ((MemberAccessExpressionSyntax)invocationExpression.Expression) + .WithName(_tryAddMethod).WithAdditionalAnnotations(Formatter.Annotation); + + var newArguments = invocationExpression.ArgumentList.AddArguments(SyntaxFactory.Argument(assignmentExpression.Right)); + var newInvocationExpr = invocationExpression.WithExpression(newExpr).WithArgumentList(newArguments); + + return CreateExpressionStatementAndReplaceOldNode(newInvocationExpr, ifStatement, editor); + } + + if (assignmentExpression!.Right is ElementAccessExpressionSyntax elementAccessExprRight && + elementAccessExprRight.Expression.IdentifierNameEquals(dictionaryName)) + { + // replaces if with ContainsKey by TryGetValue + var newTryGetValueInvocationExpr = CreateTryGetValueInvocationExpression(invocationExpression); + + editor.ReplaceNode(invocationExpression, newTryGetValueInvocationExpr); + + var identifierName = SyntaxFactory.IdentifierName("retrievedValue"); + editor.ReplaceNode(assignmentExpression.Right, identifierName); + return editor.GetChangedDocument(); + } + + return CreateExpressionStatementAndReplaceOldNode(assignmentExpression, ifStatement, editor); + } + + var removeOrAddOrTryGetSyntaxNode = expression + .DescendantNodesAndSelf() + .First(w => w.NodeHasSpecifiedMethod(editor.SemanticModel, _methodsFullNamesToBeHanled)); + + return CheckInvocation(removeOrAddOrTryGetSyntaxNode, editor, ifStatement); + } + + private static Document CheckInvocation( + SyntaxNode node, + DocumentEditor editor, + SyntaxNode ifStatement) + { + if (node.NodeHasSpecifiedMethod(editor.SemanticModel, _removeAndTryGetMethodFullNames)) + { + // replaces "if" containing "ContainsKey" with "Remove" or "TryGetValue" + return CreateExpressionStatementAndReplaceOldNode((InvocationExpressionSyntax)node, ifStatement, editor); + } + + // replaces "if" containing "ContainsKey" with "TryAdd" + var foundInvocation = (InvocationExpressionSyntax)node; + ExpressionSyntax newExpr = ((MemberAccessExpressionSyntax)foundInvocation.Expression) + .WithName(_tryAddMethod).WithAdditionalAnnotations(Formatter.Annotation); + var newInvocationExpr = foundInvocation.WithExpression(newExpr); + + return CreateExpressionStatementAndReplaceOldNode(newInvocationExpr, ifStatement, editor); + } + + private static InvocationExpressionSyntax CreateTryGetValueInvocationExpression(InvocationExpressionSyntax oldInvocation) + { + ExpressionSyntax newExpr = ((MemberAccessExpressionSyntax)oldInvocation.Expression) + .WithName(_tryGetValueMethod).WithAdditionalAnnotations(Formatter.Annotation); + + var newArgument = SyntaxFactory.DeclarationExpression( + SyntaxFactory.IdentifierName(SyntaxFactory.ParseToken("var")).WithTrailingTrivia(SyntaxFactory.TriviaList(SyntaxFactory.Whitespace(" "))), + SyntaxFactory.SingleVariableDesignation(SyntaxFactory.ParseToken("retrievedValue"))); + + var newArguments = oldInvocation.ArgumentList.AddArguments(SyntaxFactory.Argument(null, SyntaxFactory.ParseToken("out"), newArgument)); + + return oldInvocation.WithExpression(newExpr).WithArgumentList(newArguments); + } + + private static bool CheckIndentifierNameAndFirstArgument(ExpressionSyntax expression, BaseArgumentListSyntax argumentListSyntax, string expectedIndetifierName, string expectedFirstArgument) + { + return expression.IdentifierNameEquals(expectedIndetifierName) && + argumentListSyntax.Arguments[0].GetText().ToString() == expectedFirstArgument; + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExcessiveSetLookupAnalyzer.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExcessiveSetLookupAnalyzer.cs new file mode 100644 index 0000000000..3771e199b5 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExcessiveSetLookupAnalyzer.cs @@ -0,0 +1,225 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Extensions.ExtraAnalyzers.Utilities; + +namespace Microsoft.Extensions.ExtraAnalyzers; + +/// +/// C# analyzer that finds excessive set lookups. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UsingExcessiveSetLookupAnalyzer : DiagnosticAnalyzer +{ + private const string SetCollectionFullName = "System.Collections.Generic.ISet"; + private static readonly HashSet _collectionContainsMethodFullName = new(StringComparer.Ordinal) + { + "System.Collections.Generic.ICollection.Contains(T)" + }; + + private static readonly HashSet _containsMethodFullNames = new(StringComparer.Ordinal) + { + "System.Collections.Generic.SortedSet.Contains(T)", + "System.Collections.Generic.HashSet.Contains(T)", + "System.Collections.Immutable.ImmutableHashSet.Contains(T)", + "System.Collections.Immutable.ImmutableSortedSet.Contains(T)", + "System.Collections.Immutable.ImmutableHashSet.Builder.Contains(T)", + "System.Collections.Immutable.ImmutableSortedSet.Builder.Contains(T)" + }; + + private static readonly HashSet _methodsReturningBool = new(StringComparer.Ordinal) + { + "System.Collections.Generic.SortedSet.Remove(T)", + "System.Collections.Generic.HashSet.Remove(T)", + "System.Collections.Generic.ICollection.Remove(T)", + "System.Collections.Immutable.ImmutableHashSet.Builder.Remove(T)", + "System.Collections.Immutable.ImmutableSortedSet.Builder.Remove(T)", + + "System.Collections.Generic.SortedSet.TryGetValue(T, out T)", + "System.Collections.Generic.HashSet.TryGetValue(T, out T)", + "System.Collections.Immutable.ImmutableHashSet.TryGetValue(T, out T)", + "System.Collections.Immutable.ImmutableSortedSet.TryGetValue(T, out T)", + + "System.Collections.Generic.SortedSet.Add(T)", + "System.Collections.Generic.HashSet.Add(T)", + "System.Collections.Generic.ISet.Add(T)", + "System.Collections.Immutable.ImmutableHashSet.Builder.Add(T)", + "System.Collections.Immutable.ImmutableSortedSet.Builder.Add(T)" + }; + + private static readonly HashSet _otherMethodsFullNamesToCheck = new(_methodsReturningBool, StringComparer.Ordinal) + { + "System.Collections.Immutable.ImmutableHashSet.Remove(T)", + "System.Collections.Immutable.ImmutableSortedSet.Remove(T)", + "System.Collections.Immutable.ImmutableHashSet.Add(T)", + "System.Collections.Immutable.ImmutableSortedSet.Add(T)", + }; + + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagDescriptors.UsingExcessiveSetLookup); + + /// + public override void Initialize(AnalysisContext context) + { + _ = context ?? throw new ArgumentNullException(nameof(context)); + + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(syntaxNodeContext => + { + var ifStatement = (IfStatementSyntax)syntaxNodeContext.Node; + if (ifStatement.Else != null) + { + return; + } + + var invocationExpression = GetInvocationExpression(ifStatement.Condition)!; + if (!invocationExpression.NodeHasSpecifiedMethod(syntaxNodeContext.SemanticModel, _containsMethodFullNames) + && !(invocationExpression.NodeHasSpecifiedMethod(syntaxNodeContext.SemanticModel, _collectionContainsMethodFullName) + && NodeHasSpecifiedType(invocationExpression, syntaxNodeContext.SemanticModel))) + { + return; + } + + string? expectedSetIdentifierNameText = + invocationExpression.Expression is MemberAccessExpressionSyntax memberAccessExpression && + memberAccessExpression.Expression is IdentifierNameSyntax identifierNameSyntax + ? identifierNameSyntax.Identifier.Text + : null; + + if (expectedSetIdentifierNameText == null) + { + return; + } + + var expectedContainsMethodArgument = invocationExpression.ArgumentList.Arguments[0]; + if (expectedContainsMethodArgument!.DescendantNodesAndSelf().Any(w => w is InvocationExpressionSyntax)) + { + return; + } + + var expectedContainsMethodArgumentText = expectedContainsMethodArgument!.GetText().ToString(); + + bool isSetUsedInIfBody = ifStatement.Statement.DescendantNodes() + .Any(w => w is IdentifierNameSyntax id && id.Identifier.Text == expectedSetIdentifierNameText); + + if (isSetUsedInIfBody) + { + var lineToCheck = ifStatement.Statement is BlockSyntax block ? block.Statements[0] : ifStatement.Statement; + CheckSingleLineBlockAndReportDiagnostic( + lineToCheck, + syntaxNodeContext, + expectedSetIdentifierNameText, + expectedContainsMethodArgumentText, + invocationExpression.GetLocation()); + } + else if (ifStatement.Parent is BlockSyntax parentBlock) + { + var next = parentBlock.Statements.SkipWhile(w => w != ifStatement).Skip(1).FirstOrDefault(); + CheckSingleLineBlockAndReportDiagnostic( + next, + syntaxNodeContext, + expectedSetIdentifierNameText, + expectedContainsMethodArgumentText, + null, + checkOnlyMethodsReturningBool: true); + } + }, SyntaxKind.IfStatement); + } + + private static bool NodeHasSpecifiedType( + InvocationExpressionSyntax? invocationExpression, + SemanticModel semanticModel) + { + if (invocationExpression != null) + { + var memberAccessExpression = (MemberAccessExpressionSyntax)invocationExpression.Expression; + if (memberAccessExpression!.Expression is IdentifierNameSyntax identifierNameSyntax) + { + var symbol = semanticModel.GetTypeInfo(identifierNameSyntax).Type; + return symbol!.OriginalDefinition!.ToString() == SetCollectionFullName; + } + } + + return false; + } + + private static InvocationExpressionSyntax? GetInvocationExpression(ExpressionSyntax expression) + { + if (expression is PrefixUnaryExpressionSyntax logicalExpr && logicalExpr.IsKind(SyntaxKind.LogicalNotExpression)) + { + return logicalExpr.Operand as InvocationExpressionSyntax; + } + + return expression as InvocationExpressionSyntax; + } + + private static bool CheckIndentifierNameAndFirstArgument(ExpressionSyntax expression, BaseArgumentListSyntax argumentListSyntax, string expectedIndetifierName, string expectedFirstArgument) + { + return expression.IdentifierNameEquals(expectedIndetifierName) && + argumentListSyntax.Arguments[0].GetText().ToString() == expectedFirstArgument; + } + + private static void CheckSingleLineBlockAndReportDiagnostic( + StatementSyntax statementSyntax, + SyntaxNodeAnalysisContext syntaxNodeContext, + string expectedSetIdentifierName, + string expectedContainsMethodArgumentText, + Location? locationToReport, + bool checkOnlyMethodsReturningBool = false) + { + if (statementSyntax is ExpressionStatementSyntax expressionStatement) + { + if (expressionStatement.Expression is AssignmentExpressionSyntax assignmentExpression) + { + var invocationExpr = GetInvocationExpression(assignmentExpression.Right); + if (assignmentExpression.Left.IdentifierNameEquals("_") && + CheckSingleLineBlock(invocationExpr, syntaxNodeContext.SemanticModel, expectedSetIdentifierName, expectedContainsMethodArgumentText, _methodsReturningBool)) + { + CreateDiagnosticAndReport(locationToReport ?? invocationExpr!.GetLocation(), syntaxNodeContext); + } + + return; + } + + var invocationExpression = GetInvocationExpression(expressionStatement.Expression); + var methodsToCheck = checkOnlyMethodsReturningBool ? _methodsReturningBool : _otherMethodsFullNamesToCheck; + if (CheckSingleLineBlock(invocationExpression, syntaxNodeContext.SemanticModel, expectedSetIdentifierName, expectedContainsMethodArgumentText, methodsToCheck)) + { + CreateDiagnosticAndReport(locationToReport ?? invocationExpression!.GetLocation(), syntaxNodeContext); + } + } + } + + private static bool CheckSingleLineBlock( + SyntaxNode? node, + SemanticModel semanticModel, + string expectedSetIdentifierName, + string expectedContainsMethodArgumentText, + ICollection methodsToCheck) + { + if (node.NodeHasSpecifiedMethod(semanticModel, methodsToCheck)) + { + var foundInvocation = (InvocationExpressionSyntax)node!; + var memberAccessExpression = (MemberAccessExpressionSyntax)foundInvocation.Expression; + return CheckIndentifierNameAndFirstArgument(memberAccessExpression!.Expression, foundInvocation.ArgumentList, expectedSetIdentifierName, expectedContainsMethodArgumentText); + } + + return false; + } + + private static void CreateDiagnosticAndReport(Location locationToReport, SyntaxNodeAnalysisContext syntaxNodeContext) + { + var diagnostic = Diagnostic.Create(DiagDescriptors.UsingExcessiveSetLookup, locationToReport); + syntaxNodeContext.ReportDiagnostic(diagnostic); + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExcessiveSetLookupFixer.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExcessiveSetLookupFixer.cs new file mode 100644 index 0000000000..6f9a32bdd4 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExcessiveSetLookupFixer.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.Extensions.ExtraAnalyzers.Utilities; + +namespace Microsoft.Extensions.ExtraAnalyzers; + +/// +/// Removes excessive lookups. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UsingExcessiveSetLookupFixer))] +[Shared] +public sealed class UsingExcessiveSetLookupFixer : CodeFixProvider +{ + private const string ContainsMethodName = "Contains"; + private const string AddMethodName = "Add"; + private const string RemoveMethodName = "Remove"; + + /// + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DiagDescriptors.UsingExcessiveSetLookup.Id); + + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + /// + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + context.RegisterCodeFix( + CodeAction.Create( + title: Resources.UsingExcessiveSetLookupTitle, + createChangedDocument: cancellationToken => ApplyFixAsync(context.Document, context.Diagnostics.First().Location, cancellationToken), + equivalenceKey: nameof(Resources.UsingExcessiveSetLookupTitle)), + context.Diagnostics); + + return Task.CompletedTask; + } + + private static async Task ApplyFixAsync(Document document, Location diagnosticLocation, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + var invocationExpression = (InvocationExpressionSyntax)editor.OriginalRoot.FindNode(diagnosticLocation.SourceSpan); + var memberAccessExpression = (MemberAccessExpressionSyntax)invocationExpression.Expression; + var methodName = memberAccessExpression.Name.GetText().ToString(); + if (methodName == ContainsMethodName) + { + var ifStatementSyntax = (IfStatementSyntax)invocationExpression.GetFirstAncestorOfSyntaxKind(SyntaxKind.IfStatement)!; + + if (ifStatementSyntax.Statement is BlockSyntax block) + { + if (block.Statements.Count > 1) + { + var newInvocation = ((ExpressionStatementSyntax)block.Statements[0]).Expression.WithTriviaFrom(invocationExpression); + editor.ReplaceNode(invocationExpression, newInvocation); + var newBlock = block.WithStatements(block.Statements.RemoveAt(0)); + editor.ReplaceNode(block, newBlock); + } + else + { + editor.ReplaceNode(ifStatementSyntax, block.Statements[0].WithTriviaFrom(ifStatementSyntax)); + } + + return editor.GetChangedDocument(); + } + + editor.ReplaceNode(ifStatementSyntax, ifStatementSyntax.Statement.WithTriviaFrom(ifStatementSyntax)); + return editor.GetChangedDocument(); + } + + if (methodName == AddMethodName || methodName == RemoveMethodName) + { + var nodeToRemove = invocationExpression.GetFirstAncestorOfSyntaxKind(SyntaxKind.ExpressionStatement); + var blockStatements = ((BlockSyntax)nodeToRemove!.Parent!).Statements; + int ifStatementIndex = blockStatements.IndexOf((StatementSyntax)nodeToRemove) - 1; + var ifStatement = (IfStatementSyntax)blockStatements[ifStatementIndex]; + + var newNode = SyntaxFactory.PrefixUnaryExpression(SyntaxKind.LogicalNotExpression, invocationExpression); + editor.ReplaceNode(ifStatement.Condition, newNode); + editor.RemoveNode(nodeToRemove!); + return editor.GetChangedDocument(); + } + + return document; + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExperimentalApiAnalyzer.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExperimentalApiAnalyzer.cs new file mode 100644 index 0000000000..9b1e3fa8f0 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingExperimentalApiAnalyzer.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.Extensions.ExtraAnalyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class UsingExperimentalApiAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create(DiagDescriptors.UsingExperimentalApi); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterCompilationStartAction(context => + { + context.RegisterSyntaxNodeAction(context => + { + var sn = (IdentifierNameSyntax)context.Node; + if (sn.IsVar) + { + return; + } + + var sym = context.SemanticModel.GetSymbolInfo(sn).Symbol; + + if (sym != null && HasExperimentalAttribute(sym)) + { + var diagnostic = Diagnostic.Create(DiagDescriptors.UsingExperimentalApi, sn.GetLocation(), sym.Name); + context.ReportDiagnostic(diagnostic); + } + else if (sym is INamedTypeSymbol type && HasExperimentalAttribute(type.ContainingAssembly)) + { + var diagnostic = Diagnostic.Create(DiagDescriptors.UsingExperimentalApi, sn.GetLocation(), type.ContainingAssembly.Name); + context.ReportDiagnostic(diagnostic); + } + }, SyntaxKind.IdentifierName); + + static bool HasExperimentalAttribute(ISymbol sym) + { + foreach (var attributeData in sym.GetAttributes()) + { + if (attributeData.AttributeClass?.Name == "ExperimentalAttribute") + { + var ns = attributeData.AttributeClass.ContainingNamespace.ToString(); + if (ns is "System.Diagnostics.CodeAnalysis" or "Microsoft.Extensions.Diagnostics") + { + return true; + } + } + } + + return false; + } + }); + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingToStringInLoggersAnalyzer.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingToStringInLoggersAnalyzer.cs new file mode 100644 index 0000000000..5e7caa272b --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/UsingToStringInLoggersAnalyzer.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.ExtraAnalyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UsingToStringInLoggersAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagDescriptors.UsingToStringInLoggers); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationStartContext => + { + compilationStartContext.RegisterOperationBlockStartAction(operationBlockContext => + { + if (operationBlockContext.OwningSymbol.Kind != SymbolKind.Method) + { + return; + } + + operationBlockContext.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation); + }); + }); + } + + private static void AnalyzeInvocation(OperationAnalysisContext context) + { + var invocation = (IInvocationOperation)context.Operation; + if (IsLoggerMethod(invocation.TargetMethod)) + { + foreach (var diagnostic in AnalyzeLogger(invocation)) + { + context.ReportDiagnostic(diagnostic); + } + } + } + + [ExcludeFromCodeCoverage] + private static bool IsLoggerMethod(ISymbol symbol) + { + return symbol.GetAttributes().Any(a => a.AttributeClass != null && IsLogMethodAttribute(a.AttributeClass)); + } + + private static bool IsLogMethodAttribute(ISymbol attributeSymbol) + { + return attributeSymbol.Name == "LogMethodAttribute" + && attributeSymbol.ContainingNamespace.ToString() == "Microsoft.Extensions.Telemetry.Logging"; + } + + private static IEnumerable AnalyzeLogger(IInvocationOperation invocation) + { + foreach (var arg in invocation.Arguments) + { + if (arg.Value is IInvocationOperation argOperation + && argOperation.TargetMethod.Name == "ToString") + { + yield return Diagnostic.Create(DiagDescriptors.UsingToStringInLoggers, arg.Syntax.GetLocation()); + } + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/CompilationExtensions.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/CompilationExtensions.cs new file mode 100644 index 0000000000..5a9fba780f --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/CompilationExtensions.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.ExtraAnalyzers.Utilities; + +internal static class CompilationExtensions +{ + public static bool IsNet6OrGreater(this Compilation compilation) + { + var type = compilation.GetTypeByMetadataName("System.Environment"); + return type != null && type.GetMembers("ProcessPath").Length > 0; + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/OperationExtensions.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/OperationExtensions.cs new file mode 100644 index 0000000000..a9c91e9976 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/OperationExtensions.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.ExtraAnalyzers.Utilities; + +internal static class OperationExtensions +{ + /// + /// Gets the list of ancestor operations up to the specified operation. + /// + /// Node to start traversing. + /// Node to stop traversing. + /// The enumerator. + public static IEnumerable Ancestors(this IOperation operationToStart, IOperation parent) + { + while (operationToStart.Parent != null) + { + if (operationToStart.Parent == parent) + { + yield break; + } + + yield return operationToStart.Parent; + operationToStart = operationToStart.Parent; + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/SymbolExtensions.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/SymbolExtensions.cs new file mode 100644 index 0000000000..b136791636 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/SymbolExtensions.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.ExtraAnalyzers.Utilities; + +internal static class SymbolExtensions +{ + /// + /// Determines whether the current instance is an ancestor type of the parameter. + /// + /// The potential ancestor being inspected. + /// The type to test. + /// if derives directly or indirectly from . + public static bool IsAncestorOf(this ITypeSymbol potentialAncestor, ITypeSymbol potentialDescendant) + { + ITypeSymbol? t = potentialDescendant; + while (true) + { + t = t.BaseType; + if (t == null) + { + return false; + } + + if (SymbolEqualityComparer.Default.Equals(t, potentialAncestor)) + { + return true; + } + } + } + + /// + /// True if the symbol is externally visible outside this assembly. + /// + public static bool IsExternallyVisible(this ISymbol symbol) + { + while (symbol.Kind != SymbolKind.Namespace) + { + switch (symbol.DeclaredAccessibility) + { + // If we see anything private, then the symbol is private. + case Accessibility.NotApplicable: + case Accessibility.Private: + return false; + + // If we see anything internal, then knock it down from public to + // internal. + case Accessibility.Internal: + case Accessibility.ProtectedAndInternal: + return false; + } + + symbol = symbol.ContainingSymbol; + } + + return true; + } + + public static bool ImplementsPublicInterface(this IMethodSymbol method) + { + foreach (var iface in method.ContainingType.AllInterfaces) + { + if (iface.IsExternallyVisible()) + { + foreach (var member in iface.GetMembers().OfType()) + { + var impl = method.ContainingType.FindImplementationForInterfaceMember(member); + if (SymbolEqualityComparer.Default.Equals(impl, method)) + { + return true; + } + } + } + } + + return false; + } + + /// + /// Checks if a symbol has the queried fully qualified name. + /// + /// The symbol to check. + /// The fully qualified name to check against. + /// True if the symbol has the provided fully qualified name, false otherwise. + public static bool HasFullyQualifiedName(this ISymbol symbol, string fullyQualifiedName) + { + if (symbol is not null) + { + var actualSymbolFullName = symbol.ToDisplayString(); + return actualSymbolFullName.Equals(fullyQualifiedName, System.StringComparison.Ordinal); + } + + return false; + } + + /// + /// Checks if a type has the specified base type. + /// + /// The type being checked. + /// The fully qualified name of the base type to look for. + /// True if the type has the specified base type, false otherwise. + public static bool InheritsFromType(this ITypeSymbol type, string baseTypeFullName) + { + if (type is not null) + { + while (type.BaseType != null) + { + var actualBaseTypeFullName = string.Concat(type.BaseType.ContainingNamespace, ".", type.BaseType.Name); + if (actualBaseTypeFullName.Equals(baseTypeFullName, System.StringComparison.Ordinal)) + { + return true; + } + + type = type.BaseType; + } + } + + return false; + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/SyntaxEditorExtensions.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/SyntaxEditorExtensions.cs new file mode 100644 index 0000000000..10dc369f87 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/SyntaxEditorExtensions.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; + +namespace Microsoft.Extensions.ExtraAnalyzers.Utilities; + +/// +/// Class contains extensions. +/// +internal static class SyntaxEditorExtensions +{ + /// + /// Tries to add using directive. + /// + /// The syntax editor. + /// The namespace name. + public static void TryAddUsingDirective(this SyntaxEditor editor, NameSyntax namespaceName) + { + if (editor.GetChangedRoot() is CompilationUnitSyntax documentRoot) + { + var anyUsings = documentRoot.Usings.Any(u => u.Name.GetText().ToString().Equals(namespaceName.ToString(), StringComparison.Ordinal)); + var usingDirective = SyntaxFactory.UsingDirective(namespaceName); + documentRoot = anyUsings ? documentRoot : documentRoot.AddUsings(usingDirective).WithAdditionalAnnotations(Formatter.Annotation); + editor.ReplaceNode(editor.OriginalRoot, documentRoot); + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/SyntaxNodeExtensions.cs b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/SyntaxNodeExtensions.cs new file mode 100644 index 0000000000..7569252ca1 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Common/Utilities/SyntaxNodeExtensions.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Extensions.ExtraAnalyzers.Utilities; + +/// +/// Class contains extensions. +/// +internal static class SyntaxNodeExtensions +{ + /// + /// Finds closest ancestor by syntax kind. + /// + /// Start node. + /// Kind to search by. + /// Found node or null. + public static SyntaxNode? GetFirstAncestorOfSyntaxKind(this SyntaxNode node, SyntaxKind kind) + { + var n = node.Parent; + while (n != null && !n.IsKind(kind)) + { + n = n.Parent; + } + + return n; + } + + /// + /// Checks node is invocation expression with specified name. + /// + /// Node to check. + /// Semantic model. + /// Expected full method names. + /// Check result. + public static bool NodeHasSpecifiedMethod( + this SyntaxNode? node, + SemanticModel semanticModel, + ICollection expectedFullMethodNames) + { + if (node is InvocationExpressionSyntax invocationExpression) + { + var memberSymbol = semanticModel.GetSymbolInfo(invocationExpression.Expression).Symbol as IMethodSymbol; + if (memberSymbol == null) + { + return false; + } + + var result = false; + if (memberSymbol.ReducedFrom != null) + { + var fullMethodName = memberSymbol.ReducedFrom.ToString(); + result = expectedFullMethodNames.Contains(fullMethodName); + } + + if (!result) + { + var fullMethodName = memberSymbol.OriginalDefinition.ToString(); + return expectedFullMethodNames.Contains(fullMethodName); + } + + return result; + } + + return false; + } + + /// + /// Returns invocation expression name. + /// + /// The invocation expression. + /// The expression syntax name. + public static SimpleNameSyntax? GetExpressionName(this InvocationExpressionSyntax invocationExpression) + { + if (invocationExpression.Expression is MemberAccessExpressionSyntax memberExpression) + { + return memberExpression.Name; + } + + if (invocationExpression.Expression is MemberBindingExpressionSyntax memberBindingExpression) + { + return memberBindingExpression.Name; + } + + return null; + } + + /// + /// Looks for a invocation node in a tree with a specified root type. + /// + /// Node to start traversing. + /// The semantic model. + /// Expected full method names. + /// Root node types. + /// Found invocation node or null. + public static SyntaxNode? FindNodeInTreeUpToSpecifiedParentByMethodName( + this SyntaxNode nodeToStart, + SemanticModel semanticModel, + ICollection expectedFullMethodNames, + ICollection typesToStopTraversing) + { + var currentNode = nodeToStart; + do + { + var foundNode = currentNode.DescendantNodesAndSelf() + .FirstOrDefault(n => n.NodeHasSpecifiedMethod(semanticModel, expectedFullMethodNames)); + if (foundNode != null) + { + return foundNode; + } + + currentNode = currentNode.Parent; + } + while (currentNode != null && !typesToStopTraversing.Contains(currentNode.GetType())); + + return currentNode? + .DescendantNodesAndSelf() + .FirstOrDefault(n => n.NodeHasSpecifiedMethod(semanticModel, expectedFullMethodNames)); + } + + /// + /// Checks has expected name. + /// + /// Expression syntax to check. + /// Expected name. + /// if the identifier text is equal to expected name; otherwise, . + public static bool IdentifierNameEquals(this ExpressionSyntax expression, string expectedName) + { + return expression is IdentifierNameSyntax id && id.Identifier.Text == expectedName; + } +} diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Directory.Build.props b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Directory.Build.props new file mode 100644 index 0000000000..5841060902 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Directory.Build.props @@ -0,0 +1,33 @@ + + + + + Microsoft.Extesions.ExtraAnalyzers + Code analyzers and fixers + Fundamentals + Static Analysis + + + + true + true + cs + + + + + + + + + + + + + + + + + + + diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Microsoft.Extensions.ExtraAnalyzers.Roslyn3.8/Microsoft.Extensions.ExtraAnalyzers.Roslyn3.8.csproj b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Microsoft.Extensions.ExtraAnalyzers.Roslyn3.8/Microsoft.Extensions.ExtraAnalyzers.Roslyn3.8.csproj new file mode 100644 index 0000000000..7145f9e34a --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Microsoft.Extensions.ExtraAnalyzers.Roslyn3.8/Microsoft.Extensions.ExtraAnalyzers.Roslyn3.8.csproj @@ -0,0 +1,22 @@ + + + 3.8 + + + + normal + 92 + n/a + + + + + + + + + + + + + diff --git a/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Microsoft.Extensions.ExtraAnalyzers.Roslyn4.0/Microsoft.Extensions.ExtraAnalyzers.Roslyn4.0.csproj b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Microsoft.Extensions.ExtraAnalyzers.Roslyn4.0/Microsoft.Extensions.ExtraAnalyzers.Roslyn4.0.csproj new file mode 100644 index 0000000000..ba652d0365 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.ExtraAnalyzers/Microsoft.Extensions.ExtraAnalyzers.Roslyn4.0/Microsoft.Extensions.ExtraAnalyzers.Roslyn4.0.csproj @@ -0,0 +1,16 @@ + + + 4.0 + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + + + normal + 92 + 87 + + + + + + diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/ApiLifecycleAnalyzer.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/ApiLifecycleAnalyzer.cs new file mode 100644 index 0000000000..194370f326 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/ApiLifecycleAnalyzer.cs @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Extensions.LocalAnalyzers.ApiLifecycle.Model; +using Microsoft.Extensions.LocalAnalyzers.Utilities; + +namespace Microsoft.Extensions.LocalAnalyzers.ApiLifecycle; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ApiLifecycleAnalyzer : DiagnosticAnalyzer +{ + private const string ExperimentalAttributeFullName = "System.Diagnostics.CodeAnalysis.ExperimentalAttribute"; + private const string ObsoleteAttributeFullName = "System.ObsoleteAttribute"; + + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create( + DiagDescriptors.NewSymbolsMustBeMarkedExperimental, + DiagDescriptors.ExperimentalSymbolsCantBeMarkedObsolete, + DiagDescriptors.PublishedSymbolsCantBeMarkedExperimental, + DiagDescriptors.PublishedSymbolsCantBeDeleted, + DiagDescriptors.PublishedSymbolsCantChange); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterCompilationStartAction(start => + { + var compilation = start.Compilation; + + if (ModelLoader.TryLoadAssemblyModel(start, out var assemblyModel)) + { + start.RegisterCompilationEndAction(endContext => ReportDiagnosticForModel(endContext, Analyze(endContext.Compilation, assemblyModel))); + } + else if (assemblyModel == null) + { + start.RegisterCompilationEndAction(endContext => CheckAllPublicTypesAreExperimentalAndNotObsolete(endContext)); + } + }); + } + + private static AssemblyAnalysis Analyze(Compilation compilation, Assembly? assemblyModel) + { + var types = compilation + .GetSymbolsWithName(_ => true) + .Where(symbol => symbol.IsExternallyVisible() && symbol.Kind == SymbolKind.NamedType) + .Cast(); + + var assemblyAnalysis = new AssemblyAnalysis(assemblyModel ?? Assembly.Empty); + foreach (var type in types) + { + assemblyAnalysis.AnalyzeType(type); + } + + return assemblyAnalysis; + } + + private static void ReportDiagnosticForModel(CompilationAnalysisContext context, AssemblyAnalysis assemblyAnalysis) + { + var compilation = context.Compilation; + var obsoleteAttribute = compilation.GetTypeByMetadataName(ObsoleteAttributeFullName); + var experimentalAttribute = compilation.GetTypeByMetadataName(ExperimentalAttributeFullName); + + // flag symbols found in the code, but not in the model + foreach (var symbol in assemblyAnalysis.NotFoundInBaseline) + { + if (!symbol.IsContaminated(experimentalAttribute)) + { + context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.NewSymbolsMustBeMarkedExperimental, symbol.Locations.FirstOrDefault(), symbol)); + } + } + + // flag any stable or deprecated API in the model, but not in the assembly + + foreach (var type in assemblyAnalysis.MissingTypes.Where(x => x.Stage != Stage.Experimental)) + { + context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.PublishedSymbolsCantBeDeleted, null, type.ModifiersAndName)); + } + + foreach (var method in assemblyAnalysis.MissingMethods.Where(x => x.Stage != Stage.Experimental)) + { + context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.PublishedSymbolsCantBeDeleted, null, method.Member)); + } + + foreach (var prop in assemblyAnalysis.MissingProperties.Where(x => x.Stage != Stage.Experimental)) + { + context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.PublishedSymbolsCantBeDeleted, null, prop.Member)); + } + + foreach (var field in assemblyAnalysis.MissingFields.Where(x => x.Stage != Stage.Experimental)) + { + context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.PublishedSymbolsCantBeDeleted, null, field.Member)); + } + + // now make sure attributes are applied correctly + foreach (var (symbol, stage) in assemblyAnalysis.FoundInBaseline) + { + var isMarkedExperimental = symbol.IsContaminated(experimentalAttribute); + var isMarkedObsolete = symbol.IsContaminated(obsoleteAttribute); + + if (stage == Stage.Experimental) + { + if (!isMarkedExperimental) + { + context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.NewSymbolsMustBeMarkedExperimental, symbol.Locations.FirstOrDefault(), symbol)); + } + + if (isMarkedObsolete) + { + context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.ExperimentalSymbolsCantBeMarkedObsolete, symbol.Locations.FirstOrDefault(), symbol)); + } + } + else + { + if (isMarkedExperimental) + { + context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.PublishedSymbolsCantBeMarkedExperimental, symbol.Locations.FirstOrDefault(), symbol)); + } + + if (assemblyAnalysis.MissingConstraints.TryGetValue(symbol, out var missingContraintsForSymbol)) + { + if (missingContraintsForSymbol.Count > 0) + { + context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.PublishedSymbolsCantChange, symbol.Locations.FirstOrDefault(), symbol)); + } + } + + if (assemblyAnalysis.MissingBaseTypes.TryGetValue(symbol, out var missingBaseForSymbol)) + { + if (missingBaseForSymbol.Count > 0) + { + context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.PublishedSymbolsCantChange, symbol.Locations.FirstOrDefault(), symbol)); + } + } + } + } + } + + private static void CheckAllPublicTypesAreExperimentalAndNotObsolete(CompilationAnalysisContext context) + { + var types = context + .Compilation + .GetSymbolsWithName(_ => true) + .Where(symbol => symbol.IsExternallyVisible() && symbol.Kind == SymbolKind.NamedType) + .Cast(); + + var experimentalAttribute = context.Compilation.GetTypeByMetadataName(ExperimentalAttributeFullName); + var obsoleteAttribute = context.Compilation.GetTypeByMetadataName(ObsoleteAttributeFullName); + + foreach (var type in types) + { + if (!type.IsContaminated(experimentalAttribute)) + { + context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.NewSymbolsMustBeMarkedExperimental, type.Locations.FirstOrDefault(), type)); + } + else if (type.IsContaminated(obsoleteAttribute)) + { + context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.ExperimentalSymbolsCantBeMarkedObsolete, type.Locations.FirstOrDefault(), type)); + } + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/ApiLifecycleFixer.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/ApiLifecycleFixer.cs new file mode 100644 index 0000000000..1f9a48e9df --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/ApiLifecycleFixer.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; + +namespace Microsoft.Extensions.LocalAnalyzers.ApiLifecycle; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ApiLifecycleFixer))] +[Shared] +public sealed class ApiLifecycleFixer : CodeFixProvider +{ + private const string ExperimentalAttributeName = "Experimental"; + private const string ExperimentalAttributeNamespace = "System.Diagnostics.CodeAnalysis"; + + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( + DiagDescriptors.NewSymbolsMustBeMarkedExperimental.Id); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var location = context.Diagnostics.First().Location; + + context.RegisterCodeFix( + CodeAction.Create( + title: Resources.AnnotateExperimentalApi, + createChangedDocument: cancelToken => AddExperimentalAttributeAsync(context, location, cancelToken), + equivalenceKey: nameof(Resources.AnnotateExperimentalApi)), + context.Diagnostics); + + return Task.CompletedTask; + } + + private static async Task AddExperimentalAttributeAsync(CodeFixContext context, Location argumentToChangeLocation, + CancellationToken cancelToken) + { + var document = context.Document; + var editor = await DocumentEditor.CreateAsync(document, cancelToken).ConfigureAwait(false); + var member = (MemberDeclarationSyntax)editor.OriginalRoot.FindNode(argumentToChangeLocation.SourceSpan)!; + var lead = member.GetLeadingTrivia(); + var attributeLists = member!.AttributeLists; + + var withoutTriviaMember = member.WithoutLeadingTrivia(); + + attributeLists = attributeLists.Add(SyntaxFactory.AttributeList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Attribute( + SyntaxFactory.IdentifierName(ExperimentalAttributeName))))); + + if (editor.GetChangedRoot() is CompilationUnitSyntax mutableRoot) + { + mutableRoot = mutableRoot.ReplaceNode(member, withoutTriviaMember.WithAttributeLists(attributeLists).WithLeadingTrivia(lead)); + + if (!mutableRoot.Usings.Any(@using => @using.Name.ToString() == ExperimentalAttributeNamespace)) + { + mutableRoot = mutableRoot.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName(ExperimentalAttributeNamespace))); + } + + editor.ReplaceNode(editor.OriginalRoot, mutableRoot); + } + + return editor.GetChangedDocument(); + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/AssemblyAnalysis.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/AssemblyAnalysis.cs new file mode 100644 index 0000000000..5c597799eb --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/AssemblyAnalysis.cs @@ -0,0 +1,303 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.LocalAnalyzers.ApiLifecycle.Model; +using Microsoft.Extensions.LocalAnalyzers.Utilities; + +namespace Microsoft.Extensions.LocalAnalyzers.ApiLifecycle; + +internal sealed class AssemblyAnalysis +{ + public Assembly Assembly { get; } + public HashSet MissingTypes { get; } = new(); + public Dictionary> MissingConstraints { get; } = new(SymbolEqualityComparer.Default); + public Dictionary> MissingBaseTypes { get; } = new(SymbolEqualityComparer.Default); + public HashSet MissingMethods { get; } = new(); + public HashSet MissingProperties { get; } = new(); + public HashSet MissingFields { get; } = new(); + public HashSet<(ISymbol symbol, Stage stage)> FoundInBaseline { get; } = new(); + public HashSet NotFoundInBaseline { get; } = new(SymbolEqualityComparer.Default); + +#pragma warning disable SA1118 // Parameter should not span multiple lines + private static readonly SymbolDisplayFormat _format = + new(globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | + SymbolDisplayGenericsOptions.IncludeTypeConstraints | + SymbolDisplayGenericsOptions.IncludeVariance, + kindOptions: SymbolDisplayKindOptions.IncludeNamespaceKeyword | + SymbolDisplayKindOptions.IncludeTypeKeyword | + SymbolDisplayKindOptions.IncludeMemberKeyword, + parameterOptions: + SymbolDisplayParameterOptions.IncludeExtensionThis | + SymbolDisplayParameterOptions.IncludeParamsRefOut | + SymbolDisplayParameterOptions.IncludeType | + SymbolDisplayParameterOptions.IncludeName | + SymbolDisplayParameterOptions.IncludeDefaultValue, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes | + SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier, + delegateStyle: SymbolDisplayDelegateStyle.NameAndSignature); + + private static readonly SymbolDisplayFormat _formatNoVariance = + new(globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + propertyStyle: SymbolDisplayPropertyStyle.ShowReadWriteDescriptor, + memberOptions: SymbolDisplayMemberOptions.IncludeType | + SymbolDisplayMemberOptions.IncludeModifiers | + SymbolDisplayMemberOptions.IncludeExplicitInterface | + SymbolDisplayMemberOptions.IncludeParameters | + SymbolDisplayMemberOptions.IncludeContainingType | + SymbolDisplayMemberOptions.IncludeRef, + kindOptions: SymbolDisplayKindOptions.IncludeNamespaceKeyword | + SymbolDisplayKindOptions.IncludeTypeKeyword | + SymbolDisplayKindOptions.IncludeMemberKeyword, + parameterOptions: + SymbolDisplayParameterOptions.IncludeExtensionThis | + SymbolDisplayParameterOptions.IncludeParamsRefOut | + SymbolDisplayParameterOptions.IncludeType | + SymbolDisplayParameterOptions.IncludeName | + SymbolDisplayParameterOptions.IncludeDefaultValue, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes | + SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier, + delegateStyle: SymbolDisplayDelegateStyle.NameAndSignature); + + private static readonly SymbolDisplayFormat _shortSymbolNameFormat = + new(globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes | + SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier, + delegateStyle: SymbolDisplayDelegateStyle.NameAndSignature); + + private static readonly SymbolDisplayFormat _enumType = + new(globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | + SymbolDisplayGenericsOptions.IncludeTypeConstraints | + SymbolDisplayGenericsOptions.IncludeVariance, + propertyStyle: SymbolDisplayPropertyStyle.ShowReadWriteDescriptor, + memberOptions: SymbolDisplayMemberOptions.IncludeType | + SymbolDisplayMemberOptions.IncludeExplicitInterface | + SymbolDisplayMemberOptions.IncludeParameters | + SymbolDisplayMemberOptions.IncludeContainingType | + SymbolDisplayMemberOptions.IncludeModifiers | + SymbolDisplayMemberOptions.IncludeRef, + kindOptions: SymbolDisplayKindOptions.IncludeNamespaceKeyword | + SymbolDisplayKindOptions.IncludeMemberKeyword, + parameterOptions: + SymbolDisplayParameterOptions.IncludeExtensionThis | + SymbolDisplayParameterOptions.IncludeParamsRefOut | + SymbolDisplayParameterOptions.IncludeType | + SymbolDisplayParameterOptions.IncludeName | + SymbolDisplayParameterOptions.IncludeDefaultValue, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes | + SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier, + delegateStyle: SymbolDisplayDelegateStyle.NameAndSignature); + +#pragma warning restore SA1118 // Parameter should not span multiple lines + + public AssemblyAnalysis(Assembly assembly) + { + Assembly = assembly; + + foreach (var type in Assembly.Types) + { + _ = MissingTypes.Add(type); + + foreach (var method in type.Methods) + { + _ = MissingMethods.Add(method); + } + + foreach (var property in type.Properties) + { + _ = MissingProperties.Add(property); + } + + foreach (var field in type.Fields) + { + _ = MissingFields.Add(field); + } + } + } + + public void AnalyzeType(INamedTypeSymbol type) + { + var typeSignature = type.ToDisplayString(_format); + var typeModifiersAndName = PrependModifiers(typeSignature, type); + var typeDef = Assembly.Types.FirstOrDefault(x => x.ModifiersAndName == typeModifiersAndName); + + if (typeDef == null) + { + _ = NotFoundInBaseline.Add(type); + return; + } + + var baseTypes = new HashSet(type.AllInterfaces.Select(x => x.ToDisplayString(_shortSymbolNameFormat))); + var baseType = type.BaseType; + + if (baseType != null && baseType.SpecialType != SpecialType.System_Object && baseType.SpecialType != SpecialType.System_ValueType) + { + _ = baseTypes.Add(baseType.ToDisplayString(_shortSymbolNameFormat)); + } + + foreach (var @base in typeDef.BaseTypes) + { + if (!baseTypes.Contains(@base)) + { + if (MissingBaseTypes.TryGetValue(type, out var collection)) + { + collection.Add(@base); + } + else + { + MissingBaseTypes.Add(type, new List { @base }); + } + } + } + + var constraints = new HashSet(Utils.GetConstraints(typeSignature)); + foreach (var constraint in typeDef.Constraints) + { + if (!constraints.Contains(constraint)) + { + if (MissingConstraints.TryGetValue(type, out var collection)) + { + collection.Add(constraint); + } + else + { + MissingConstraints.Add(type, new List { constraint }); + } + } + } + + _ = MissingTypes.Remove(typeDef); + _ = FoundInBaseline.Add((type, typeDef.Stage)); + + if (type.TypeKind != TypeKind.Delegate) + { + var members = type + .GetMembers() + .Where(member => member.IsExternallyVisible()) + .Where(member => + { + if (member.Kind != SymbolKind.Method) + { + return true; + } + + var method = (IMethodSymbol)member; + return method.MethodKind + is not MethodKind.PropertyGet + and not MethodKind.PropertySet + and not MethodKind.EventAdd + and not MethodKind.EventRemove; + }); + + foreach (var member in members) + { + if (member is IMethodSymbol methodSymbol) + { + var methodSignature = member.ToDisplayString(_formatNoVariance) + ";"; + + var method = typeDef.Methods.FirstOrDefault(x => x.Member == methodSignature); + if (method != null) + { + _ = MissingMethods.Remove(method); + _ = FoundInBaseline.Add((member, method.Stage)); + } + else + { + _ = NotFoundInBaseline.Add(member); + } + } + else if (member is IPropertySymbol) + { + var propSignature = member.ToDisplayString(_formatNoVariance); + + var prop = typeDef.Properties.FirstOrDefault(x => x.Member == propSignature); + if (prop != null) + { + _ = MissingProperties.Remove(prop); + _ = FoundInBaseline.Add((member, prop.Stage)); + } + else + { + _ = NotFoundInBaseline.Add(member); + } + } + else if (member is IFieldSymbol fieldSym) + { + var fieldSignature = member.ToDisplayString(_formatNoVariance); + + var t = fieldSym.GetFieldOrPropertyType() as INamedTypeSymbol; + if (t?.EnumUnderlyingType != null) + { + // enum value definitions need special attention + fieldSignature = "const " + t.ToDisplayString(_enumType) + " " + fieldSignature; + } + + var field = typeDef.Fields.FirstOrDefault(x => x.Member == fieldSignature); + if (field != null) + { + _ = MissingFields.Remove(field); + _ = FoundInBaseline.Add((member, field.Stage)); + } + else + { + _ = NotFoundInBaseline.Add(member); + } + } + } + } + } + + private static string PrependModifiers(string typeSignature, INamedTypeSymbol type) + { + if (type.EnumUnderlyingType != null) + { + return typeSignature; + } + + if (typeSignature.StartsWith("class", StringComparison.Ordinal) + || typeSignature.StartsWith("record", StringComparison.Ordinal)) + { + var modifiers = new List(6); + + if (type.IsStatic) + { + modifiers.Add("static"); + } + + if (type.IsSealed) + { + modifiers.Add("sealed"); + } + + if (type.IsAbstract) + { + modifiers.Add("abstract"); + } + + if (type.IsVirtual) + { + modifiers.Add("virtual"); + } + + if (type.IsOverride) + { + modifiers.Add("override"); + } + + typeSignature = string.Join(" ", modifiers) + " " + typeSignature; + } + + return Utils.StripBaseAndConstraints(typeSignature); + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonArray.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonArray.cs new file mode 100644 index 0000000000..76e210c843 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonArray.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Forked from StyleCop.Analyzers repo. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.LocalAnalyzers.Json; + +/// +/// Represents an ordered collection of JsonValues. +/// +[DebuggerDisplay("Count = {Count}")] +[DebuggerTypeProxy(typeof(JsonArrayDebugView))] +internal sealed class JsonArray : IEnumerable +{ + private readonly List _items = new(); + + /// + /// Initializes a new instance of the class, adding the given values to the collection. + /// + /// The values to be added to this collection. + public JsonArray(params JsonValue[] values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + _items.AddRange(values); + } + + /// + /// Gets the number of values in this collection. + /// + /// The number of values in this collection. + public int Count => _items.Count; + + /// + /// Gets or sets the value at the given index. + /// + /// The zero-based index of the value to get or set. + /// + /// The getter will return JsonValue.Null if the given index is out of range. + /// + public JsonValue this[int index] + { + get => index >= 0 && index < _items.Count + ? _items[index] + : JsonValue.Null; + set => _items[index] = value; + } + + /// + /// Adds the given value to this collection. + /// + /// The value to be added. + /// Returns this collection. + public JsonArray Add(JsonValue value) + { + _items.Add(value); + return this; + } + + /// + /// Inserts the given value at the given index in this collection. + /// + /// The index where the given value will be inserted. + /// The value to be inserted into this collection. + /// Returns this collection. + public JsonArray Insert(int index, JsonValue value) + { + _items.Insert(index, value); + return this; + } + + /// + /// Removes the value at the given index. + /// + /// The index of the value to be removed. + /// Return this collection. + public JsonArray Remove(int index) + { + _items.RemoveAt(index); + return this; + } + + /// + /// Clears the contents of this collection. + /// + /// Returns this collection. + public JsonArray Clear() + { + _items.Clear(); + return this; + } + + /// + /// Determines whether the given item is in the JsonArray. + /// + /// The item to locate in the JsonArray. + /// Returns true if the item is found; otherwise, false. + public bool Contains(JsonValue item) => _items.Contains(item); + + /// + /// Determines the index of the given item in this JsonArray. + /// + /// The item to locate in this JsonArray. + /// The index of the item, if found. Otherwise, returns -1. + public int IndexOf(JsonValue item) => _items.IndexOf(item); + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// The enumerator that iterates through the collection. + public IEnumerator GetEnumerator() => _items.GetEnumerator(); + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// The enumerator that iterates through the collection. + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + + [ExcludeFromCodeCoverage] + private sealed class JsonArrayDebugView + { + [SuppressMessage("Major Code Smell", "S1144:Unused private types or members should be removed", Justification = "Used by debugger.")] + public JsonArrayDebugView(JsonArray array) + { + Items = array._items.ToArray(); + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public JsonValue[] Items { get; } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonObject.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonObject.cs new file mode 100644 index 0000000000..d45e6c4302 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonObject.cs @@ -0,0 +1,212 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Forked from StyleCop.Analyzers repo. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.LocalAnalyzers.Json; + +/// +/// Represents a key-value pair collection of JsonValue objects. +/// +[DebuggerDisplay("Count = {Count}")] +[DebuggerTypeProxy(typeof(JsonObjectDebugView))] +internal sealed class JsonObject : IEnumerable>, IEnumerable +{ + private readonly IDictionary _properties; + + /// + /// Initializes a new instance of the class. + /// + public JsonObject() + { + _properties = new Dictionary(); + } + + /// + /// Gets the number of properties in this JsonObject. + /// + /// The number of properties in this JsonObject. + public int Count => _properties.Count; + + /// + /// Gets or sets the property with the given key. + /// + /// The key of the property to get or set. + /// + /// The getter will return JsonValue.Null if the given key is not associated with any value. + /// + public JsonValue this[string key] + { + get => _properties.TryGetValue(key, out var value) + ? value + : JsonValue.Null; + set => _properties[key] = value; + } + + /// + /// Adds a key with a null value to this collection. + /// + /// The key of the property to be added. + /// Returns this JsonObject. + /// The that was added. + public JsonObject Add(string key) => Add(key, JsonValue.Null); + + /// + /// Adds a value associated with a key to this collection. + /// + /// The key of the property to be added. + /// The value of the property to be added. + /// Returns this JsonObject. + public JsonObject Add(string key, JsonValue value) + { + _properties.Add(key, value); + return this; + } + + /// + /// Removes a property with the given key. + /// + /// The key of the property to be removed. + /// + /// Returns true if the given key is found and removed; otherwise, false. + /// + public bool Remove(string key) => _properties.Remove(key); + + /// + /// Clears the contents of this collection. + /// + /// Returns this JsonObject. + public JsonObject Clear() + { + _properties.Clear(); + return this; + } + + /// + /// Changes the key of one of the items in the collection. + /// + /// + /// This method has no effects if the oldKey does not exists. + /// If the newKey already exists, the value will be overwritten. + /// + /// The name of the key to be changed. + /// The new name of the key. + /// Returns this JsonObject. + public JsonObject Rename(string oldKey, string newKey) + { + if (oldKey == newKey) + { + return this; + } + + if (_properties.TryGetValue(oldKey, out var value)) + { + this[newKey] = value; + _ = Remove(oldKey); + } + + return this; + } + + /// + /// Determines whether this collection contains an item associated with the given key. + /// + /// The key to locate in this collection. + /// Returns true if the key is found; otherwise, false. + public bool ContainsKey(string key) => _properties.ContainsKey(key); + + /// + /// Determines whether this collection contains the given JsonValue. + /// + /// The value to locate in this collection. + /// Returns true if the value is found; otherwise, false. + public bool Contains(JsonValue value) => _properties.Values.Contains(value); + + /// + /// Returns an enumerator that iterates through this collection. + /// + /// The enumerator that iterates through this collection. + public IEnumerator> GetEnumerator() => _properties.GetEnumerator(); + + /// + /// Returns an enumerator that iterates through this collection. + /// + /// The enumerator that iterates through this collection. + IEnumerator IEnumerable.GetEnumerator() => _properties.Values.GetEnumerator(); + + /// + /// Returns an enumerator that iterates through this collection. + /// + /// The enumerator that iterates through this collection. + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + + [ExcludeFromCodeCoverage] + private sealed class JsonObjectDebugView + { + private readonly JsonObject _object; + + [SuppressMessage("Major Code Smell", "S1144:Unused private types or members should be removed", Justification = "Used by debugger.")] + public JsonObjectDebugView(JsonObject jsonObject) + { + _object = jsonObject; + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public KeyValuePair[] Keys + { + get + { + var keys = new KeyValuePair[_object.Count]; + + var i = 0; + foreach (var property in _object) + { + keys[i] = new KeyValuePair(property.Key, property.Value); + i += 1; + } + + return keys; + } + } + + [DebuggerDisplay("{value.ToString(),nq}", Name = "{key}", Type = "JsonValue({Type})")] + public sealed class KeyValuePair + { + [DebuggerBrowsable(DebuggerBrowsableState.Never)] +#pragma warning disable IDE0052 // Remove unread private members + private readonly string _key; +#pragma warning restore IDE0052 // Remove unread private members + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly JsonValue _value; + + public KeyValuePair(string key, JsonValue value) + { + _key = key; + _value = value; + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public object View + { + get + { + if (_value.IsJsonObject) + { + return (JsonObject)_value!; + } + else if (_value.IsJsonArray) + { + return (JsonArray)_value!; + } + + return _value; + } + } + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonObjectExtensions.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonObjectExtensions.cs new file mode 100644 index 0000000000..27a2e6a549 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonObjectExtensions.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.LocalAnalyzers.Json; + +namespace Microsoft.Extensions.LocalAnalyzers.Json; + +internal static class JsonObjectExtensions +{ + public static T[] GetValueArray(this JsonObject value, string name) + { + var arrayOfTypes = value[name].AsJsonArray; + + if (arrayOfTypes == null) + { + return Array.Empty(); + } + + var types = new T[arrayOfTypes.Count]; + + for (var i = 0; i < arrayOfTypes.Count; i++) + { + types[i] = (T)Activator.CreateInstance(typeof(T), arrayOfTypes[i].AsJsonObject); + } + + return types; + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonParseException.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonParseException.cs new file mode 100644 index 0000000000..9c9d6abaaf --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonParseException.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Forked from StyleCop.Analyzers repo. + +using System; + +namespace Microsoft.Extensions.LocalAnalyzers.Json; + +/// +/// The exception that is thrown when a JSON message cannot be parsed. +/// +/// +/// This exception is only intended to be thrown by LightJson. +/// +#pragma warning disable CA1032 // Implement standard exception constructors +public sealed class JsonParseException : Exception +#pragma warning restore CA1032 // Implement standard exception constructors +{ + /// + /// Gets the text position where the error occurred. + /// + /// The text position where the error occurred. + public TextPosition Position { get; } + + /// + /// Gets the type of error that caused the exception to be thrown. + /// + /// The type of error that caused the exception to be thrown. + public ParsingError Error { get; } + + /// + /// Initializes a new instance of the class. + /// + public JsonParseException() + : base(GetMessage(ParsingError.Unknown)) + { + } + + /// + /// Initializes a new instance of the class with the given error type and position. + /// + /// The error type that describes the cause of the error. + /// The position in the text where the error occurred. + public JsonParseException(ParsingError type, TextPosition position) + : this(GetMessage(type), type, position) + { + } + + /// + /// Initializes a new instance of the class with the given message, error type, and position. + /// + /// The message that describes the error. + /// The error type that describes the cause of the error. + /// The position in the text where the error occurred. + public JsonParseException(string message, ParsingError error, TextPosition position) + : base(message) + { + Error = error; + Position = position; + } + + private static string GetMessage(ParsingError type) + { + return type switch + { + ParsingError.IncompleteMessage => "The string ended before a value could be parsed.", + ParsingError.InvalidOrUnexpectedCharacter => "The parser encountered an invalid or unexpected character.", + ParsingError.DuplicateObjectKeys => "The parser encountered a JsonObject with duplicate keys.", + _ => "An error occurred while parsing the JSON message." + }; + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonReader.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonReader.cs new file mode 100644 index 0000000000..7d06df7b37 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonReader.cs @@ -0,0 +1,389 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Forked from StyleCop.Analyzers repo. + +using System; +using System.Globalization; +using System.IO; +using System.Text; + +namespace Microsoft.Extensions.LocalAnalyzers.Json; + +/// +/// Represents a reader that can read JsonValues. +/// +internal sealed class JsonReader +{ + private readonly TextScanner _scanner; + + private JsonReader(TextReader reader) + { + _scanner = new TextScanner(reader); + } + + /// + /// Creates a JsonValue by using the given TextReader. + /// + /// The TextReader used to read a JSON message. + /// The parsed . + public static JsonValue Parse(TextReader reader) + { + if (reader == null) + { + throw new ArgumentNullException(nameof(reader)); + } + + return new JsonReader(reader).Parse(); + } + + /// + /// Creates a JsonValue by reader the JSON message in the given string. + /// + /// The string containing the JSON message. + /// The parsed . + public static JsonValue Parse(string source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + using var reader = new StringReader(source); + + return Parse(reader); + } + + private string ReadJsonKey() + { + return ReadString(); + } + + private JsonValue ReadJsonValue() + { + _scanner.SkipWhitespace(); + + var next = _scanner.Peek(); + + if (char.IsNumber(next)) + { + return ReadNumber(); + } + + return next switch + { + '{' => ReadObject(), + '[' => ReadArray(), + '"' => ReadString(), + '-' => ReadNumber(), + 't' or 'f' => ReadBoolean(), + 'n' => ReadNull(), + _ => throw new JsonParseException( + ParsingError.InvalidOrUnexpectedCharacter, + _scanner.Position), + }; + } + + private JsonValue ReadNull() + { + _scanner.Assert("null"); + return JsonValue.Null; + } + + private JsonValue ReadBoolean() + { + switch (_scanner.Peek()) + { + case 't': + _scanner.Assert("true"); + return true; + + default: + _scanner.Assert("false"); + return false; + } + } + + private void ReadDigits(StringBuilder builder) + { + while (true) + { + int next = _scanner.Peek(throwAtEndOfFile: false); + if (next == -1 || !char.IsNumber((char)next)) + { + return; + } + + _ = builder.Append(_scanner.Read()); + } + } + + private JsonValue ReadNumber() + { + var builder = new StringBuilder(); + + if (_scanner.Peek() == '-') + { + _ = builder.Append(_scanner.Read()); + } + + if (_scanner.Peek() == '0') + { + _ = builder.Append(_scanner.Read()); + } + else + { + ReadDigits(builder); + } + + if (_scanner.Peek(throwAtEndOfFile: false) == '.') + { + _ = builder.Append(_scanner.Read()); + + ReadDigits(builder); + } + + if (_scanner.Peek(throwAtEndOfFile: false) == 'e' || _scanner.Peek(throwAtEndOfFile: false) == 'E') + { + _ = builder.Append(_scanner.Read()); + + var next = _scanner.Peek(); + + switch (next) + { + case '+': + case '-': + _ = builder.Append(_scanner.Read()); + break; + } + + ReadDigits(builder); + } + + return double.Parse( + builder.ToString(), + CultureInfo.InvariantCulture); + } + + private string ReadString() + { + var builder = new StringBuilder(); + + _scanner.Assert('"'); + + while (true) + { + var errorPosition = _scanner.Position; + var c = _scanner.Read(); + + if (c == '\\') + { + errorPosition = _scanner.Position; + c = _scanner.Read(); + + _ = char.ToLowerInvariant(c) switch + { + '"' or '\\' or '/' => builder.Append(c), + 'b' => builder.Append('\b'), + 'f' => builder.Append('\f'), + 'n' => builder.Append('\n'), + 'r' => builder.Append('\r'), + 't' => builder.Append('\t'), + 'u' => builder.Append(ReadUnicodeLiteral()), + _ => throw new JsonParseException( + ParsingError.InvalidOrUnexpectedCharacter, + errorPosition), + }; + } + else if (c == '"') + { + break; + } + else + { + if (char.IsControl(c)) + { + throw new JsonParseException( + ParsingError.InvalidOrUnexpectedCharacter, + errorPosition); + } + + _ = builder.Append(c); + } + } + + return builder.ToString(); + } + + private int ReadHexDigit() + { + var errorPosition = _scanner.Position; +#pragma warning disable S109 // Magic numbers should not be used + return char.ToUpperInvariant(_scanner.Read()) switch + { + '0' => 0, + '1' => 1, + '2' => 2, + '3' => 3, + '4' => 4, + '5' => 5, + '6' => 6, + '7' => 7, + '8' => 8, + '9' => 9, + 'A' => 10, + 'B' => 11, + 'C' => 12, + 'D' => 13, + 'E' => 14, + 'F' => 15, + _ => throw new JsonParseException( + ParsingError.InvalidOrUnexpectedCharacter, + errorPosition), + }; + } + + private char ReadUnicodeLiteral() + { + int value = 0; + + value += ReadHexDigit() * 4096; // 16^3 + value += ReadHexDigit() * 256; // 16^2 + value += ReadHexDigit() * 16; // 16^1 + value += ReadHexDigit(); // 16^0 + + return (char)value; + } +#pragma warning restore S109 // Magic numbers should not be used + private JsonObject ReadObject() + { + return ReadObject(new JsonObject()); + } + + private JsonObject ReadObject(JsonObject jsonObject) + { + _scanner.Assert('{'); + + _scanner.SkipWhitespace(); + + if (_scanner.Peek() == '}') + { + _ = _scanner.Read(); + } + else + { + while (true) + { + _scanner.SkipWhitespace(); + + var errorPosition = _scanner.Position; + var key = ReadJsonKey(); + + if (jsonObject.ContainsKey(key)) + { + throw new JsonParseException( + ParsingError.DuplicateObjectKeys, + errorPosition); + } + + _scanner.SkipWhitespace(); + + _scanner.Assert(':'); + + _scanner.SkipWhitespace(); + + var value = ReadJsonValue(); + + _ = jsonObject.Add(key, value); + + _scanner.SkipWhitespace(); + + errorPosition = _scanner.Position; + var next = _scanner.Read(); + if (next == ',') + { + // Allow trailing commas in objects + _scanner.SkipWhitespace(); + if (_scanner.Peek() == '}') + { + next = _scanner.Read(); + } + } + + if (next == '}') + { + break; + } + else if (next != ',') + { + throw new JsonParseException( + ParsingError.InvalidOrUnexpectedCharacter, + errorPosition); + } + } + } + + return jsonObject; + } + + private JsonArray ReadArray() + { + return ReadArray(new JsonArray()); + } + + private JsonArray ReadArray(JsonArray jsonArray) + { + _scanner.Assert('['); + + _scanner.SkipWhitespace(); + + if (_scanner.Peek() == ']') + { + _ = _scanner.Read(); + } + else + { + while (true) + { + _scanner.SkipWhitespace(); + + var value = ReadJsonValue(); + + _ = jsonArray.Add(value); + + _scanner.SkipWhitespace(); + + var errorPosition = _scanner.Position; + var next = _scanner.Read(); + if (next == ',') + { + // Allow trailing commas in arrays + _scanner.SkipWhitespace(); + if (_scanner.Peek() == ']') + { + next = _scanner.Read(); + } + } + + if (next == ']') + { + break; + } + else if (next != ',') + { + throw new JsonParseException( + ParsingError.InvalidOrUnexpectedCharacter, + errorPosition); + } + } + } + + return jsonArray; + } + + private JsonValue Parse() + { + _scanner.SkipWhitespace(); + return ReadJsonValue(); + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonValue.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonValue.cs new file mode 100644 index 0000000000..20ee492e5f --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonValue.cs @@ -0,0 +1,622 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Forked from StyleCop.Analyzers repo. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace Microsoft.Extensions.LocalAnalyzers.Json; + +/// +/// A wrapper object that contains a valid JSON value. +/// +[DebuggerDisplay("{ToString(),nq}", Type = "JsonValue({Type})")] +[DebuggerTypeProxy(typeof(JsonValueDebugView))] +[SuppressMessage("Major Bug", "S1244:Floating point numbers should not be tested for equality", + Justification = "Would require unnecessary refactor.")] +internal readonly struct JsonValue : IEquatable +{ + /// + /// Represents a null JsonValue. + /// + public static readonly JsonValue Null = new(JsonValueType.Null, default, null); + private readonly object? _reference; + private readonly double _value; + + /// + /// Initializes a new instance of the struct, representing a Boolean value. + /// + /// The value to be wrapped. + public JsonValue(bool? value) + { + if (!value.HasValue) + { + this = Null; + return; + } + + Type = JsonValueType.Boolean; + _reference = null; + _value = value.Value ? 1 : 0; + } + + /// + /// Initializes a new instance of the struct, representing a Number value. + /// + /// The value to be wrapped. + public JsonValue(double? value) + { + if (!value.HasValue) + { + this = Null; + return; + } + + Type = JsonValueType.Number; + _reference = null; + _value = value.Value; + } + + /// + /// Initializes a new instance of the struct, representing a String value. + /// + /// The value to be wrapped. + public JsonValue(string? value) + { + if (value is null) + { + this = Null; + return; + } + + Type = JsonValueType.String; + _value = default; + _reference = value; + } + + /// + /// Initializes a new instance of the struct, representing a JsonObject. + /// + /// The value to be wrapped. + public JsonValue(JsonObject? value) + { + if (value is null) + { + this = Null; + return; + } + + Type = JsonValueType.Object; + _value = default; + _reference = value; + } + + /// + /// Initializes a new instance of the struct, representing a Array reference value. + /// + /// The value to be wrapped. + public JsonValue(JsonArray? value) + { + if (value is null) + { + this = Null; + return; + } + + Type = JsonValueType.Array; + _value = default; + _reference = value; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The Json type of the JsonValue. + /// + /// The internal value of the JsonValue. + /// This is used when the Json type is Number or Boolean. + /// + /// + /// The internal value reference of the JsonValue. + /// This value is used when the Json type is String, JsonObject, or JsonArray. + /// + private JsonValue(JsonValueType type, double value, object? reference) + { + Type = type; + _value = value; + _reference = reference; + } + + /// + /// Gets the type of this JsonValue. + /// + /// The type of this JsonValue. + public JsonValueType Type { get; } + + /// + /// Gets a value indicating whether this JsonValue is Null. + /// + /// A value indicating whether this JsonValue is Null. + public bool IsNull => Type == JsonValueType.Null; + + /// + /// Gets a value indicating whether this JsonValue is a Boolean. + /// + /// A value indicating whether this JsonValue is a Boolean. + public bool IsBoolean => Type == JsonValueType.Boolean; + + /// + /// Gets a value indicating whether this JsonValue is an Integer. + /// + /// A value indicating whether this JsonValue is an Integer. +#pragma warning disable S2589 // Boolean expressions should not be gratuitous + public bool IsInteger => IsNumber && unchecked((int)_value) == _value; +#pragma warning restore S2589 // Boolean expressions should not be gratuitous + + /// + /// Gets a value indicating whether this JsonValue is a Number. + /// + /// A value indicating whether this JsonValue is a Number. + public bool IsNumber => Type == JsonValueType.Number; + + /// + /// Gets a value indicating whether this JsonValue is a String. + /// + /// A value indicating whether this JsonValue is a String. + public bool IsString => Type == JsonValueType.String; + + /// + /// Gets a value indicating whether this JsonValue is a JsonObject. + /// + /// A value indicating whether this JsonValue is a JsonObject. + public bool IsJsonObject => Type == JsonValueType.Object; + + /// + /// Gets a value indicating whether this JsonValue is a JsonArray. + /// + /// A value indicating whether this JsonValue is a JsonArray. + public bool IsJsonArray => Type == JsonValueType.Array; + + /// + /// Gets a value indicating whether this JsonValue represents a DateTime. + /// + /// A value indicating whether this JsonValue represents a DateTime. + public bool IsDateTime => AsDateTime != null; + + /// + /// Gets a value indicating whether this value is true or false. + /// + /// This value as a Boolean type. + public bool AsBoolean => Type switch + { + JsonValueType.Boolean => _value == 1, + JsonValueType.Number => _value != 0, + JsonValueType.String => !string.IsNullOrEmpty((string?)_reference), + JsonValueType.Object or JsonValueType.Array => true, + _ => false, + }; + + /// + /// Gets this value as an Integer type. + /// + /// This value as an Integer type. + public int AsInteger + { + get + { + var current = AsNumber; + + if (current >= int.MaxValue) + { + return int.MaxValue; + } + + if (_value <= int.MinValue) + { + return int.MinValue; + } + + return (int)_value; + } + } + + /// + /// Gets this value as a Number type. + /// + /// This value as a Number type. + public double AsNumber => Type switch + { + JsonValueType.Boolean => _value == 1 ? 1 : 0, + JsonValueType.Number => _value, + JsonValueType.String => double.TryParse((string?)_reference, NumberStyles.Float, CultureInfo.InvariantCulture, out var number) + ? number + : 0, + _ => 0 + }; + + /// + /// Gets this value as a String type. + /// + /// This value as a String type. + public string? AsString => Type switch + { + JsonValueType.Boolean => (_value == 1) + ? "true" + : "false", + JsonValueType.Number => _value.ToString(CultureInfo.InvariantCulture), + JsonValueType.String => (string?)_reference, + _ => null, + }; + + /// + /// Gets this value as an JsonObject. + /// + /// This value as an JsonObject. +#pragma warning disable S1168 // Empty arrays and collections should be returned instead of null + public JsonObject? AsJsonObject => IsJsonObject ? (JsonObject?)_reference : null; + + /// + /// Gets this value as an JsonArray. + /// + /// This value as an JsonArray. + public JsonArray? AsJsonArray => IsJsonArray ? (JsonArray?)_reference : null; +#pragma warning restore S1168 // Empty arrays and collections should be returned instead of null + + /// + /// Gets this value as a system.DateTime. + /// + /// This value as a system.DateTime. + public DateTime? AsDateTime => IsString && DateTime.TryParse((string?)_reference ?? string.Empty, out var value) + ? value + : null; + + /// + /// Gets this (inner) value as a System.object. + /// + /// This (inner) value as a System.object. + public object? AsObject => Type switch + { + JsonValueType.Boolean or JsonValueType.Number => _value, + JsonValueType.String or JsonValueType.Object or JsonValueType.Array => _reference, + _ => null + }; + + /// + /// Gets or sets the value associated with the specified key. + /// + /// The key of the value to get or set. + /// + /// Thrown when this JsonValue is not a JsonObject. + /// + public JsonValue this[string key] + { + get + { + if (IsJsonObject) + { + return ((JsonObject)_reference!)[key]; + } + else + { + throw new InvalidOperationException("This value does not represent a JsonObject."); + } + } + + set + { + if (IsJsonObject) + { + ((JsonObject)_reference!)[key] = value; + } + else + { + throw new InvalidOperationException("This value does not represent a JsonObject."); + } + } + } + + /// + /// Gets or sets the value at the specified index. + /// + /// The zero-based index of the value to get or set. + /// + /// Thrown when this is not a . + /// + public JsonValue this[int index] + { + get + { + if (IsJsonArray) + { + return ((JsonArray)_reference!)[index]; + } + else + { + throw new InvalidOperationException("This value does not represent a JsonArray."); + } + } + + set + { + if (IsJsonArray) + { + ((JsonArray)_reference!)[index] = value; + } + else + { + throw new InvalidOperationException("This value does not represent a JsonArray."); + } + } + } + + /// + /// Converts the given nullable boolean into a JsonValue. + /// + /// The value to be converted. + public static implicit operator JsonValue(bool? value) + { + return new JsonValue(value); + } + + /// + /// Converts the given nullable double into a JsonValue. + /// + /// The value to be converted. + public static implicit operator JsonValue(double? value) + { + return new JsonValue(value); + } + + /// + /// Converts the given string into a JsonValue. + /// + /// The value to be converted. + public static implicit operator JsonValue(string value) + { + return new JsonValue(value); + } + + /// + /// Converts the given JsonObject into a JsonValue. + /// + /// The value to be converted. + public static implicit operator JsonValue(JsonObject value) + { + return new JsonValue(value); + } + + /// + /// Converts the given JsonArray into a JsonValue. + /// + /// The value to be converted. + public static implicit operator JsonValue(JsonArray value) + { + return new JsonValue(value); + } + + /// + /// Converts the given DateTime? into a JsonValue. + /// + /// + /// The DateTime value will be stored as a string using ISO 8601 format, + /// since JSON does not define a DateTime type. + /// + /// The value to be converted. + public static implicit operator JsonValue(DateTime? value) + { + return value == null + ? Null + : new JsonValue(value.Value.ToString("o", CultureInfo.InvariantCulture)); + } + + /// + /// Converts the given JsonValue into an Int. + /// + /// The JsonValue to be converted. + public static explicit operator int(JsonValue jsonValue) + { + return jsonValue.IsInteger ? jsonValue.AsInteger : 0; + } + + /// + /// Converts the given JsonValue into a nullable Int. + /// + /// The JsonValue to be converted. + /// + /// Throws System.InvalidCastException when the inner value type of the + /// JsonValue is not the desired type of the conversion. + /// + public static explicit operator int?(JsonValue jsonValue) + { + return jsonValue.IsNull ? null : (int)jsonValue; + } + + /// + /// Converts the given JsonValue into a Bool. + /// + /// The JsonValue to be converted. + public static explicit operator bool(JsonValue jsonValue) + { + return jsonValue.IsBoolean && jsonValue._value == 1; + } + + /// + /// Converts the given JsonValue into a nullable Bool. + /// + /// The JsonValue to be converted. + /// + /// Throws System.InvalidCastException when the inner value type of the + /// JsonValue is not the desired type of the conversion. + /// + public static explicit operator bool?(JsonValue jsonValue) + { + return jsonValue.IsNull ? null : (bool)jsonValue; + } + + /// + /// Converts the given JsonValue into a Double. + /// + /// The JsonValue to be converted. + public static explicit operator double(JsonValue jsonValue) + { + return jsonValue.IsNumber ? jsonValue._value : double.NaN; + } + + /// + /// Converts the given JsonValue into a nullable Double. + /// + /// The JsonValue to be converted. + /// + /// Throws System.InvalidCastException when the inner value type of the + /// JsonValue is not the desired type of the conversion. + /// + public static explicit operator double?(JsonValue jsonValue) + { + return jsonValue.IsNull + ? null + : (double)jsonValue; + } + + /// + /// Converts the given JsonValue into a String. + /// + /// The JsonValue to be converted. + public static explicit operator string?(JsonValue jsonValue) + { + return jsonValue.IsString || jsonValue.IsNull + ? jsonValue._reference as string + : null; + } + + /// + /// Converts the given JsonValue into a JsonObject. + /// + /// The JsonValue to be converted. + public static explicit operator JsonObject?(JsonValue jsonValue) + { + return jsonValue.IsJsonObject || jsonValue.IsNull ? jsonValue._reference as JsonObject : null; + } + + /// + /// Converts the given JsonValue into a JsonArray. + /// + /// The JsonValue to be converted. + public static explicit operator JsonArray?(JsonValue jsonValue) + { + return jsonValue.IsJsonArray || jsonValue.IsNull ? jsonValue._reference as JsonArray : null; + } + + /// + /// Converts the given JsonValue into a DateTime. + /// + /// The JsonValue to be converted. + public static explicit operator DateTime(JsonValue jsonValue) + { + return jsonValue.AsDateTime ?? DateTime.MinValue; + } + + /// + /// Converts the given JsonValue into a nullable DateTime. + /// + /// The JsonValue to be converted. + public static explicit operator DateTime?(JsonValue jsonValue) + { + return jsonValue.IsDateTime || jsonValue.IsNull + ? jsonValue.AsDateTime + : null; + } + + /// + /// Returns a value indicating whether the two given JsonValues are equal. + /// + /// First JsonValue to compare. + /// Second JsonValue to compare. + public static bool operator ==(JsonValue a, JsonValue b) + { + return (a.Type == b.Type) + && (a._value == b._value) + && Equals(a._reference, b._reference); + } + + /// + /// Returns a value indicating whether the two given JsonValues are unequal. + /// + /// First JsonValue to compare. + /// Second JsonValue to compare. + public static bool operator !=(JsonValue a, JsonValue b) + { + return !(a == b); + } + + /// + /// Returns a JsonValue by parsing the given string. + /// + /// The JSON-formatted string to be parsed. + /// The representing the parsed text. + public static JsonValue Parse(string text) + { + return JsonReader.Parse(text); + } + + public bool Equals(JsonValue other) + { + return other == this; + } + + /// + public override bool Equals(object? obj) + { + if (obj is JsonValue jv) + { + return this == jv; + } + + return IsNull && obj is null; + } + + /// + public override int GetHashCode() + { + var r = _reference != null ? EqualityComparer.Default.GetHashCode(_reference) : 1; + + return IsNull + ? Type.GetHashCode() + : Type.GetHashCode() + ^ _value.GetHashCode() + ^ r; + } + + [ExcludeFromCodeCoverage] + private sealed class JsonValueDebugView + { + private readonly JsonValue _jsonValue; + + [SuppressMessage("Major Code Smell", "S1144:Unused private types or members should be removed", Justification = "Used by debugger.")] + public JsonValueDebugView(JsonValue jsonValue) + { + _jsonValue = jsonValue; + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] +#pragma warning disable S1168 // Empty arrays and collections should be returned instead of null + public JsonObject? ObjectView => _jsonValue.IsJsonObject + ? (JsonObject?)_jsonValue._reference + : null; + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public JsonArray? ArrayView => _jsonValue.IsJsonArray + ? (JsonArray?)_jsonValue._reference + : null; +#pragma warning restore S1168 // Empty arrays and collections should be returned instead of null + + public JsonValueType Type => _jsonValue.Type; + + public object? Value => _jsonValue.IsJsonObject || _jsonValue.IsJsonArray + ? _jsonValue._reference + : null; + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonValueType.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonValueType.cs new file mode 100644 index 0000000000..6854c2d759 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/JsonValueType.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Forked from StyleCop.Analyzers repo. + +namespace Microsoft.Extensions.LocalAnalyzers.Json; + +/// +/// Enumerates the types of Json values. +/// +internal enum JsonValueType +{ + /// + /// A null value. + /// + Null = 0, + + /// + /// A boolean value. + /// + Boolean, + + /// + /// A number value. + /// + Number, + + /// + /// A string value. + /// + String, + + /// + /// An object value. + /// + Object, + + /// + /// An array value. + /// + Array, +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/ParsingError.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/ParsingError.cs new file mode 100644 index 0000000000..67f9aa2d28 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/ParsingError.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Forked from StyleCop.Analyzers repo. + +namespace Microsoft.Extensions.LocalAnalyzers.Json; + +/// +/// Enumerates the types of errors that can occur when parsing a JSON message. +/// +public enum ParsingError +{ + /// + /// Indicates that the cause of the error is unknown. + /// + Unknown = 0, + + /// + /// Indicates that the text ended before the message could be parsed. + /// + IncompleteMessage, + + /// + /// Indicates that a JsonObject contains more than one key with the same name. + /// + DuplicateObjectKeys, + + /// + /// Indicates that the parser encountered and invalid or unexpected character. + /// + InvalidOrUnexpectedCharacter, +} + diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/TextPosition.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/TextPosition.cs new file mode 100644 index 0000000000..a2e5e67204 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/TextPosition.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Forked from StyleCop.Analyzers repo. + +namespace Microsoft.Extensions.LocalAnalyzers.Json; + +/// +/// Represents a position within a plain text resource. +/// +#pragma warning disable CA1815 // Override equals and operator equals on value types +public readonly struct TextPosition +#pragma warning restore CA1815 // Override equals and operator equals on value types +{ + /// + /// Gets the column position, 0-based. + /// + public long Column { get; } + + /// + /// Gets the line position, 0-based. + /// + public long Line { get; } + + public TextPosition(long column, long line) + { + Column = column; + Line = line; + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/TextScanner.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/TextScanner.cs new file mode 100644 index 0000000000..5fa5b946d2 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Json/TextScanner.cs @@ -0,0 +1,217 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Forked from StyleCop.Analyzers repo. + +using System.Globalization; +using System.IO; + +#pragma warning disable R9A018 + +namespace Microsoft.Extensions.LocalAnalyzers.Json; + +/// +/// Represents a text scanner that reads one character at a time. +/// +internal sealed class TextScanner +{ + private readonly TextReader _reader; + + /// + /// Initializes a new instance of the class. + /// + /// The TextReader to read the text. + public TextScanner(TextReader reader) + { + _reader = reader; + } + + /// + /// Gets the position of the scanner within the text. + /// + /// The position of the scanner within the text. + public TextPosition Position { get; private set; } + + /// + /// Reads the next character in the stream without changing the current position. + /// + /// The next character in the stream. + public char Peek() => (char)Peek(throwAtEndOfFile: true); + + /// + /// Reads the next character in the stream without changing the current position. + /// + /// to throw an exception if the end of the file is + /// reached; otherwise, . + /// The next character in the stream, or -1 if the end of the file is reached with + /// set to . + public int Peek(bool throwAtEndOfFile) + { + var next = _reader.Peek(); + + if (next == -1 && throwAtEndOfFile) + { + throw new JsonParseException(ParsingError.IncompleteMessage, Position); + } + + return next; + } + + /// + /// Reads the next character in the stream, advancing the text position. + /// + /// The next character in the stream. + public char Read() + { + var next = _reader.Read(); + + if (next == -1) + { + throw new JsonParseException(ParsingError.IncompleteMessage, Position); + } + else + { + Position = next == '\n' + ? new(0, Position.Line + 1) + : new(Position.Column + 1, Position.Line); + + return (char)next; + } + } + + /// + /// Advances the scanner to next non-whitespace character. + /// + public void SkipWhitespace() + { + while (true) + { + char next = Peek(); + + if (char.IsWhiteSpace(next)) + { + _ = Read(); + continue; + } + else if (next == '/') + { + SkipComment(); + continue; + } + + break; + } + } + + /// + /// Verifies that the given character matches the next character in the stream. + /// If the characters do not match, an exception will be thrown. + /// + /// The expected character. + public void Assert(char next) + { + var errorPosition = Position; + + if (Read() != next) + { + throw new JsonParseException( + string.Format(CultureInfo.InvariantCulture, "Parser expected '{0}'", next), + ParsingError.InvalidOrUnexpectedCharacter, + errorPosition); + } + } + + /// + /// Verifies that the given string matches the next characters in the stream. + /// If the strings do not match, an exception will be thrown. + /// + /// The expected string. + public void Assert(string next) + { + for (var i = 0; i < next.Length; i += 1) + { + Assert(next[i]); + } + } + + private void SkipComment() + { + // First character is the first slash + _ = Read(); + + switch (Peek()) + { + case '/': + SkipLineComment(); + return; + + case '*': + SkipBlockComment(); + return; + + default: + throw new JsonParseException( + string.Format(CultureInfo.InvariantCulture, "Parser expected '{0}'", Peek()), + ParsingError.InvalidOrUnexpectedCharacter, + Position); + } + } + + private void SkipLineComment() + { + // First character is the second '/' of the opening '//' + _ = Read(); + + while (true) + { + switch (_reader.Peek()) + { + case '\n': + // Reached the end of the line + _ = Read(); + return; + case -1: + return; + default: + _ = Read(); + break; + } + } + } + + private void SkipBlockComment() + { + // First character is the '*' of the opening '/*' + _ = Read(); + + bool foundStar = false; + + while (true) + { + switch (_reader.Peek()) + { + case '*': + _ = Read(); + foundStar = true; + break; + + case '/': + _ = Read(); + if (foundStar) + { + return; + } + + foundStar = false; + break; + + case -1: + return; + default: + _ = Read(); + foundStar = false; + break; + } + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Assembly.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Assembly.cs new file mode 100644 index 0000000000..bf6428b24a --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Assembly.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.LocalAnalyzers.Json; + +namespace Microsoft.Extensions.LocalAnalyzers.ApiLifecycle.Model; + +internal sealed class Assembly +{ + public static readonly Assembly Empty = new(); + + public string Name { get; } + public TypeDef[] Types { get; } + + public Assembly(JsonObject value) + { + Name = value[nameof(Name)].AsString ?? string.Empty; + Types = value.GetValueArray(nameof(Types)); + } + + private Assembly() + { + Name = string.Empty; + Types = Array.Empty(); + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Field.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Field.cs new file mode 100644 index 0000000000..0f1f7f0d87 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Field.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.LocalAnalyzers.Json; + +namespace Microsoft.Extensions.LocalAnalyzers.ApiLifecycle.Model; + +internal sealed class Field +{ + public Stage Stage { get; } + public string Member { get; } + + public Field(JsonObject value) + { + Member = value[nameof(Member)].AsString ?? string.Empty; + + var enumString = value[nameof(Stage)].AsString; + + _ = Enum.TryParse(enumString, out var stage); + + Stage = stage; + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Method.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Method.cs new file mode 100644 index 0000000000..7d28a01e41 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Method.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.LocalAnalyzers.Json; + +namespace Microsoft.Extensions.LocalAnalyzers.ApiLifecycle.Model; + +internal sealed class Method +{ + public Stage Stage { get; } + public string Member { get; } + + public Method(JsonObject value) + { + Member = value[nameof(Member)].AsString ?? string.Empty; + + var enumString = value[nameof(Stage)].AsString; + + if (Enum.TryParse(enumString, out var stage)) + { + Stage = stage; + } + else + { + Stage = Stage.Experimental; + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Prop.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Prop.cs new file mode 100644 index 0000000000..c148cc8300 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Prop.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.LocalAnalyzers.Json; + +namespace Microsoft.Extensions.LocalAnalyzers.ApiLifecycle.Model; + +internal sealed class Prop +{ + public Stage Stage { get; } + public string Member { get; } + + public Prop(JsonObject value) + { + Member = value[nameof(Member)].AsString ?? string.Empty; + + var stageString = value[nameof(Stage)].AsString; + + _ = Enum.TryParse(stageString, out var stage); + + Stage = stage; + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Stage.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Stage.cs new file mode 100644 index 0000000000..fff031c5f8 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/Stage.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.LocalAnalyzers.ApiLifecycle.Model; + +internal enum Stage +{ + /// + /// Public interface can be changed without notice. + /// + Experimental, + + /// + /// Public interface changes needs to be documented and follow deprecation policy. + /// + Stable, + + /// + /// Public interface is still functional but no longer recommended. + /// + Obsolete +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/TypeDef.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/TypeDef.cs new file mode 100644 index 0000000000..608f98852a --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Model/TypeDef.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.LocalAnalyzers.Json; + +namespace Microsoft.Extensions.LocalAnalyzers.ApiLifecycle.Model; + +internal sealed class TypeDef +{ + public string ModifiersAndName { get; } + public string[] Constraints { get; } + public string[] BaseTypes { get; } + public Stage Stage { get; } + public Method[] Methods { get; } + public Prop[] Properties { get; } + public Field[] Fields { get; } + + public TypeDef(JsonObject value) + { + ModifiersAndName = Utils.StripBaseAndConstraints(value["Type"].AsString ?? string.Empty); + Constraints = Utils.GetConstraints(value["Type"].AsString ?? string.Empty); + BaseTypes = Utils.GetBaseTypes(value["Type"].AsString ?? string.Empty); + _ = Enum.TryParse(value[nameof(Stage)].AsString, out var stage); + + Stage = stage; + Methods = value.GetValueArray(nameof(Methods)); + Properties = value.GetValueArray(nameof(Properties)); + Fields = value.GetValueArray(nameof(Fields)); + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/ModelLoader.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/ModelLoader.cs new file mode 100644 index 0000000000..68d25b04dc --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/ModelLoader.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Extensions.LocalAnalyzers.ApiLifecycle.Model; +using Microsoft.Extensions.LocalAnalyzers.Json; + +namespace Microsoft.Extensions.LocalAnalyzers.ApiLifecycle; + +internal static class ModelLoader +{ +#pragma warning disable RS1012 // Start action has no registered actions + internal static bool TryLoadAssemblyModel(CompilationStartAnalysisContext context, out Assembly? assembly) +#pragma warning restore RS1012 // Start action has no registered actions + { + assembly = null; + + var files = context.Options.AdditionalFiles; + var compilation = context.Compilation; + var assemblyName = compilation.AssemblyName!; + + var assemblyBaselineFile = files.FirstOrDefault(file => + { + var filePath = file.Path; + var fileName = Path.GetFileNameWithoutExtension(filePath); + var fileExtension = Path.GetExtension(filePath); + + if (assemblyName.EndsWith(fileName, StringComparison.OrdinalIgnoreCase) && string.Equals(fileExtension, ".json", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return false; + }); + + if (assemblyBaselineFile == null) + { + return false; + } + + var publicInterface = string.Empty; + + try + { + publicInterface = assemblyBaselineFile.GetText()?.ToString(); + } + catch (FileNotFoundException) + { + return false; + } + + if (string.IsNullOrWhiteSpace(publicInterface)) + { + return false; + } + +#pragma warning disable CA1031 // Do not catch general exception types + try + { + using var reader = new StringReader(publicInterface); + var value = JsonReader.Parse(reader); + + assembly = new Assembly(value.AsJsonObject!); + if (!assembly.Name.Contains(assemblyName)) + { + return false; + } + } + catch (Exception) + { + // failed to deserialize. + return false; + } +#pragma warning restore CA1031 // Do not catch general exception types + + return true; + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Utils.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Utils.cs new file mode 100644 index 0000000000..cfa0c4d586 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/ApiLifecycle/Utils.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; + +#pragma warning disable R9A043 + +namespace Microsoft.Extensions.LocalAnalyzers.ApiLifecycle; + +internal static class Utils +{ + private static readonly char[] _colon = { ':' }; + private static readonly char[] _comma = { ',' }; + + public static string[] GetConstraints(string typeSignature) + { + var whereClauseIndex = typeSignature.IndexOf(" where ", StringComparison.Ordinal); + + if (whereClauseIndex == -1) + { + return Array.Empty(); + } + + var substrings = typeSignature.Split(_colon); + +#pragma warning disable S109 // Magic numbers should not be used + return substrings.Length == 2 + ? substrings[1].Split(_comma).Select(x => x.Trim()).ToArray() + : substrings[2].Split(_comma).Select(x => x.Trim()).ToArray(); +#pragma warning restore S109 // Magic numbers should not be used + } + + public static string StripBaseAndConstraints(string typeSignature) + { + var type = typeSignature.Split(_colon)[0]; + var whereClauseIndex = type.IndexOf(" where ", StringComparison.Ordinal); + + if (whereClauseIndex != -1) + { + return type.Substring(0, whereClauseIndex).Trim(); + } + + return type.Trim(); + } + + public static string[] GetBaseTypes(string typeSignature) + { + var whereClauseIndex = typeSignature.IndexOf(" where ", StringComparison.Ordinal); + var substrings = typeSignature.Split(_colon); + var result = new List(); + + if (whereClauseIndex == -1) + { + if (substrings.Length > 1) + { + GetBaseTypesImpl(result, substrings[1]); + } + } + else + { + if (substrings.Length > 2) + { + var substring = substrings[1].Substring(0, substrings[1].IndexOf(" where ", StringComparison.Ordinal)); + GetBaseTypesImpl(result, substring.Trim()); + } + } + + return result.ToArray(); + } + + private static void GetBaseTypesImpl(List results, string baseTypesString) + { + var generic = 0; + var last = 1; + + if (baseTypesString.IndexOf(',') == -1) + { + results.Add(baseTypesString.Trim()); + return; + } + + for (var i = 0; i < baseTypesString.Length; i++) + { + if (baseTypesString[i] == '<') + { + generic++; + } + + if (baseTypesString[i] == '>') + { + generic--; + } + + if (baseTypesString[i] == ',' && generic == 0) + { + results.Add(baseTypesString.Substring(last, i - last)); +#pragma warning disable S109 // Magic numbers should not be used + last = i + 2; // ", " +#pragma warning restore S109 // Magic numbers should not be used + } + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/CallAnalyzer.Handlers.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/CallAnalyzer.Handlers.cs new file mode 100644 index 0000000000..2cec3b9923 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/CallAnalyzer.Handlers.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.LocalAnalyzers.CallAnalysis; + +public partial class CallAnalyzer +{ + private sealed class Handlers + { + private readonly State _state; + + public Handlers(State state) + { + _state = state; + } + + public void HandleInvocation(OperationAnalysisContext context) + { + var op = (IInvocationOperation)context.Operation; + var target = op.TargetMethod; + + if (target != null) + { + if (_state.Methods.TryGetValue(target.OriginalDefinition, out var handlers)) + { + if (op.Arguments.Length == target.Parameters.Length) + { + foreach (var handler in handlers) + { + handler(context, op); + } + } + } + + if (_state.InterfaceMethodNames.Contains(target.Name)) + { + var type = target.ContainingType; + if (type.TypeKind == TypeKind.Interface) + { + if (_state.Interfaces.TryGetValue(type, out var l)) + { + foreach (var h in l) + { + if (SymbolEqualityComparer.Default.Equals(target, h.Method)) + { + foreach (var action in h.Actions) + { + action(context, op); + } + } + } + } + } + else + { + foreach (var iface in type.AllInterfaces) + { + if (_state.Interfaces.TryGetValue(iface, out var l)) + { + foreach (var h in l) + { + var impl = type.FindImplementationForInterfaceMember(h.Method); + if (SymbolEqualityComparer.Default.Equals(target, impl)) + { + foreach (var action in h.Actions) + { + action(context, op); + } + } + } + } + } + } + } + } + } + + public void HandleObjectCreation(OperationAnalysisContext context) + { + var op = (IObjectCreationOperation)context.Operation; + if (op.Constructor != null) + { + if (_state.Ctors.TryGetValue(op.Constructor.OriginalDefinition, out var handlers)) + { + if (op.Arguments.Length == op.Constructor.Parameters.Length) + { + foreach (var handler in handlers) + { + handler(context, op); + } + } + } + } + } + + public void HandlePropertyReference(OperationAnalysisContext context) + { + var op = (IPropertyReferenceOperation)context.Operation; + if (_state.Props.TryGetValue(op.Property, out var handlers)) + { + foreach (var handler in handlers) + { + handler(context, op); + } + } + } + + public void HandleThrow(OperationAnalysisContext context) + { + var op = (IThrowOperation)context.Operation; + + if (op.Exception is IConversionOperation convOp) + { + if (convOp.Operand is IObjectCreationOperation creationOp) + { + if (creationOp.Type != null) + { + if (_state.ExceptionTypes.TryGetValue(creationOp.Type, out var handlers)) + { + foreach (var handler in handlers) + { + handler(context, op); + } + } + } + } + } + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/CallAnalyzer.Registrar.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/CallAnalyzer.Registrar.cs new file mode 100644 index 0000000000..1711b1ac6b --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/CallAnalyzer.Registrar.cs @@ -0,0 +1,241 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.LocalAnalyzers.CallAnalysis; + +public partial class CallAnalyzer +{ + /// + /// Enables call analysis classes to register callbacks. + /// + internal sealed class Registrar + { + private readonly State _state; + + internal Registrar(State state, Compilation compilation) + { + _state = state; + Compilation = compilation; + } + + /// + /// Registers a callback to be invoked whenever the given method is invoked directly in code. + /// + /// + /// Note that this is not designed for use with interface methods. + /// + public void RegisterMethod(IMethodSymbol method, Action action) + { + if (!_state.Methods.TryGetValue(method, out var l)) + { + l = new(); + _state.Methods.Add(method, l); + } + + l.Add(action); + } + + /// + /// Registers a callback to be invoked whenever the given method overloads are invoked directly in code. + /// + /// + /// Note that this is not designed for use with interface methods. + /// + public void RegisterMethods(string typeName, string methodName, Action action) + { + var dict = new Dictionary + { + { typeName, new[] { methodName } }, + }; + + RegisterMethods(dict, action); + } + + /// + /// Registers a callback to be invoked whenever any of the specified methods are invoked. + /// + /// + /// The input dictionary has type names as keys, and arrays of method names as values. + /// + public void RegisterMethods(Dictionary methods, Action action) + { + foreach (var pair in methods) + { + var type = Compilation.GetTypeByMetadataName(pair.Key); + if (type != null) + { + foreach (var m in pair.Value) + { + foreach (var method in type.GetMembers(m).OfType()) + { + RegisterMethod(method, action); + } + } + } + } + } + + /// + /// Registers a callback to be invoked whenever the specified constructor is invoked. + /// + public void RegisterConstructor(IMethodSymbol ctor, Action action) + { + if (!_state.Ctors.TryGetValue(ctor, out var l)) + { + l = new(); + _state.Ctors.Add(ctor, l); + } + + l.Add(action); + } + + /// + /// Registers a callback to be invoked whenever constructors for the given type are invoked. + /// + public void RegisterConstructors(string typeName, Action action) + { + RegisterConstructors(new[] { typeName }, action); + } + + /// + /// Registers a callback to be invoked whenever constructors for any of the given types are invoked. + /// + public void RegisterConstructors(string[] typeNames, Action action) + { + foreach (var typeName in typeNames) + { + var type = Compilation.GetTypeByMetadataName(typeName); + if (type != null) + { + foreach (var ctor in type.Constructors) + { + RegisterConstructor(ctor, action); + } + } + } + } + + /// + /// Registers a callback to be invoked whenever the given property is invoked (set or get). + /// + public void RegisterProperty(IPropertySymbol prop, Action action) + { + if (!_state.Props.TryGetValue(prop, out var l)) + { + l = new(); + _state.Props.Add(prop, l); + } + + l.Add(action); + } + + /// + /// Registers a callback to be invoked whenever any of the given properties are invoked (set or get). + /// + /// + /// The input dictionary has type names as keys, and arrays of method names as values. + /// + public void RegisterProperties(Dictionary props, Action action) + { + foreach (var pair in props) + { + var type = Compilation.GetTypeByMetadataName(pair.Key); + if (type != null) + { + foreach (var m in pair.Value) + { + foreach (var prop in type.GetMembers(m).OfType()) + { + RegisterProperty(prop, action); + } + } + } + } + } + + /// + /// Registers a callback to be invoked whenever the given interface method is invoked. + /// + public void RegisterInterfaceMethod(IMethodSymbol method, Action action) + { + if (!_state.Interfaces.TryGetValue(method.ContainingType, out var handlers)) + { + handlers = new(); + _state.Interfaces.Add(method.ContainingType, handlers); + } + + bool found = false; + foreach (var h in handlers) + { + if (SymbolEqualityComparer.Default.Equals(h.Method, method)) + { + h.Actions.Add(action); + found = true; + break; + } + } + + if (!found) + { + var h = new MethodHandlers(method); + h.Actions.Add(action); + handlers.Add(h); + } + + _ = _state.InterfaceMethodNames.Add(method.Name); + } + + /// + /// Registers a callback to be invoked whenever any of the given interface methods are invoked. + /// + /// + /// The input dictionary has type names as keys, and arrays of method names as values. + /// + public void RegisterInterfaceMethods(Dictionary methods, Action action) + { + foreach (var pair in methods) + { + var type = Compilation.GetTypeByMetadataName(pair.Key); + if (type != null) + { + foreach (var m in pair.Value) + { + foreach (var method in type.GetMembers(m).OfType()) + { + RegisterInterfaceMethod(method, action); + } + } + } + } + } + + /// + /// Registers a callback to be invoked whenever any of the given exception types are thrown. + /// + public void RegisterExceptionTypes(string[] exceptionTypes, Action action) + { + foreach (var et in exceptionTypes) + { + var type = Compilation.GetTypeByMetadataName(et); + if (type != null) + { + if (!_state.ExceptionTypes.TryGetValue(type, out var l)) + { + l = new(); + _state.ExceptionTypes.Add(type, l); + } + + l.Add(action); + } + } + } + + public Compilation Compilation { get; } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/CallAnalyzer.State.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/CallAnalyzer.State.cs new file mode 100644 index 0000000000..c1ca1b321e --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/CallAnalyzer.State.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.LocalAnalyzers.CallAnalysis; + +public partial class CallAnalyzer +{ + internal sealed class State + { + public readonly Dictionary>> Methods = new(SymbolEqualityComparer.Default); + public readonly Dictionary>> Ctors = new(SymbolEqualityComparer.Default); + public readonly Dictionary>> Props = new(SymbolEqualityComparer.Default); + public readonly Dictionary>> ExceptionTypes = new(SymbolEqualityComparer.Default); + public readonly Dictionary> Interfaces = new(SymbolEqualityComparer.Default); + public readonly HashSet InterfaceMethodNames = new(); + } + + internal sealed class MethodHandlers + { + public MethodHandlers(IMethodSymbol method) + { + Method = method; + } + + public IMethodSymbol Method { get; } + public List> Actions { get; } = new(); + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/CallAnalyzer.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/CallAnalyzer.cs new file mode 100644 index 0000000000..e250a651c6 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/CallAnalyzer.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.Extensions.LocalAnalyzers.CallAnalysis; + +/// +/// Composite analyzer that efficiently inspects various types of method/ctor/property calls. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed partial class CallAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + DiagDescriptors.ToInvariantString, + DiagDescriptors.ThrowsStatement, + DiagDescriptors.ThrowsExpression); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationStartContext => + { + var state = new State(); + + var reg = new Registrar(state, compilationStartContext.Compilation); + + _ = new ToInvariantString(reg); + _ = new Throws(reg); + + var handlers = new Handlers(state); +#pragma warning disable R9A044 + compilationStartContext.RegisterOperationAction(handlers.HandleInvocation, OperationKind.Invocation); + compilationStartContext.RegisterOperationAction(handlers.HandleObjectCreation, OperationKind.ObjectCreation); + compilationStartContext.RegisterOperationAction(handlers.HandlePropertyReference, OperationKind.PropertyReference); + compilationStartContext.RegisterOperationAction(handlers.HandleThrow, OperationKind.Throw); +#pragma warning restore R9A044 + }); + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/Throws.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/Throws.cs new file mode 100644 index 0000000000..6adf637ee8 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/Throws.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.LocalAnalyzers.CallAnalysis; + +/// +/// Recommends using R9's Throws class. +/// +internal sealed class Throws +{ + private static readonly string[] _useThrowsExceptionTypes = new[] + { + "System.ArgumentException", + "System.ArgumentNullException", + "System.ArgumentOutOfRangeException", + + // temporarily disabled in order to roll out analyzer updates without changing + // the rest of the source base. I'll start enabling the new analyzers and fixing + // all the warnings in subsequent prs. +#if TURNED_OFF_FOR_ANALYZER_ROLLOUT + "System.InvalidOperationException", +#endif + }; + + public Throws(CallAnalyzer.Registrar reg) + { + reg.RegisterExceptionTypes(_useThrowsExceptionTypes, Handle); + + static void Handle(OperationAnalysisContext context, IThrowOperation op) + { + var convOp = (IConversionOperation?)op.Exception; + var creationOp = (IObjectCreationOperation?)convOp?.Operand; + + if (creationOp?.Type != null) + { + if (op.Syntax.IsKind(SyntaxKind.ThrowStatement)) + { + var diagnostic = Diagnostic.Create( + DiagDescriptors.ThrowsStatement, + op.Syntax.GetLocation(), + $"Microsoft.Extensions.Diagnostics.Throws.{creationOp.Type.Name}"); + + context.ReportDiagnostic(diagnostic); + } + else if (op.Syntax.IsKind(SyntaxKind.ThrowExpression)) + { + if (creationOp.Type.Name == "ArgumentNullException") + { + var throwExpression = (ThrowExpressionSyntax)op.Syntax; + if (throwExpression.Parent is BinaryExpressionSyntax binaryExpression) + { + var diagnostic = Diagnostic.Create( + DiagDescriptors.ThrowsExpression, + binaryExpression.GetLocation(), + "Microsoft.Extensions.Diagnostics.Throws.IfNull"); + + context.ReportDiagnostic(diagnostic); + } + } + } + } + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/ToInvariantString.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/ToInvariantString.cs new file mode 100644 index 0000000000..2070c508f3 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/CallAnalysis/ToInvariantString.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.LocalAnalyzers.CallAnalysis; + +/// +/// Recommends using R9's ToInvariantString extension method. +/// +internal sealed class ToInvariantString +{ + private static readonly SpecialType[] _intTypes = new[] + { + SpecialType.System_Byte, + SpecialType.System_Int16, + SpecialType.System_Int32, + SpecialType.System_Int64, + }; + + public ToInvariantString(CallAnalyzer.Registrar reg) + { + var formatProvider = reg.Compilation.GetTypeByMetadataName("System.IFormatProvider"); + + foreach (var type in _intTypes) + { + foreach (var method in reg.Compilation.GetSpecialType(type).GetMembers("ToString").OfType()) + { + if (method.Parameters.Length == 1 && SymbolEqualityComparer.Default.Equals(method.Parameters[0].Type, formatProvider)) + { + reg.RegisterMethod(method, Handle); + } + } + } + + static void Handle(OperationAnalysisContext context, IInvocationOperation op) + { + var a = op.Arguments[0]; + if (a.Value is IConversionOperation conv) + { + if (conv.Operand is IPropertyReferenceOperation prop) + { + var cultureInfo = context.Compilation.GetTypeByMetadataName("System.Globalization.CultureInfo"); + var invariantCulture = cultureInfo?.GetMembers("InvariantCulture").OfType().SingleOrDefault(); + + if (SymbolEqualityComparer.Default.Equals(invariantCulture, prop.Property)) + { + var diagnostic = Diagnostic.Create(DiagDescriptors.ToInvariantString, op.Syntax.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } + } + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/DiagDescriptors.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/DiagDescriptors.cs new file mode 100644 index 0000000000..9d3e341f5f --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/DiagDescriptors.cs @@ -0,0 +1,568 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +[assembly: System.Resources.NeutralResourcesLanguage("en-us")] + +namespace Microsoft.Extensions.LocalAnalyzers; + +internal static class DiagDescriptors +{ + /// + /// Category for analyzers that will improve performance of the application. + /// + private const string Performance = nameof(Performance); + + /// + /// Category for analyzers that will make code more readable. + /// + private const string Readability = nameof(Readability); + + /// + /// Category for analyzers that will improve reliability of the application. + /// + private const string Reliability = nameof(Reliability); + + /// + /// Category for analyzers that will improve resiliency of the application. + /// + private const string Resilience = nameof(Resilience); + + /// + /// Category for analyzers that will make code more correct. + /// + private const string Correctness = nameof(Correctness); + + /// + /// Category for analyzers that will improve the privacy posture of code. + /// + private const string Privacy = nameof(Privacy); + + public static DiagnosticDescriptor LegacyLogging { get; } = new( + id: "R9A000", + messageFormat: Resources.LegacyLoggingMessage, + title: Resources.LegacyLoggingTitle, + category: Performance, + description: Resources.LegacyLoggingDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a000", + isEnabledByDefault: true); + + public static DiagnosticDescriptor MemoryStream { get; } = new( + id: "R9A001", + messageFormat: Resources.MemoryStreamMessage, + title: Resources.MemoryStreamTitle, + category: Performance, + description: Resources.MemoryStreamDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a001", + isEnabledByDefault: true); + + // R9A002 is no longer in use + + public static DiagnosticDescriptor DistributedCache { get; } = new( + id: "R9A003", + messageFormat: Resources.DistributedCacheMessage, + title: Resources.DistributedCacheTitle, + category: Performance, + description: Resources.DistributedCacheDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a003", + isEnabledByDefault: true); + + // R9A004: retired + + public static DiagnosticDescriptor UsingMicrosoftExtensionsCachingRedis { get; } = new( + id: "R9A005", + messageFormat: Resources.UsingMicrosoftExtensionsCachingRedisMessage, + title: Resources.UsingMicrosoftExtensionsCachingRedisTitle, + category: Resilience, + description: Resources.UsingMicrosoftExtensionsCachingRedisDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a005", + isEnabledByDefault: true); + + public static DiagnosticDescriptor UpdateReturnType { get; } = new( + id: "R9A006", + messageFormat: Resources.UpdateReturnTypeMessage, + title: Resources.UpdateReturnTypeTitle, + category: Correctness, + description: Resources.UsingMetricMethodDescription, + defaultSeverity: DiagnosticSeverity.Error, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a006", + isEnabledByDefault: true); + + public static DiagnosticDescriptor RemoveMethodBody { get; } = new( + id: "R9A007", + messageFormat: Resources.RemoveMethodBodyMessage, + title: Resources.RemoveMethodBodyTitle, + category: Correctness, + description: Resources.UsingMetricMethodDescription, + defaultSeverity: DiagnosticSeverity.Error, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a007", + isEnabledByDefault: true); + + public static DiagnosticDescriptor AddMissingMeter { get; } = new( + id: "R9A008", + messageFormat: Resources.AddMissingMeterMessage, + title: Resources.AddMissingMeterTitle, + category: Correctness, + description: Resources.UsingMetricMethodDescription, + defaultSeverity: DiagnosticSeverity.Error, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a008", + isEnabledByDefault: true); + + public static DiagnosticDescriptor UpdateDimensionParamTypes { get; } = new( + id: "R9A009", + messageFormat: Resources.UpdateDimensionParamTypesMessage, + title: Resources.UpdateDimensionParamTypesTitle, + category: Correctness, + description: Resources.UsingMetricMethodDescription, + defaultSeverity: DiagnosticSeverity.Error, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a009", + isEnabledByDefault: true); + + public static DiagnosticDescriptor StaticMethodDeclaration { get; } = new( + id: "R9A010", + messageFormat: Resources.StaticMethodDeclarationMessage, + title: Resources.StaticMethodDeclarationTitle, + category: Correctness, + description: Resources.UsingMetricMethodDescription, + defaultSeverity: DiagnosticSeverity.Error, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a010", + isEnabledByDefault: true); + + public static DiagnosticDescriptor PartialMethodDeclaration { get; } = new( + id: "R9A011", + messageFormat: Resources.PartialMethodDeclarationMessage, + title: Resources.PartialMethodDeclarationTitle, + category: Correctness, + description: Resources.UsingMetricMethodDescription, + defaultSeverity: DiagnosticSeverity.Error, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a011", + isEnabledByDefault: true); + + public static DiagnosticDescriptor PublicMethodDeclaration { get; } = new( + id: "R9A012", + messageFormat: Resources.PublicMethodDeclarationMessage, + title: Resources.PublicMethodDeclarationTitle, + category: Correctness, + description: Resources.UsingMetricMethodDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a012", + isEnabledByDefault: true); + + public static DiagnosticDescriptor SealInternalClass { get; } = new( + id: "R9A013", + messageFormat: Resources.SealInternalClassMessage, + title: Resources.SealInternalClassTitle, + category: Performance, + description: Resources.SealInternalClassDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a013", + isEnabledByDefault: true); + + public static DiagnosticDescriptor ThrowsExpression { get; } = new( + id: "R9A014", + messageFormat: Resources.ThrowsExpressionMessage, + title: Resources.ThrowsExpressionTitle, + category: Performance, + description: Resources.ThrowsExpressionDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a014", + isEnabledByDefault: true); + + public static DiagnosticDescriptor ThrowsStatement { get; } = new( + id: "R9A015", + messageFormat: Resources.ThrowsStatementMessage, + title: Resources.ThrowsStatementTitle, + category: Performance, + description: Resources.ThrowsStatementDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a015", + isEnabledByDefault: true); + + // R9A016 has been retired + + public static DiagnosticDescriptor BlockingCall { get; } = new( + id: "R9A017", + messageFormat: Resources.BlockingCallMessage, + title: Resources.BlockingCallTitle, + category: Performance, + description: Resources.BlockingCallDescription, + defaultSeverity: DiagnosticSeverity.Info, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a017", + isEnabledByDefault: true); + + public static DiagnosticDescriptor StringFormat { get; } = new( + id: "R9A018", + messageFormat: Resources.StringFormatMessage, + title: Resources.StringFormatTitle, + category: Performance, + description: Resources.StringFormatDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a018", + isEnabledByDefault: true); + + public static DiagnosticDescriptor UsingExcessiveDictionaryLookup { get; } = new( + id: "R9A019", + messageFormat: Resources.UsingExcessiveDictionaryLookupMessage, + title: Resources.UsingExcessiveDictionaryLookupTitle, + category: Performance, + description: Resources.UsingExcessiveDictionaryLookupDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a019", + isEnabledByDefault: true); + + public static DiagnosticDescriptor UsingExcessiveSetLookup { get; } = new( + id: "R9A020", + messageFormat: Resources.UsingExcessiveSetLookupMessage, + title: Resources.UsingExcessiveSetLookupTitle, + category: Performance, + description: Resources.UsingExcessiveSetLookupDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a020", + isEnabledByDefault: true); + + public static DiagnosticDescriptor UsingToStringInLoggers { get; } = new( + id: "R9A021", + messageFormat: Resources.UsingToStringInLoggersMessage, + title: Resources.UsingToStringInLoggersTitle, + category: "Performance", + description: Resources.UsingToStringInLoggersDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a021", + isEnabledByDefault: true); + + public static DiagnosticDescriptor StaticTime { get; } = new( + id: "R9A022", + messageFormat: Resources.StaticTimeMessage, + title: Resources.StaticTimeTitle, + category: Reliability, + description: Resources.StaticTimeDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a022", + isEnabledByDefault: true); + + public static DiagnosticDescriptor Stopwatch { get; } = new( + id: "R9A023", + messageFormat: Resources.StopwatchMessage, + title: Resources.StopwatchTitle, + category: Performance, + description: Resources.StopwatchDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a023", + isEnabledByDefault: true); + + public static DiagnosticDescriptor SensitiveDataClassifierPropagation { get; } = new( + id: "R9A024", + messageFormat: Resources.SensitiveDataClassifierPropagationMessage, + title: Resources.SensitiveDataClassifierPropagationTitle, + category: Privacy, + description: Resources.SensitiveDataClassifierPropagationDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a024", + isEnabledByDefault: true); + + // R9A025..R9A027 unused + + public static DiagnosticDescriptor UserInputFromRequestAnalyzer { get; } = new( + id: "R9A028", + messageFormat: Resources.UserInputFromRequestAnalyzerMessage, + title: Resources.UserInputFromRequestAnalyzerTitle, + category: Privacy, + description: Resources.UserInputFromRequestAnalyzerDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a028", + isEnabledByDefault: true); + + public static DiagnosticDescriptor UsingExperimentalApi { get; } = new( + id: "R9A029", + messageFormat: Resources.UsingExperimentalApiMessage, + title: Resources.UsingExperimentalApiTitle, + category: Reliability, + description: Resources.UsingExperimentalApiDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a029", + isEnabledByDefault: true); + + public static DiagnosticDescriptor StartsEndsWith { get; } = new( + id: "R9A030", + messageFormat: Resources.StartsEndsWithMessage, + title: Resources.StartsEndsWithTitle, + category: Performance, + description: Resources.StartsEndsWithDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a030", + isEnabledByDefault: true); + + public static DiagnosticDescriptor MakeExeTypesInternal { get; } = new( + id: "R9A031", + messageFormat: Resources.MakeExeTypesInternalMessage, + title: Resources.MakeExeTypesInternalTitle, + category: Performance, + description: Resources.MakeExeTypesInternalDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a031", + isEnabledByDefault: true); + + public static DiagnosticDescriptor Arrays { get; } = new( + id: "R9A032", + messageFormat: Resources.ArraysMessage, + title: Resources.ArraysTitle, + category: Performance, + description: Resources.ArraysDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a032", + isEnabledByDefault: true); + + public static DiagnosticDescriptor EnumStrings { get; } = new( + id: "R9A033", + messageFormat: Resources.EnumStringsMessage, + title: Resources.EnumStringsTitle, + category: Performance, + description: Resources.EnumStringsDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a033", + isEnabledByDefault: true); + + // R9A034 deprecated + // R9A035 deprecated + + public static DiagnosticDescriptor ToInvariantString { get; } = new( + id: "R9A036", + messageFormat: Resources.ToInvariantStringMessage, + title: Resources.ToInvariantStringTitle, + category: Performance, + description: Resources.ToInvariantStringDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a036", + isEnabledByDefault: true); + + public static DiagnosticDescriptor ValueTuple { get; } = new( + id: "R9A037", + messageFormat: Resources.ValueTupleMessage, + title: Resources.ValueTupleTitle, + category: Performance, + description: Resources.ValueTupleDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a037", + isEnabledByDefault: true); + + public static DiagnosticDescriptor ObjectPool { get; } = new( + id: "R9A038", + messageFormat: Resources.ObjectPoolMessage, + title: Resources.ObjectPoolTitle, + category: Performance, + description: Resources.ObjectPoolDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a038", + isEnabledByDefault: true); + + public static DiagnosticDescriptor NullCheck { get; } = new( + id: "R9A039", + messageFormat: Resources.NullCheckMessage, + title: Resources.NullCheckTitle, + category: Performance, + description: Resources.NullCheckDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a039", + isEnabledByDefault: true); + + public static DiagnosticDescriptor LegacyCollection { get; } = new( + id: "R9A040", + messageFormat: Resources.LegacyCollectionMessage, + title: Resources.LegacyCollectionTitle, + category: Performance, + description: Resources.LegacyCollectionDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a040", + isEnabledByDefault: true); + + public static DiagnosticDescriptor UseConcreteTypeForField { get; } = new( + id: "R9A041", + messageFormat: Resources.UseConcreteTypeForFieldMessage, + title: Resources.UseConcreteTypeTitle, + category: Performance, + description: Resources.UseConcreteTypeDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a041", + isEnabledByDefault: true); + + public static DiagnosticDescriptor UseConcreteTypeForParameter { get; } = new( + id: "R9A041", + messageFormat: Resources.UseConcreteTypeForParameterMessage, + title: Resources.UseConcreteTypeTitle, + category: Performance, + description: Resources.UseConcreteTypeDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a041", + isEnabledByDefault: true); + + public static DiagnosticDescriptor UseConcreteTypeForLocal { get; } = new( + id: "R9A041", + messageFormat: Resources.UseConcreteTypeForLocalMessage, + title: Resources.UseConcreteTypeTitle, + category: Performance, + description: Resources.UseConcreteTypeDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a041", + isEnabledByDefault: true); + + public static DiagnosticDescriptor UseConcreteTypeForMethodReturn { get; } = new( + id: "R9A041", + messageFormat: Resources.UseConcreteTypeForMethodReturnMessage, + title: Resources.UseConcreteTypeTitle, + category: Performance, + description: Resources.UseConcreteTypeDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a041", + isEnabledByDefault: true); + + public static DiagnosticDescriptor UserDataAPIAllParametersAnnotated { get; } = new( + id: "R9A042", + messageFormat: Resources.UserDataAPIAllParametersAnnotatedMessage, + title: Resources.UserDataAPIAllParametersAnnotatedTitle, + category: Privacy, + description: Resources.UserDataAPIAllParametersAnnotatedDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a042", + isEnabledByDefault: true); + + public static DiagnosticDescriptor Split { get; } = new( + id: "R9A043", + messageFormat: Resources.SplitMessage, + title: Resources.SplitTitle, + category: Performance, + description: Resources.SplitDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a043", + isEnabledByDefault: true); + + public static DiagnosticDescriptor MakeArrayStatic { get; } = new( + id: "R9A044", + messageFormat: Resources.MakeArrayStaticMessage, + title: Resources.MakeArrayStaticTitle, + category: Performance, + description: Resources.MakeArrayStaticDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a044", + isEnabledByDefault: true); + + // R9A045..R9A047 were retired + + public static DiagnosticDescriptor Any { get; } = new( + id: "R9A048", + messageFormat: Resources.AnyMessage, + title: Resources.AnyTitle, + category: Performance, + description: Resources.AnyDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a048", + isEnabledByDefault: true); + + public static DiagnosticDescriptor NewSymbolsMustBeMarkedExperimental { get; } = new( + id: "R9A049", + messageFormat: Resources.NewSymbolsMustBeMarkedExperimentalMessage, + title: Resources.NewSymbolsMustBeMarkedExperimentalTitle, + category: Correctness, + description: Resources.NewSymbolsMustBeMarkedExperimentalDescription, + defaultSeverity: DiagnosticSeverity.Hidden, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a049", + isEnabledByDefault: true); + + public static DiagnosticDescriptor ExperimentalSymbolsCantBeMarkedObsolete { get; } = new( + id: "R9A050", + messageFormat: Resources.ExperimentalSymbolsCantBeMarkedObsoleteMessage, + title: Resources.ExperimentalSymbolsCantBeMarkedObsoleteTitle, + category: Correctness, + description: Resources.ExperimentalSymbolsCantBeMarkedObsoleteDescription, + defaultSeverity: DiagnosticSeverity.Hidden, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a050", + isEnabledByDefault: true); + + public static DiagnosticDescriptor PublishedSymbolsCantBeMarkedExperimental { get; } = new( + id: "R9A051", + messageFormat: Resources.PublishedSymbolsCantBeMarkedExperimentalMessage, + title: Resources.PublishedSymbolsCantBeMarkedExperimentalTitle, + category: Correctness, + description: Resources.PublishedSymbolsCantBeMarkedExperimentalDescription, + defaultSeverity: DiagnosticSeverity.Hidden, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a051", + isEnabledByDefault: true); + + public static DiagnosticDescriptor PublishedSymbolsCantBeDeleted { get; } = new( + id: "R9A052", + messageFormat: Resources.PublishedSymbolsCantBeDeletedMessage, + title: Resources.PublishedSymbolsCantBeDeletedTitle, + category: Correctness, + description: Resources.PublishedSymbolsCantBeDeletedDescription, + defaultSeverity: DiagnosticSeverity.Hidden, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a052", + isEnabledByDefault: true); + + // R9A053 and R9A054 were retired + + public static DiagnosticDescriptor PublishedSymbolsCantChange { get; } = new( + id: "R9A055", + messageFormat: Resources.PublishedSymbolsCantChangeMessage, + title: Resources.PublishedSymbolsCantChangedTitle, + category: Correctness, + description: Resources.PublishedSymbolsCantChangeDescription, + defaultSeverity: DiagnosticSeverity.Hidden, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a055", + isEnabledByDefault: true); + + public static DiagnosticDescriptor AsyncCallInsideUsingBlock { get; } = new( + id: "R9A056", + messageFormat: Resources.AsyncCallInsideUsingBlockMessage, + title: Resources.AsyncCallInsideUsingBlockTitle, + category: Correctness, + description: Resources.AsyncCallInsideUsingBlockDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a056", + isEnabledByDefault: true); + + // R9A057 retired + + public static DiagnosticDescriptor ConditionalAccess { get; } = new( + id: "R9A058", + messageFormat: Resources.ConditionalAccessMessage, + title: Resources.ConditionalAccessTitle, + category: Performance, + description: Resources.ConditionalAccessDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a058", + isEnabledByDefault: true); + + public static DiagnosticDescriptor CoalesceAssignment { get; } = new( + id: "R9A059", + messageFormat: Resources.CoalesceAssignmentMessage, + title: Resources.CoalesceAssignmentTitle, + category: Performance, + description: Resources.CoalesceAssignmentDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a059", + isEnabledByDefault: true); + + public static DiagnosticDescriptor Coalesce { get; } = new( + id: "R9A060", + messageFormat: Resources.CoalesceMessage, + title: Resources.CoalesceTitle, + category: Performance, + description: Resources.CoalesceDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a060", + isEnabledByDefault: true); + + public static DiagnosticDescriptor AsyncMethodWithoutCancellation { get; } = new( + id: "R9A061", + messageFormat: Resources.AsyncMethodWithoutCancellationMessage, + title: Resources.AsyncMethodWithoutCancellationTitle, + category: Resilience, + description: Resources.AsyncMethodWithoutCancellationDescription, + defaultSeverity: DiagnosticSeverity.Warning, + helpLinkUri: "https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a061", + isEnabledByDefault: true); +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Microsoft.Extensions.LocalAnalyzers.csproj b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Microsoft.Extensions.LocalAnalyzers.csproj new file mode 100644 index 0000000000..b56ce6b366 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Microsoft.Extensions.LocalAnalyzers.csproj @@ -0,0 +1,38 @@ + + + Microsoft.Extensions.LocalAnalyzers + Analyzers used only in this repo + Fundamentals + Static Analysis + + + + true + cs + 4.0 + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + + + normal + 92 + 87 + + + + + + + + + + + + + + + + + + + diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Resources.Designer.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Resources.Designer.cs new file mode 100644 index 0000000000..c9cac5e042 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Resources.Designer.cs @@ -0,0 +1,1467 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Extensions.LocalAnalyzers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Extensions.LocalAnalyzers.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Add a parameter of type 'IMeter' as the first parameter to the method declaration. + /// + internal static string AddMissingMeterMessage { + get { + return ResourceManager.GetString("AddMissingMeterMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add a parameter of type 'IMeter' to the method declaration. + /// + internal static string AddMissingMeterTitle { + get { + return ResourceManager.GetString("AddMissingMeterTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Annotate experimental API. + /// + internal static string AnnotateExperimentalApi { + get { + return ResourceManager.GetString("AnnotateExperimentalApi", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Using the 'Count' or 'Length' properties to determine if a collection is empty is considerably more efficient than using the 'Any' LINQ method. + /// + internal static string AnyDescription { + get { + return ResourceManager.GetString("AnyDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use the '{0}' property instead of the 'Any' method for improved performance. + /// + internal static string AnyMessage { + get { + return ResourceManager.GetString("AnyMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use the 'Count' or 'Length' properties instead of the 'Any' method for improved performance. + /// + internal static string AnyTitle { + get { + return ResourceManager.GetString("AnyTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Dictionaries and sets which use enums and bytes as keys can often be replaced with simple arrays for improved performance. + /// + internal static string ArraysDescription { + get { + return ResourceManager.GetString("ArraysDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Consider using '{0}?[]' instead of '{1}'. + /// + internal static string ArraysMessage { + get { + return ResourceManager.GetString("ArraysMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Consider using an array instead of a collection. + /// + internal static string ArraysTitle { + get { + return ResourceManager.GetString("ArraysTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to When skipping the await keyword for asynchronous operations inside a using block, then a disposable object could be disposed before the asynchronous invocation finishes. This might result in incorrect behavior and very often ends with a runtime exception notifying that the code is trying to operate on a disposed object.. + /// + internal static string AsyncCallInsideUsingBlockDescription { + get { + return ResourceManager.GetString("AsyncCallInsideUsingBlockDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Async call should be awaited before leaving the 'using' block. + /// + internal static string AsyncCallInsideUsingBlockMessage { + get { + return ResourceManager.GetString("AsyncCallInsideUsingBlockMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fire-and-forget async call inside a 'using' block. + /// + internal static string AsyncCallInsideUsingBlockTitle { + get { + return ResourceManager.GetString("AsyncCallInsideUsingBlockTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Accepting a CancellationToken as a parameter allows caller to express a loss of interest in the result enabling the method to save cycles by finishing early. + /// + internal static string AsyncMethodWithoutCancellationDescription { + get { + return ResourceManager.GetString("AsyncMethodWithoutCancellationDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add CancellationToken as the parameter of asynchronous method. + /// + internal static string AsyncMethodWithoutCancellationMessage { + get { + return ResourceManager.GetString("AsyncMethodWithoutCancellationMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The async method doesn't support cancellation. + /// + internal static string AsyncMethodWithoutCancellationTitle { + get { + return ResourceManager.GetString("AsyncMethodWithoutCancellationTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Modern .NET code should avoid blocking I/O calls since they substantially impact performance. + /// + internal static string BlockingCallDescription { + get { + return ResourceManager.GetString("BlockingCallDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use asynchronous operations instead of legacy thread blocking code. + /// + internal static string BlockingCallMessage { + get { + return ResourceManager.GetString("BlockingCallMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use asynchronous operations instead of legacy thread blocking code. + /// + internal static string BlockingCallTitle { + get { + return ResourceManager.GetString("BlockingCallTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Using the null coalescing assignment operator (??=) with values which are statically known not to be null causes superfluous null checks to be performed at runtime. + /// + internal static string CoalesceAssignmentDescription { + get { + return ResourceManager.GetString("CoalesceAssignmentDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Consider removing unnecessary null coalescing assignment (??=) since the target value is statically known not to be null. + /// + internal static string CoalesceAssignmentMessage { + get { + return ResourceManager.GetString("CoalesceAssignmentMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Consider removing unnecessary null coalescing assignment (??=). + /// + internal static string CoalesceAssignmentTitle { + get { + return ResourceManager.GetString("CoalesceAssignmentTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Using the null coalescing operator (??) with values which are statically known to be null causes superfluous null checks to be performed at runtime. + /// + internal static string CoalesceDescription { + get { + return ResourceManager.GetString("CoalesceDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Consider removing unnecessary null coalescing (??) since the left-hand value is statically known not to be null. + /// + internal static string CoalesceMessage { + get { + return ResourceManager.GetString("CoalesceMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Consider removing unnecessary null coalescing operator (??). + /// + internal static string CoalesceTitle { + get { + return ResourceManager.GetString("CoalesceTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Using the conditional access operator (?) to access values which are statically known not to be null causes superfluous null checks to be performed at runtime. + /// + internal static string ConditionalAccessDescription { + get { + return ResourceManager.GetString("ConditionalAccessDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Consider removing unnecessary conditional access operator (?) since the value is statically known not to be null. + /// + internal static string ConditionalAccessMessage { + get { + return ResourceManager.GetString("ConditionalAccessMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Consider removing unnecessary conditional access operator (?). + /// + internal static string ConditionalAccessTitle { + get { + return ResourceManager.GetString("ConditionalAccessTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Symbols that have been removed from the public API of an assembly must be marked as obsolete. + /// + internal static string DeprecatedSymbolsMustBeMarkedObsoleteDescription { + get { + return ResourceManager.GetString("DeprecatedSymbolsMustBeMarkedObsoleteDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Deprecated symbol '{0}' must be annotated as obsolete. + /// + internal static string DeprecatedSymbolsMustBeMarkedObsoleteMessage { + get { + return ResourceManager.GetString("DeprecatedSymbolsMustBeMarkedObsoleteMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Deprecated symbols must be annotated as obsolete. + /// + internal static string DeprecatedSymbolsMustBeMarkedObsoleteTitle { + get { + return ResourceManager.GetString("DeprecatedSymbolsMustBeMarkedObsoleteTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Calls to 'IDistributedCache.Get/GetAsync' can be replaced with faster alternatives from 'IExtendedDistributedCache'. + /// + internal static string DistributedCacheDescription { + get { + return ResourceManager.GetString("DistributedCacheDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Calls to 'IDistributedCache.Get/GetAsync' can be replaced with faster alternatives from 'IExtendedDistributedCache'. + /// + internal static string DistributedCacheMessage { + get { + return ResourceManager.GetString("DistributedCacheMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use higher performance methods from 'IExtendedDistributedCache'. + /// + internal static string DistributedCacheTitle { + get { + return ResourceManager.GetString("DistributedCacheTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replace uses of 'Enum.GetName' and 'Enum.ToString' for improved performance. + /// + internal static string EnumStringsDescription { + get { + return ResourceManager.GetString("EnumStringsDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use {0} instead of '{1}' for improved performance. + /// + internal static string EnumStringsMessage { + get { + return ResourceManager.GetString("EnumStringsMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replace uses of 'Enum.GetName' and 'Enum.ToString' for improved performance. + /// + internal static string EnumStringsTitle { + get { + return ResourceManager.GetString("EnumStringsTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Symbols being added to the public API of an assembly cannot be marked as obsolete. + /// + internal static string ExperimentalSymbolsCantBeMarkedObsoleteDescription { + get { + return ResourceManager.GetString("ExperimentalSymbolsCantBeMarkedObsoleteDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Experimental symbol '{0}' cannot be marked as obsolete. + /// + internal static string ExperimentalSymbolsCantBeMarkedObsoleteMessage { + get { + return ResourceManager.GetString("ExperimentalSymbolsCantBeMarkedObsoleteMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Experimental symbols cannot be marked as obsolete. + /// + internal static string ExperimentalSymbolsCantBeMarkedObsoleteTitle { + get { + return ResourceManager.GetString("ExperimentalSymbolsCantBeMarkedObsoleteTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generate or update the metric method. + /// + internal static string GenerateOrUpdateMetricMethod { + get { + return ResourceManager.GetString("GenerateOrUpdateMetricMethod", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generate a strongly-typed logging method. + /// + internal static string GenerateStronglyTypedLoggingMethod { + get { + return ResourceManager.GetString("GenerateStronglyTypedLoggingMethod", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Using generic collections can avoid boxing overhead and provides strong typing. + /// + internal static string LegacyCollectionDescription { + get { + return ResourceManager.GetString("LegacyCollectionDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use generic collections instead of legacy collections for improved performance. + /// + internal static string LegacyCollectionMessage { + get { + return ResourceManager.GetString("LegacyCollectionMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use generic collections instead of legacy collections for improved performance. + /// + internal static string LegacyCollectionTitle { + get { + return ResourceManager.GetString("LegacyCollectionTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Identifies calls to legacy logging methods. + /// + internal static string LegacyLoggingDescription { + get { + return ResourceManager.GetString("LegacyLoggingDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use source generated logging methods for improved performance. + /// + internal static string LegacyLoggingMessage { + get { + return ResourceManager.GetString("LegacyLoggingMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use source generated logging methods for improved performance. + /// + internal static string LegacyLoggingTitle { + get { + return ResourceManager.GetString("LegacyLoggingTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Arrays of literal values should generally be assigned to static fields in order to avoid creating them redundantly over time. + /// + internal static string MakeArrayStaticDescription { + get { + return ResourceManager.GetString("MakeArrayStaticDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Assign array of literal values to a static field for improved performance. + /// + internal static string MakeArrayStaticMessage { + get { + return ResourceManager.GetString("MakeArrayStaticMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Assign array of literal values to a static field for improved performance. + /// + internal static string MakeArrayStaticTitle { + get { + return ResourceManager.GetString("MakeArrayStaticTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Making an executable's types internal enables dead code analysis along with other potential optimizations. + /// + internal static string MakeExeTypesInternalDescription { + get { + return ResourceManager.GetString("MakeExeTypesInternalDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Make type '{0}' internal since it is declared in an executable. + /// + internal static string MakeExeTypesInternalMessage { + get { + return ResourceManager.GetString("MakeExeTypesInternalMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Make types declared in an executable internal. + /// + internal static string MakeExeTypesInternalTitle { + get { + return ResourceManager.GetString("MakeExeTypesInternalTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Make the type internal. + /// + internal static string MakeTypeInternal { + get { + return ResourceManager.GetString("MakeTypeInternal", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Identifies uses of 'System.IO.MemoryStream' and recommends a better alternative. + /// + internal static string MemoryStreamDescription { + get { + return ResourceManager.GetString("MemoryStreamDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.IO.RecyclableMemoryStream' instead of 'System.IO.MemoryStream' for improved performance. + /// + internal static string MemoryStreamMessage { + get { + return ResourceManager.GetString("MemoryStreamMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.IO.RecyclableMemoryStream' instead of 'System.IO.MemoryStream' for improved performance. + /// + internal static string MemoryStreamTitle { + get { + return ResourceManager.GetString("MemoryStreamTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Symbols being added to the public API of an assembly must be marked as experimental until they have been appoved. + /// + internal static string NewSymbolsMustBeMarkedExperimentalDescription { + get { + return ResourceManager.GetString("NewSymbolsMustBeMarkedExperimentalDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Newly added symbol '{0}' must be marked as experimental. + /// + internal static string NewSymbolsMustBeMarkedExperimentalMessage { + get { + return ResourceManager.GetString("NewSymbolsMustBeMarkedExperimentalMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Newly added symbols must be marked as experimental. + /// + internal static string NewSymbolsMustBeMarkedExperimentalTitle { + get { + return ResourceManager.GetString("NewSymbolsMustBeMarkedExperimentalTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to When compiling in a nullable context, the C# compiler performs null analysis at compile time so there is no need to also perform null checking at runtime. + /// + internal static string NullCheckDescription { + get { + return ResourceManager.GetString("NullCheckDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove superfluous null check when compiling in a nullable context. + /// + internal static string NullCheckMessage { + get { + return ResourceManager.GetString("NullCheckMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove superfluous null checks when compiling in a nullable context. + /// + internal static string NullCheckTitle { + get { + return ResourceManager.GetString("NullCheckTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Pools.PoolFactory' instead for improved performance. + /// + internal static string ObjectPoolDescription { + get { + return ResourceManager.GetString("ObjectPoolDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Pools.PoolFactory' instead of '{0}' for improved performance. + /// + internal static string ObjectPoolMessage { + get { + return ResourceManager.GetString("ObjectPoolMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Pools.PoolFactory' instead for improved performance. + /// + internal static string ObjectPoolTitle { + get { + return ResourceManager.GetString("ObjectPoolTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Make method partial. + /// + internal static string PartialMethodDeclarationMessage { + get { + return ResourceManager.GetString("PartialMethodDeclarationMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Make method partial. + /// + internal static string PartialMethodDeclarationTitle { + get { + return ResourceManager.GetString("PartialMethodDeclarationTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Make method public. + /// + internal static string PublicMethodDeclarationMessage { + get { + return ResourceManager.GetString("PublicMethodDeclarationMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Make method public. + /// + internal static string PublicMethodDeclarationTitle { + get { + return ResourceManager.GetString("PublicMethodDeclarationTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Published symbols cannot be deleted to maintain compatibility. + /// + internal static string PublishedSymbolsCantBeDeletedDescription { + get { + return ResourceManager.GetString("PublishedSymbolsCantBeDeletedDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Published symbol '{0}' cannot be deleted to maintain compatibility. + /// + internal static string PublishedSymbolsCantBeDeletedMessage { + get { + return ResourceManager.GetString("PublishedSymbolsCantBeDeletedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Published symbols cannot be deleted to maintain compatibility. + /// + internal static string PublishedSymbolsCantBeDeletedTitle { + get { + return ResourceManager.GetString("PublishedSymbolsCantBeDeletedTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Previously published symbols in the public API of an assembly cannot be marked experimental. + /// + internal static string PublishedSymbolsCantBeMarkedExperimentalDescription { + get { + return ResourceManager.GetString("PublishedSymbolsCantBeMarkedExperimentalDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Published symbol '{0}' cannot be marked experimental. + /// + internal static string PublishedSymbolsCantBeMarkedExperimentalMessage { + get { + return ResourceManager.GetString("PublishedSymbolsCantBeMarkedExperimentalMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Published symbols cannot be marked experimental. + /// + internal static string PublishedSymbolsCantBeMarkedExperimentalTitle { + get { + return ResourceManager.GetString("PublishedSymbolsCantBeMarkedExperimentalTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Published symbols cannot change to maintain compatibility. + /// + internal static string PublishedSymbolsCantChangeDescription { + get { + return ResourceManager.GetString("PublishedSymbolsCantChangeDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Published symbols cannot be changed to maintain compatibility. + /// + internal static string PublishedSymbolsCantChangedTitle { + get { + return ResourceManager.GetString("PublishedSymbolsCantChangedTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Published symbol '{0}' cannot be changed to maintain compatibility. + /// + internal static string PublishedSymbolsCantChangeMessage { + get { + return ResourceManager.GetString("PublishedSymbolsCantChangeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove method body. + /// + internal static string RemoveMethodBodyMessage { + get { + return ResourceManager.GetString("RemoveMethodBodyMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove method body. + /// + internal static string RemoveMethodBodyTitle { + get { + return ResourceManager.GetString("RemoveMethodBodyTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replace 'Any' method call with property access. + /// + internal static string ReplaceAnyCallWithPropertyAccess { + get { + return ResourceManager.GetString("ReplaceAnyCallWithPropertyAccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replace explicit null check with call to 'Throws.IfNull' (needs a 'PackageReference' to 'Microsoft.R9.Extensions.Essentials'). + /// + internal static string ReplaceWithStaticNullCheckMethod { + get { + return ResourceManager.GetString("ReplaceWithStaticNullCheckMethod", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replace explicit throw with call to 'Throws' (needs a 'PackageReference' to 'Microsoft.R9.Extensions.Essentials'). + /// + internal static string ReplaceWithStaticThrowMethod { + get { + return ResourceManager.GetString("ReplaceWithStaticThrowMethod", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Seal class. + /// + internal static string SealClass { + get { + return ResourceManager.GetString("SealClass", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Identifies classes which can be optimized by being sealed. + /// + internal static string SealInternalClassDescription { + get { + return ResourceManager.GetString("SealInternalClassDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Seal class '{0}' for improved performance. + /// + internal static string SealInternalClassMessage { + get { + return ResourceManager.GetString("SealInternalClassMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Seal non-public classes for improved performance. + /// + internal static string SealInternalClassTitle { + get { + return ResourceManager.GetString("SealInternalClassTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A declared type contains members that have data classifications that are not included in the data classifications annotating the declared type. Consider propagating the data classifications from the type's members to the type itself.. + /// + internal static string SensitiveDataClassifierPropagationDescription { + get { + return ResourceManager.GetString("SensitiveDataClassifierPropagationDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type '{0}' contains members with data classifications that are not included on the type itself. Propagate the data classification.. + /// + internal static string SensitiveDataClassifierPropagationMessage { + get { + return ResourceManager.GetString("SensitiveDataClassifierPropagationMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Propagate data classification. + /// + internal static string SensitiveDataClassifierPropagationTitle { + get { + return ResourceManager.GetString("SensitiveDataClassifierPropagationTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Apply code fix for all issues in '{0}' '{1}'. + /// + internal static string SequentialFixAllFormat { + get { + return ResourceManager.GetString("SequentialFixAllFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Apply code fix for all issues in current solution. + /// + internal static string SequentialFixAllInSolution { + get { + return ResourceManager.GetString("SequentialFixAllInSolution", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Text.StringSplitExtensions.TrySplit' for improved performance. + /// + internal static string SplitDescription { + get { + return ResourceManager.GetString("SplitDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Text.StringSplitExtensions.TrySplit' for improved performance. + /// + internal static string SplitMessage { + get { + return ResourceManager.GetString("SplitMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Text.StringSplitExtensions.TrySplit' for improved performance. + /// + internal static string SplitTitle { + get { + return ResourceManager.GetString("SplitTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to When checking for a single character, prefer the character overloads of 'String.StartsWith' and 'String.EndsWith' for improved performance. + /// + internal static string StartsEndsWithDescription { + get { + return ResourceManager.GetString("StartsEndsWithDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use the character-based overload of '{0}'. + /// + internal static string StartsEndsWithMessage { + get { + return ResourceManager.GetString("StartsEndsWithMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use the character-based overloads of 'String.StartsWith' or 'String.EndsWith'. + /// + internal static string StartsEndsWithTitle { + get { + return ResourceManager.GetString("StartsEndsWithTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Make method static. + /// + internal static string StaticMethodDeclarationMessage { + get { + return ResourceManager.GetString("StaticMethodDeclarationMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Make method static. + /// + internal static string StaticMethodDeclarationTitle { + get { + return ResourceManager.GetString("StaticMethodDeclarationTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Identifies uses of time dependent APIs that can lead to flaky tests. + /// + internal static string StaticTimeDescription { + get { + return ResourceManager.GetString("StaticTimeDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Time.IClock' to make the code easier to test. + /// + internal static string StaticTimeMessage { + get { + return ResourceManager.GetString("StaticTimeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Time.IClock' to make the code easier to test. + /// + internal static string StaticTimeTitle { + get { + return ResourceManager.GetString("StaticTimeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Identifies uses of 'System.Diagnostics.Stopwatch'. + /// + internal static string StopwatchDescription { + get { + return ResourceManager.GetString("StopwatchDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Time.PerfStopwatch' instead of 'System.Diagnostics.Stopwatch' for improved performance. + /// + internal static string StopwatchMessage { + get { + return ResourceManager.GetString("StopwatchMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Time.PerfStopwatch' instead of 'System.Diagnostics.Stopwatch' for improved performance. + /// + internal static string StopwatchTitle { + get { + return ResourceManager.GetString("StopwatchTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Identifies uses of 'String.Format' and 'StringBuilder.AppendFormat'. + /// + internal static string StringFormatDescription { + get { + return ResourceManager.GetString("StringFormatDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Text.CompositeFormat' instead of 'string.Format' for improved performance. + /// + internal static string StringFormatMessage { + get { + return ResourceManager.GetString("StringFormatMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Text.CompositeFormat' instead of 'string.Format' for improved performance. + /// + internal static string StringFormatTitle { + get { + return ResourceManager.GetString("StringFormatTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Recommends replacing explicit argument throwing with the more efficient 'Microsoft.R9.Extensions.Diagnostics.Throws' class. + /// + internal static string ThrowsExpressionDescription { + get { + return ResourceManager.GetString("ThrowsExpressionDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use '{0}' to throw the exception instead to improve performance. + /// + internal static string ThrowsExpressionMessage { + get { + return ResourceManager.GetString("ThrowsExpressionMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use the 'Microsoft.R9.Extensions.Diagnostics.Throws' class instead of explicitly throwing exception for improved performance. + /// + internal static string ThrowsExpressionTitle { + get { + return ResourceManager.GetString("ThrowsExpressionTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Recommends replacing explicit argument throwing with the more efficient 'Microsoft.R9.Extensions.Diagnostics.Throws' class. + /// + internal static string ThrowsStatementDescription { + get { + return ResourceManager.GetString("ThrowsStatementDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use '{0}' to throw the exception instead to improve performance. + /// + internal static string ThrowsStatementMessage { + get { + return ResourceManager.GetString("ThrowsStatementMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use the 'Microsoft.R9.Extensions.Diagnostics.Throws' class instead of explicitly throwing exception for improved performance. + /// + internal static string ThrowsStatementTitle { + get { + return ResourceManager.GetString("ThrowsStatementTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to 'Microsoft.R9.Extensions.Text.NumericExtensions.ToInvariantString' provides caching for common numeric values, avoiding the need to allocate new strings in many situations. + /// + internal static string ToInvariantStringDescription { + get { + return ResourceManager.GetString("ToInvariantStringDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Text.NumericExtensions.ToInvariantString' for improved performance. + /// + internal static string ToInvariantStringMessage { + get { + return ResourceManager.GetString("ToInvariantStringMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'Microsoft.R9.Extensions.Text.NumericExtensions.ToInvariantString' for improved performance. + /// + internal static string ToInvariantStringTitle { + get { + return ResourceManager.GetString("ToInvariantStringTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Update method parameters for dimensions to be string type. + /// + internal static string UpdateDimensionParamTypesMessage { + get { + return ResourceManager.GetString("UpdateDimensionParamTypesMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Update method parameters for dimensions to be string type. + /// + internal static string UpdateDimensionParamTypesTitle { + get { + return ResourceManager.GetString("UpdateDimensionParamTypesTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Update return type to be the strong type being created for this metric. + /// + internal static string UpdateReturnTypeMessage { + get { + return ResourceManager.GetString("UpdateReturnTypeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Update return type to match metric type. + /// + internal static string UpdateReturnTypeTitle { + get { + return ResourceManager.GetString("UpdateReturnTypeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Using concrete types avoid virtual or interface call overhead and enables inlining. + /// + internal static string UseConcreteTypeDescription { + get { + return ResourceManager.GetString("UseConcreteTypeDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Change type of field '{0}' from '{1}' to '{2}' for improved performance. + /// + internal static string UseConcreteTypeForFieldMessage { + get { + return ResourceManager.GetString("UseConcreteTypeForFieldMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Change type of variable '{0}' from '{1}' to '{2}' for improved performance. + /// + internal static string UseConcreteTypeForLocalMessage { + get { + return ResourceManager.GetString("UseConcreteTypeForLocalMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Change return type of method '{0}' from '{1}' to '{2}' for improved performance. + /// + internal static string UseConcreteTypeForMethodReturnMessage { + get { + return ResourceManager.GetString("UseConcreteTypeForMethodReturnMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Change type of parameter '{0}' from '{1}' to '{2}' for improved performance. + /// + internal static string UseConcreteTypeForParameterMessage { + get { + return ResourceManager.GetString("UseConcreteTypeForParameterMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use concrete types when possible for improved performance. + /// + internal static string UseConcreteTypeTitle { + get { + return ResourceManager.GetString("UseConcreteTypeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use fixed obsolete attribute message format. + /// + internal static string UseObsoleteAttributeMessageFormatTemplate { + get { + return ResourceManager.GetString("UseObsoleteAttributeMessageFormatTemplate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to All parameters of User Data APIs should be annotated with either UserInputFromRequest or RequestAgnosticAPIParameter attributes. + /// + internal static string UserDataAPIAllParametersAnnotatedDescription { + get { + return ResourceManager.GetString("UserDataAPIAllParametersAnnotatedDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter {0} on User Data API {1} should be annotated with either UserInputFromRequest if it originates from a Http request, or RequestAgnosticAPIParameter if not. + /// + internal static string UserDataAPIAllParametersAnnotatedMessage { + get { + return ResourceManager.GetString("UserDataAPIAllParametersAnnotatedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Annotate all User Data APIs parameters. + /// + internal static string UserDataAPIAllParametersAnnotatedTitle { + get { + return ResourceManager.GetString("UserDataAPIAllParametersAnnotatedTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only Request Context members should be passed as parameters to invoke APIs which are annotated as User Data APIs. + /// + internal static string UserInputFromRequestAnalyzerDescription { + get { + return ResourceManager.GetString("UserInputFromRequestAnalyzerDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Argument '{0}' passed to the user data vending API '{1}''s parameter '{2}' is not an instance of '{3}'. + /// + internal static string UserInputFromRequestAnalyzerMessage { + get { + return ResourceManager.GetString("UserInputFromRequestAnalyzerMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Argument provided for user input parameter on user data vending API is not from any request context. + /// + internal static string UserInputFromRequestAnalyzerTitle { + get { + return ResourceManager.GetString("UserInputFromRequestAnalyzerTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use fixed obsolete attribute message format for soft deleted API. + /// + internal static string UseSoftDeleteObsoleteAttributeFormat { + get { + return ResourceManager.GetString("UseSoftDeleteObsoleteAttributeFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Encourages optimal use of dictionary lookup. + /// + internal static string UsingExcessiveDictionaryLookupDescription { + get { + return ResourceManager.GetString("UsingExcessiveDictionaryLookupDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove excessive dictionary lookups. + /// + internal static string UsingExcessiveDictionaryLookupMessage { + get { + return ResourceManager.GetString("UsingExcessiveDictionaryLookupMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove unnecessary dictionary lookups. + /// + internal static string UsingExcessiveDictionaryLookupTitle { + get { + return ResourceManager.GetString("UsingExcessiveDictionaryLookupTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Encourages optimal use of set lookup. + /// + internal static string UsingExcessiveSetLookupDescription { + get { + return ResourceManager.GetString("UsingExcessiveSetLookupDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove excessive set lookups. + /// + internal static string UsingExcessiveSetLookupMessage { + get { + return ResourceManager.GetString("UsingExcessiveSetLookupMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove unnecessary set lookups. + /// + internal static string UsingExcessiveSetLookupTitle { + get { + return ResourceManager.GetString("UsingExcessiveSetLookupTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Indicates that code is depending on an experimental API. + /// + internal static string UsingExperimentalApiDescription { + get { + return ResourceManager.GetString("UsingExperimentalApiDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to '{0}' is experimental and is subject to change without notice. + /// + internal static string UsingExperimentalApiMessage { + get { + return ResourceManager.GetString("UsingExperimentalApiMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Using experimental API. + /// + internal static string UsingExperimentalApiTitle { + get { + return ResourceManager.GetString("UsingExperimentalApiTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Identifies issues in a method declaration corresponding to the metric attribute. + /// + internal static string UsingMetricMethodDescription { + get { + return ResourceManager.GetString("UsingMetricMethodDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Uses of the 'Microsoft.Extensions.Caching.StackExchangeRedis' package should be replaced with the 'Microsoft.R9.Extensions.Caching.Redis' package. + /// + internal static string UsingMicrosoftExtensionsCachingRedisDescription { + get { + return ResourceManager.GetString("UsingMicrosoftExtensionsCachingRedisDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use the 'Microsoft.R9.Extensions.Caching.Redis' package instead of 'Microsoft.Extensions.Caching.StackExchangeRedis'. + /// + internal static string UsingMicrosoftExtensionsCachingRedisMessage { + get { + return ResourceManager.GetString("UsingMicrosoftExtensionsCachingRedisMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use the 'Microsoft.R9.Extensions.Caching.Redis' package instead of 'Microsoft.Extensions.Caching.StackExchangeRedis'. + /// + internal static string UsingMicrosoftExtensionsCachingRedisTitle { + get { + return ResourceManager.GetString("UsingMicrosoftExtensionsCachingRedisTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Identifies calls to the 'ToString' method as arguments to an R9 logging method. + /// + internal static string UsingToStringInLoggersDescription { + get { + return ResourceManager.GetString("UsingToStringInLoggersDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Provide a logging method that accepts an instance of the object instead of a string. + /// + internal static string UsingToStringInLoggersMessage { + get { + return ResourceManager.GetString("UsingToStringInLoggersMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Perform message formatting in the body of the logging method. + /// + internal static string UsingToStringInLoggersTitle { + get { + return ResourceManager.GetString("UsingToStringInLoggersTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Using 'System.ValueTuple' avoids allocations and is generally more efficient than 'System.Tuple'. + /// + internal static string ValueTupleDescription { + get { + return ResourceManager.GetString("ValueTupleDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'System.ValueTuple' instead of 'System.Tuple' for improved performance. + /// + internal static string ValueTupleMessage { + get { + return ResourceManager.GetString("ValueTupleMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use 'System.ValueTuple' instead of 'System.Tuple' for improved performance. + /// + internal static string ValueTupleTitle { + get { + return ResourceManager.GetString("ValueTupleTitle", resourceCulture); + } + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Resources.resx b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Resources.resx new file mode 100644 index 0000000000..b8b89896cf --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Resources.resx @@ -0,0 +1,588 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Generate a strongly-typed logging method + + + Identifies calls to legacy logging methods + + + Use source generated logging methods for improved performance + + + Use source generated logging methods for improved performance + + + Identifies uses of 'System.IO.MemoryStream' and recommends a better alternative + + + Use 'Microsoft.IO.RecyclableMemoryStream' instead of 'System.IO.MemoryStream' for improved performance + + + Use 'Microsoft.IO.RecyclableMemoryStream' instead of 'System.IO.MemoryStream' for improved performance + + + Calls to 'IDistributedCache.Get/GetAsync' can be replaced with faster alternatives from 'IExtendedDistributedCache' + + + Calls to 'IDistributedCache.Get/GetAsync' can be replaced with faster alternatives from 'IExtendedDistributedCache' + + + Use higher performance methods from 'IExtendedDistributedCache' + + + Use the 'Microsoft.R9.Extensions.Caching.Redis' package instead of 'Microsoft.Extensions.Caching.StackExchangeRedis' + + + Uses of the 'Microsoft.Extensions.Caching.StackExchangeRedis' package should be replaced with the 'Microsoft.R9.Extensions.Caching.Redis' package + + + Use the 'Microsoft.R9.Extensions.Caching.Redis' package instead of 'Microsoft.Extensions.Caching.StackExchangeRedis' + + + Generate or update the metric method + + + Identifies issues in a method declaration corresponding to the metric attribute + + + Add a parameter of type 'IMeter' as the first parameter to the method declaration + + + Add a parameter of type 'IMeter' to the method declaration + + + Make method partial + + + Make method partial + + + Make method public + + + Make method public + + + Remove method body + + + Remove method body + + + Make method static + + + Make method static + + + Update method parameters for dimensions to be string type + + + Update method parameters for dimensions to be string type + + + Update return type to be the strong type being created for this metric + + + Update return type to match metric type + + + Seal class + + + Identifies classes which can be optimized by being sealed + + + Seal class '{0}' for improved performance + + + Seal non-public classes for improved performance + + + Recommends replacing explicit argument throwing with the more efficient 'Microsoft.R9.Extensions.Diagnostics.Throws' class + + + Use '{0}' to throw the exception instead to improve performance + + + Use the 'Microsoft.R9.Extensions.Diagnostics.Throws' class instead of explicitly throwing exception for improved performance + + + Recommends replacing explicit argument throwing with the more efficient 'Microsoft.R9.Extensions.Diagnostics.Throws' class + + + Use '{0}' to throw the exception instead to improve performance + + + Use the 'Microsoft.R9.Extensions.Diagnostics.Throws' class instead of explicitly throwing exception for improved performance + + + Replace explicit null check with call to 'Throws.IfNull' (needs a 'PackageReference' to 'Microsoft.R9.Extensions.Essentials') + + + Replace explicit throw with call to 'Throws' (needs a 'PackageReference' to 'Microsoft.R9.Extensions.Essentials') + + + Modern .NET code should avoid blocking I/O calls since they substantially impact performance + + + Use asynchronous operations instead of legacy thread blocking code + + + Use asynchronous operations instead of legacy thread blocking code + + + Identifies uses of 'String.Format' and 'StringBuilder.AppendFormat' + + + Use 'Microsoft.R9.Extensions.Text.CompositeFormat' instead of 'string.Format' for improved performance + + + Use 'Microsoft.R9.Extensions.Text.CompositeFormat' instead of 'string.Format' for improved performance + + + Encourages optimal use of dictionary lookup + + + Remove excessive dictionary lookups + + + Remove unnecessary dictionary lookups + + + Encourages optimal use of set lookup + + + Remove excessive set lookups + + + Remove unnecessary set lookups + + + Identifies calls to the 'ToString' method as arguments to an R9 logging method + + + Provide a logging method that accepts an instance of the object instead of a string + + + Perform message formatting in the body of the logging method + + + Identifies uses of time dependent APIs that can lead to flaky tests + + + Use 'Microsoft.R9.Extensions.Time.IClock' to make the code easier to test + + + Use 'Microsoft.R9.Extensions.Time.IClock' to make the code easier to test + + + Identifies uses of 'System.Diagnostics.Stopwatch' + + + Use 'Microsoft.R9.Extensions.Time.PerfStopwatch' instead of 'System.Diagnostics.Stopwatch' for improved performance + + + Use 'Microsoft.R9.Extensions.Time.PerfStopwatch' instead of 'System.Diagnostics.Stopwatch' for improved performance + + + A declared type contains members that have data classifications that are not included in the data classifications annotating the declared type. Consider propagating the data classifications from the type's members to the type itself. + + + Type '{0}' contains members with data classifications that are not included on the type itself. Propagate the data classification. + + + Propagate data classification + + + Apply code fix for all issues in '{0}' '{1}' + + + Apply code fix for all issues in current solution + + + Only Request Context members should be passed as parameters to invoke APIs which are annotated as User Data APIs + + + Argument '{0}' passed to the user data vending API '{1}''s parameter '{2}' is not an instance of '{3}' + + + Argument provided for user input parameter on user data vending API is not from any request context + + + Use fixed obsolete attribute message format + + + Use fixed obsolete attribute message format for soft deleted API + + + Indicates that code is depending on an experimental API + + + Using experimental API + + + '{0}' is experimental and is subject to change without notice + + + When checking for a single character, prefer the character overloads of 'String.StartsWith' and 'String.EndsWith' for improved performance + + + Use the character-based overload of '{0}' + + + Use the character-based overloads of 'String.StartsWith' or 'String.EndsWith' + + + Making an executable's types internal enables dead code analysis along with other potential optimizations + + + Make type '{0}' internal since it is declared in an executable + + + Make types declared in an executable internal + + + Dictionaries and sets which use enums and bytes as keys can often be replaced with simple arrays for improved performance + + + Consider using '{0}?[]' instead of '{1}' + + + Consider using an array instead of a collection + + + Replace uses of 'Enum.GetName' and 'Enum.ToString' for improved performance + + + Replace uses of 'Enum.GetName' and 'Enum.ToString' for improved performance + + + Use {0} instead of '{1}' for improved performance + + + 'Microsoft.R9.Extensions.Text.NumericExtensions.ToInvariantString' provides caching for common numeric values, avoiding the need to allocate new strings in many situations + + + Use 'Microsoft.R9.Extensions.Text.NumericExtensions.ToInvariantString' for improved performance + + + Use 'Microsoft.R9.Extensions.Text.NumericExtensions.ToInvariantString' for improved performance + + + Using 'System.ValueTuple' avoids allocations and is generally more efficient than 'System.Tuple' + + + Use 'System.ValueTuple' instead of 'System.Tuple' for improved performance + + + Use 'System.ValueTuple' instead of 'System.Tuple' for improved performance + + + Use 'Microsoft.R9.Extensions.Pools.PoolFactory' instead for improved performance + + + Use 'Microsoft.R9.Extensions.Pools.PoolFactory' instead of '{0}' for improved performance + + + Use 'Microsoft.R9.Extensions.Pools.PoolFactory' instead for improved performance + + + When compiling in a nullable context, the C# compiler performs null analysis at compile time so there is no need to also perform null checking at runtime + + + Remove superfluous null check when compiling in a nullable context + + + Remove superfluous null checks when compiling in a nullable context + + + Make the type internal + + + Using generic collections can avoid boxing overhead and provides strong typing + + + Use generic collections instead of legacy collections for improved performance + + + Use generic collections instead of legacy collections for improved performance + + + Using concrete types avoid virtual or interface call overhead and enables inlining + + + Change type of field '{0}' from '{1}' to '{2}' for improved performance + + + Use concrete types when possible for improved performance + + + Change type of variable '{0}' from '{1}' to '{2}' for improved performance + + + Change return type of method '{0}' from '{1}' to '{2}' for improved performance + + + Change type of parameter '{0}' from '{1}' to '{2}' for improved performance + + + Use 'Microsoft.R9.Extensions.Text.StringSplitExtensions.TrySplit' for improved performance + + + Use 'Microsoft.R9.Extensions.Text.StringSplitExtensions.TrySplit' for improved performance + + + Use 'Microsoft.R9.Extensions.Text.StringSplitExtensions.TrySplit' for improved performance + + + All parameters of User Data APIs should be annotated with either UserInputFromRequest or RequestAgnosticAPIParameter attributes + + + Parameter {0} on User Data API {1} should be annotated with either UserInputFromRequest if it originates from a Http request, or RequestAgnosticAPIParameter if not + + + Annotate all User Data APIs parameters + + + Arrays of literal values should generally be assigned to static fields in order to avoid creating them redundantly over time + + + Assign array of literal values to a static field for improved performance + + + Assign array of literal values to a static field for improved performance + + + Using the 'Count' or 'Length' properties to determine if a collection is empty is considerably more efficient than using the 'Any' LINQ method + + + Use the '{0}' property instead of the 'Any' method for improved performance + + + Use the 'Count' or 'Length' properties instead of the 'Any' method for improved performance + + + Symbols being added to the public API of an assembly cannot be marked as obsolete + + + Experimental symbol '{0}' cannot be marked as obsolete + + + Experimental symbols cannot be marked as obsolete + + + Symbols being added to the public API of an assembly must be marked as experimental until they have been appoved + + + Newly added symbol '{0}' must be marked as experimental + + + Newly added symbols must be marked as experimental + + + Published symbols cannot be deleted to maintain compatibility + + + Published symbol '{0}' cannot be deleted to maintain compatibility + + + Published symbols cannot be deleted to maintain compatibility + + + Deprecated symbol '{0}' must be annotated as obsolete + + + Deprecated symbols must be annotated as obsolete + + + Previously published symbols in the public API of an assembly cannot be marked experimental + + + Published symbol '{0}' cannot be marked experimental + + + Published symbols cannot be marked experimental + + + Published symbols cannot change to maintain compatibility + + + Published symbol '{0}' cannot be changed to maintain compatibility + + + Published symbols cannot be changed to maintain compatibility + + + Replace 'Any' method call with property access + + + When skipping the await keyword for asynchronous operations inside a using block, then a disposable object could be disposed before the asynchronous invocation finishes. This might result in incorrect behavior and very often ends with a runtime exception notifying that the code is trying to operate on a disposed object. + + + Async call should be awaited before leaving the 'using' block + + + Fire-and-forget async call inside a 'using' block + + + Using the conditional access operator (?) to access values which are statically known not to be null causes superfluous null checks to be performed at runtime + + + Consider removing unnecessary conditional access operator (?) since the value is statically known not to be null + + + Consider removing unnecessary conditional access operator (?) + + + Using the null coalescing assignment operator (??=) with values which are statically known not to be null causes superfluous null checks to be performed at runtime + + + Consider removing unnecessary null coalescing assignment (??=) since the target value is statically known not to be null + + + Consider removing unnecessary null coalescing assignment (??=) + + + Using the null coalescing operator (??) with values which are statically known to be null causes superfluous null checks to be performed at runtime + + + Consider removing unnecessary null coalescing (??) since the left-hand value is statically known not to be null + + + Consider removing unnecessary null coalescing operator (??) + + + Accepting a CancellationToken as a parameter allows caller to express a loss of interest in the result enabling the method to save cycles by finishing early + + + Add CancellationToken as the parameter of asynchronous method + + + The async method doesn't support cancellation + + + Annotate experimental API + + + Symbols that have been removed from the public API of an assembly must be marked as obsolete + + \ No newline at end of file diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/CompilationExtensions.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/CompilationExtensions.cs new file mode 100644 index 0000000000..ed2f43855c --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/CompilationExtensions.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.LocalAnalyzers.Utilities; + +internal static class CompilationExtensions +{ + public static bool IsNet6OrGreater(this Compilation compilation) + { + var type = compilation.GetTypeByMetadataName("System.Environment"); + return type != null && type.GetMembers("ProcessPath").Length > 0; + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/OperationExtensions.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/OperationExtensions.cs new file mode 100644 index 0000000000..7fcafb7d7f --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/OperationExtensions.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.LocalAnalyzers.Utilities; + +internal static class OperationExtensions +{ + /// + /// Gets the list of ancestor operations up to the specified operation. + /// + /// Node to start traversing. + /// Node to stop traversing. + /// The enumerator. + public static IEnumerable Ancestors(this IOperation operationToStart, IOperation parent) + { + while (operationToStart.Parent != null) + { + if (operationToStart.Parent == parent) + { + yield break; + } + + yield return operationToStart.Parent; + operationToStart = operationToStart.Parent; + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/SymbolExtensions.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/SymbolExtensions.cs new file mode 100644 index 0000000000..b27a66f310 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/SymbolExtensions.cs @@ -0,0 +1,209 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.LocalAnalyzers.Utilities; + +internal static class SymbolExtensions +{ + /// + /// Determines whether the current instance is an ancestor type of the parameter. + /// + /// The potential ancestor being inspected. + /// The type to test. + /// if derives directly or indirectly from . + public static bool IsAncestorOf(this ITypeSymbol potentialAncestor, ITypeSymbol potentialDescendant) + { + ITypeSymbol? t = potentialDescendant; + while (true) + { + t = t.BaseType; + if (t == null) + { + return false; + } + + if (SymbolEqualityComparer.Default.Equals(t, potentialAncestor)) + { + return true; + } + } + } + + /// + /// True if the symbol is externally visible outside this assembly. + /// + public static bool IsExternallyVisible(this ISymbol symbol) + { + while (symbol.Kind != SymbolKind.Namespace) + { + switch (symbol.DeclaredAccessibility) + { + // If we see anything private, then the symbol is private. + case Accessibility.NotApplicable: + case Accessibility.Private: + return false; + + // If we see anything internal, then knock it down from public to + // internal. + case Accessibility.Internal: + case Accessibility.ProtectedAndInternal: + return false; + } + + symbol = symbol.ContainingSymbol; + } + + return true; + } + + public static bool ImplementsPublicInterface(this IMethodSymbol method) + { + foreach (var iface in method.ContainingType.AllInterfaces) + { + if (iface.IsExternallyVisible()) + { + foreach (var member in iface.GetMembers().OfType()) + { + var impl = method.ContainingType.FindImplementationForInterfaceMember(member); + if (SymbolEqualityComparer.Default.Equals(impl, method)) + { + return true; + } + } + } + } + + return false; + } + + /// + /// Checks if a symbol has the queried fully qualified name. + /// + /// The symbol to check. + /// The fully qualified name to check against. + /// True if the symbol has the provided fully qualified name, false otherwise. + public static bool HasFullyQualifiedName(this ISymbol symbol, string fullyQualifiedName) + { + if (symbol is not null) + { + var actualSymbolFullName = symbol.ToDisplayString(); + return actualSymbolFullName.Equals(fullyQualifiedName, System.StringComparison.Ordinal); + } + + return false; + } + + /// + /// Checks if a type has the specified base type. + /// + /// The type being checked. + /// The fully qualified name of the base type to look for. + /// True if the type has the specified base type, false otherwise. + public static bool InheritsFromType(this ITypeSymbol type, string baseTypeFullName) + { + if (type is not null) + { + while (type.BaseType != null) + { + var actualBaseTypeFullName = string.Concat(type.BaseType.ContainingNamespace, ".", type.BaseType.Name); + if (actualBaseTypeFullName.Equals(baseTypeFullName, System.StringComparison.Ordinal)) + { + return true; + } + + type = type.BaseType; + } + } + + return false; + } + + public static bool HasAttribute(this ISymbol sym, INamedTypeSymbol attribute) + { + foreach (var a in sym.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(a.AttributeClass, attribute)) + { + return true; + } + } + + return false; + } + + public static bool IsTopLevelStatementsEntryPointMethod(this IMethodSymbol? methodSymbol) + => methodSymbol?.IsStatic == true && methodSymbol.Name switch + { + "$Main" => true, + "
$" => true, + _ => false + }; + + public static bool IsContaminated(this ISymbol symbol, INamedTypeSymbol? contaminationAttribute) + { + return (contaminationAttribute != null) && IsContaminated(symbol); + + bool IsContaminated(ISymbol symbol) + { + if (symbol.HasAttribute(contaminationAttribute)) + { + // symbol is annotated + return true; + } + + if (symbol.ContainingAssembly != null + && symbol.ContainingAssembly.HasAttribute(contaminationAttribute)) + { + // symbol's assembly is annotated + return true; + } + + var container = symbol.ContainingType; + while (container != null) + { + if (IsContaminated(container)) + { + // symbol's container is annotated + return true; + } + + container = container.ContainingType; + } + + if (symbol is INamedTypeSymbol type) + { + var baseType = type.BaseType; + while (baseType != null) + { + if (IsContaminated(baseType)) + { + // symbol's base type is annotated + return true; + } + + baseType = baseType.BaseType; + } + } + + return false; + } + } + + internal static ITypeSymbol? GetFieldOrPropertyType(this ISymbol symbol) + { + if (symbol is IFieldSymbol fieldSymbol) + { + return fieldSymbol.Type; + } + else if (symbol is IPropertySymbol propertySymbol) + { + return propertySymbol.Type; + } + else + { + return null; + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/SyntaxEditorExtensions.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/SyntaxEditorExtensions.cs new file mode 100644 index 0000000000..509a8e54e8 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/SyntaxEditorExtensions.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; + +namespace Microsoft.Extensions.LocalAnalyzers.Utilities; + +/// +/// Class contains extensions. +/// +internal static class SyntaxEditorExtensions +{ + /// + /// Tries to add using directive. + /// + /// The syntax editor. + /// The namespace name. + public static void TryAddUsingDirective(this SyntaxEditor editor, NameSyntax namespaceName) + { + if (editor.GetChangedRoot() is CompilationUnitSyntax documentRoot) + { + var anyUsings = documentRoot.Usings.Any(u => u.Name.GetText().ToString().Equals(namespaceName.ToString(), StringComparison.Ordinal)); + var usingDirective = SyntaxFactory.UsingDirective(namespaceName); + documentRoot = anyUsings ? documentRoot : documentRoot.AddUsings(usingDirective).WithAdditionalAnnotations(Formatter.Annotation); + editor.ReplaceNode(editor.OriginalRoot, documentRoot); + } + } +} diff --git a/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/SyntaxNodeExtensions.cs b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/SyntaxNodeExtensions.cs new file mode 100644 index 0000000000..54861717a2 --- /dev/null +++ b/src/Analyzers/Microsoft.Extensions.LocalAnalyzers/Utilities/SyntaxNodeExtensions.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Extensions.LocalAnalyzers.Utilities; + +/// +/// Class contains extensions. +/// +internal static class SyntaxNodeExtensions +{ + /// + /// Finds closest ancestor by syntax kind. + /// + /// Start node. + /// Kind to search by. + /// Found node or null. + public static SyntaxNode? GetFirstAncestorOfSyntaxKind(this SyntaxNode node, SyntaxKind kind) + { + var n = node.Parent; + while (n != null && !n.IsKind(kind)) + { + n = n.Parent; + } + + return n; + } + + /// + /// Checks node is invocation expression with specified name. + /// + /// Node to check. + /// Semantic model. + /// Expected full method names. + /// Check result. + public static bool NodeHasSpecifiedMethod( + this SyntaxNode? node, + SemanticModel semanticModel, + ICollection expectedFullMethodNames) + { + if (node is InvocationExpressionSyntax invocationExpression) + { + var memberSymbol = semanticModel.GetSymbolInfo(invocationExpression.Expression).Symbol as IMethodSymbol; + if (memberSymbol == null) + { + return false; + } + + var result = false; + if (memberSymbol.ReducedFrom != null) + { + var fullMethodName = memberSymbol.ReducedFrom.ToString(); + result = expectedFullMethodNames.Contains(fullMethodName); + } + + if (!result) + { + var fullMethodName = memberSymbol.OriginalDefinition.ToString(); + return expectedFullMethodNames.Contains(fullMethodName); + } + + return result; + } + + return false; + } + + /// + /// Returns invocation expression name. + /// + /// The invocation expression. + /// The expression syntax name. + public static SimpleNameSyntax? GetExpressionName(this InvocationExpressionSyntax invocationExpression) + { + if (invocationExpression.Expression is MemberAccessExpressionSyntax memberExpression) + { + return memberExpression.Name; + } + + if (invocationExpression.Expression is MemberBindingExpressionSyntax memberBindingExpression) + { + return memberBindingExpression.Name; + } + + return null; + } + + /// + /// Looks for a invocation node in a tree with a specified root type. + /// + /// Node to start traversing. + /// The semantic model. + /// Expected full method names. + /// Root node types. + /// Found invocation node or null. + public static SyntaxNode? FindNodeInTreeUpToSpecifiedParentByMethodName( + this SyntaxNode nodeToStart, + SemanticModel semanticModel, + ICollection expectedFullMethodNames, + ICollection typesToStopTraversing) + { + var currentNode = nodeToStart; + do + { + var foundNode = currentNode.DescendantNodesAndSelf() + .FirstOrDefault(n => n.NodeHasSpecifiedMethod(semanticModel, expectedFullMethodNames)); + if (foundNode != null) + { + return foundNode; + } + + currentNode = currentNode.Parent; + } + while (currentNode != null && !typesToStopTraversing.Contains(currentNode.GetType())); + + return currentNode? + .DescendantNodesAndSelf() + .FirstOrDefault(n => n.NodeHasSpecifiedMethod(semanticModel, expectedFullMethodNames)); + } + + /// + /// Checks has expected name. + /// + /// Expression syntax to check. + /// Expected name. + /// if the identifier text is equal to expected name; otherwise, . + public static bool IdentifierNameEquals(this ExpressionSyntax expression, string expectedName) + { + return expression is IdentifierNameSyntax id && id.Identifier.Text == expectedName; + } +} diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000000..b02b401762 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,19 @@ + + + true + + + + + + $(MSBuildProjectFullPath).$([System.Guid]::NewGuid().ToString().Substring(0,8)).sarif + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets new file mode 100644 index 0000000000..0e1ab38465 --- /dev/null +++ b/src/Directory.Build.targets @@ -0,0 +1,13 @@ + + + + + $(PackageTags);$(Category) + + + + + $(PkgMicrosoft_M365_Internal_Security_SecurityTooling_SDLToolingConfig)\content\SDLToolingConfig\Sdl-ProdSec-Roslyn.ruleset + + + diff --git a/src/Generators/Directory.Build.props b/src/Generators/Directory.Build.props new file mode 100644 index 0000000000..a7bbe98d97 --- /dev/null +++ b/src/Generators/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + netstandard2.0 + n/a + + diff --git a/src/Generators/Directory.Build.targets b/src/Generators/Directory.Build.targets new file mode 100644 index 0000000000..88cbdae86d --- /dev/null +++ b/src/Generators/Directory.Build.targets @@ -0,0 +1,8 @@ + + + + + false + false + + diff --git a/src/Generators/Microsoft.Gen.AutoClient/Common/DiagDescriptors.cs b/src/Generators/Microsoft.Gen.AutoClient/Common/DiagDescriptors.cs new file mode 100644 index 0000000000..69d8e5a415 --- /dev/null +++ b/src/Generators/Microsoft.Gen.AutoClient/Common/DiagDescriptors.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.AutoClient; + +internal sealed class DiagDescriptors : DiagDescriptorsBase +{ + private const string Category = "Design"; + + public static DiagnosticDescriptor ErrorClientMustNotBeNested { get; } = Make( + id: "R9G301", + title: Resources.ErrorClientMustNotBeNestedTitle, + messageFormat: Resources.ErrorClientMustNotBeNestedMessage, + category: Category); + + public static DiagnosticDescriptor WarningRestClientWithoutRestMethods { get; } = Make( + id: "R9G302", + title: Resources.WarningRestClientWithoutRestMethodsTitle, + messageFormat: Resources.WarningRestClientWithoutRestMethodsMessage, + category: Category, + defaultSeverity: DiagnosticSeverity.Warning); + + public static DiagnosticDescriptor ErrorApiMethodMoreThanOneAttribute { get; } = Make( + id: "R9G303", + title: Resources.ErrorApiMethodMoreThanOneAttributeTitle, + messageFormat: Resources.ErrorApiMethodMoreThanOneAttributeMessage, + category: Category); + + public static DiagnosticDescriptor ErrorInvalidReturnType { get; } = Make( + id: "R9G304", + title: Resources.ErrorInvalidReturnTypeTitle, + messageFormat: Resources.ErrorInvalidReturnTypeMessage, + category: Category); + + public static DiagnosticDescriptor ErrorMethodIsGeneric { get; } = Make( + id: "R9G305", + title: Resources.ErrorMethodIsGenericTitle, + messageFormat: Resources.ErrorMethodIsGenericMessage, + category: Category); + + public static DiagnosticDescriptor ErrorUnsupportedMethodBody { get; } = Make( + id: "R9G306", + title: Resources.ErrorUnsupportedMethodBodyTitle, + messageFormat: Resources.ErrorUnsupportedMethodBodyMessage, + category: Category); + + public static DiagnosticDescriptor ErrorStaticMethod { get; } = Make( + id: "R9G307", + title: Resources.ErrorStaticMethodTitle, + messageFormat: Resources.ErrorStaticMethodMessage, + category: Category); + + public static DiagnosticDescriptor ErrorInvalidMethodName { get; } = Make( + id: "R9G308", + title: Resources.ErrorInvalidMethodNameTitle, + messageFormat: Resources.ErrorInvalidMethodNameMessage, + category: Category); + + public static DiagnosticDescriptor ErrorInvalidParameterName { get; } = Make( + id: "R9G309", + title: Resources.ErrorInvalidParameterNameTitle, + messageFormat: Resources.ErrorInvalidParameterNameMessage, + category: Category); + + public static DiagnosticDescriptor ErrorMissingMethodAttribute { get; } = Make( + id: "R9G310", + title: Resources.ErrorMissingMethodAttributeTitle, + messageFormat: Resources.ErrorMissingMethodAttributeMessage, + category: Category); + + public static DiagnosticDescriptor ErrorInterfaceIsGeneric { get; } = Make( + id: "R9G311", + title: Resources.ErrorInterfaceIsGenericTitle, + messageFormat: Resources.ErrorInterfaceIsGenericMessage, + category: Category); + + public static DiagnosticDescriptor ErrorInterfaceName { get; } = Make( + id: "R9G312", + title: Resources.ErrorInterfaceNameTitle, + messageFormat: Resources.ErrorInterfaceNameMessage, + category: Category); + + public static DiagnosticDescriptor ErrorDuplicateBody { get; } = Make( + id: "R9G313", + title: Resources.ErrorDuplicateBodyTitle, + messageFormat: Resources.ErrorDuplicateBodyMessage, + category: Category); + + public static DiagnosticDescriptor ErrorMissingParameterUrl { get; } = Make( + id: "R9G314", + title: Resources.ErrorMissingParameterUrlTitle, + messageFormat: Resources.ErrorMissingParameterUrlMessage, + category: Category); + + public static DiagnosticDescriptor ErrorDuplicateCancellationToken { get; } = Make( + id: "R9G315", + title: Resources.ErrorDuplicateCancellationTokenTitle, + messageFormat: Resources.ErrorDuplicateCancellationTokenMessage, + category: Category); + + public static DiagnosticDescriptor ErrorMissingCancellationToken { get; } = Make( + id: "R9G315", + title: Resources.ErrorMissingCancellationTokenTitle, + messageFormat: Resources.ErrorMissingCancellationTokenMessage, + category: Category); + + public static DiagnosticDescriptor ErrorPathWithQuery { get; } = Make( + id: "R9G316", + title: Resources.ErrorPathWithQueryTitle, + messageFormat: Resources.ErrorPathWithQueryMessage, + category: Category); + + public static DiagnosticDescriptor ErrorDuplicateRequestName { get; } = Make( + id: "R9G317", + title: Resources.ErrorDuplicateRequestNameTitle, + messageFormat: Resources.ErrorDuplicateRequestNameMessage, + category: Category); +} diff --git a/src/Generators/Microsoft.Gen.AutoClient/Common/Emitter.cs b/src/Generators/Microsoft.Gen.AutoClient/Common/Emitter.cs new file mode 100644 index 0000000000..19e512eb2b --- /dev/null +++ b/src/Generators/Microsoft.Gen.AutoClient/Common/Emitter.cs @@ -0,0 +1,434 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using Microsoft.Gen.AutoClient.Model; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.AutoClient; + +// Stryker disable all + +internal sealed class Emitter : EmitterBase +{ + private const string IServiceCollection = "global::Microsoft.Extensions.DependencyInjection.IServiceCollection"; + private const string Task = "global::System.Threading.Tasks.Task"; + private const string HttpMethod = "global::System.Net.Http.HttpMethod"; + private const string HttpRequestMessage = "global::System.Net.Http.HttpRequestMessage"; + private const string HttpResponseMessage = "global::System.Net.Http.HttpResponseMessage"; + private const string Uri = "global::System.Uri"; + private const string StringContent = "global::System.Net.Http.StringContent"; + private const string Encoding = "global::System.Text.Encoding"; + private const string RequestMetadata = "global::Microsoft.Extensions.Http.Telemetry.RequestMetadata"; + private const string TelemetryExtensions = "global::Microsoft.Extensions.Telemetry.TelemetryExtensions"; + private const string RestApiException = "global::Microsoft.Extensions.Http.AutoClient.AutoClientException"; + private const string HttpClient = "global::System.Net.Http.HttpClient"; + private const string IHttpClientFactory = "global::System.Net.Http.IHttpClientFactory"; + private const string CancellationToken = "global::System.Threading.CancellationToken"; + private const string RestApiClientOptions = "global::Microsoft.Extensions.Http.AutoClient.AutoClientOptions"; + private const string Action = "global::System.Action"; + private const string OptionsBuilderExtensions = "global::Microsoft.Extensions.Options.Validation.OptionsBuilderExtensions"; + private const string IOptionsMonitor = "global::Microsoft.Extensions.Options.IOptionsMonitor"; + private const string ServiceProviderServiceExtensions = "global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions"; + private const string MediaTypeHeaderValue = "global::System.Net.Http.Headers.MediaTypeHeaderValue"; + private const string HttpContentJsonExtensions = "global::System.Net.Http.Json.HttpContentJsonExtensions"; + private const string JsonContent = "global::System.Net.Http.Json.JsonContent"; + private const string ServiceCollectionDescriptorExtensions = "global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions"; + private const string RestApiHttpError = "global::Microsoft.Extensions.Http.AutoClient.AutoClientHttpError"; + private const string Invariant = "global::System.FormattableString.Invariant"; + private const string UriKind = "global::System.UriKind"; + + public string EmitRestApis(IReadOnlyList restApiTypes, CancellationToken cancellationToken) + { + Dictionary> metricClassesDict = new(); + foreach (var cl in restApiTypes) + { + if (!metricClassesDict.TryGetValue(cl.Namespace, out var list)) + { + list = new List(); + metricClassesDict.Add(cl.Namespace, list); + } + + list.Add(cl); + } + + foreach (var entry in metricClassesDict.OrderBy(static x => x.Key)) + { + cancellationToken.ThrowIfCancellationRequested(); + GenTypeByNamespace(entry.Key, entry.Value, cancellationToken); + } + + return Capture(); + } + + private static string GetPathTemplate(RestApiMethod restApiMethod) + { + var pathTemplateSb = new StringBuilder(restApiMethod.Path); + + var firstQuery = true; + foreach (var param in restApiMethod.AllParameters.Where(m => m.IsQuery)) + { + if (firstQuery) + { + _ = pathTemplateSb.Append($"?{param.QueryKey}={{{param.QueryKey}}}"); + } + else + { + _ = pathTemplateSb.Append($"&{param.QueryKey}={{{param.QueryKey}}}"); + } + + firstQuery = false; + } + + return pathTemplateSb.ToString(); + } + + private void GenTypeByNamespace(string nspace, IEnumerable restApiTypes, CancellationToken cancellationToken) + { + OutLn(); + if (!string.IsNullOrWhiteSpace(nspace)) + { + OutLn($"namespace {nspace}"); + OutOpenBrace(); + } + + foreach (var restApiClass in restApiTypes.OrderBy(static x => x.Name)) + { + cancellationToken.ThrowIfCancellationRequested(); + GenType(restApiClass); + } + + EmitExtensions(restApiTypes); + + if (!string.IsNullOrWhiteSpace(nspace)) + { + OutCloseBrace(); + } + + OutLn(); + } + + private void EmitExtensions(IEnumerable restApiTypes) + { + OutGeneratedCodeAttribute(); + OutLn("public static class AutoClientsExtensions"); + OutOpenBrace(); + + foreach (var restApiType in restApiTypes.OrderBy(static x => x.Namespace + "." + x.Name)) + { + OutLn(@$"public static {IServiceCollection} Add{restApiType.Name}(this {IServiceCollection} services)"); + OutOpenBrace(); + OutLn($"return services.Add{restApiType.Name}(_ => {{ }});"); + OutCloseBrace(); + OutLn(); + + OutLn(@$"public static {IServiceCollection} Add{restApiType.Name}( + this {IServiceCollection} services, + {Action}<{RestApiClientOptions}> configureOptions)"); + OutOpenBrace(); + OutLn(@$"{OptionsBuilderExtensions}.AddValidatedOptions<{RestApiClientOptions}>(services, ""{restApiType.Name}"").Configure(configureOptions);"); + OutLn($"{ServiceCollectionDescriptorExtensions}.TryAddSingleton(services, provider =>"); + OutOpenBrace(); + OutLn(@$"var httpClient = {ServiceProviderServiceExtensions}.GetRequiredService<{IHttpClientFactory}>(provider).CreateClient(""{restApiType.HttpClientName}"");"); + OutLn(@$"var restAutoClientOptions = {ServiceProviderServiceExtensions}.GetRequiredService<{IOptionsMonitor}<{RestApiClientOptions}>>(provider).Get(""{restApiType.Name}"");"); + OutLn($"return new {restApiType.Name}(httpClient, restAutoClientOptions);"); + OutCloseBraceWithExtra(");"); + OutLn($"return services;"); + OutCloseBrace(); + OutLn(); + } + + OutCloseBrace(); + } + + private void GenType(RestApiType restApiType) + { + GetRestApiMethods(restApiType); + OutLn(); + } + + private void GetRestApiMethods(RestApiType restApiType) + { + OutGeneratedCodeAttribute(); + + OutLn($"{restApiType.Modifiers} {restApiType.Keyword} {restApiType.Name} {restApiType.Constraints} : I{restApiType.Name}"); + OutOpenBrace(); + + EmitClassVariablesAndConstructor(restApiType); + + var dependencyName = restApiType.DependencyName; + + foreach (var restApiMethod in restApiType.Methods.OrderBy(static x => x.MethodName)) + { + GetRestApiMethod(restApiMethod, restApiType, dependencyName); + } + + EmitSendRequestMethod(); + + OutCloseBrace(); + } + + private void GetRestApiMethod(RestApiMethod restApiMethod, RestApiType restApiType, string dependencyName) + { + string? ctParameter = null; + + OutLn(); + OutIndent(); + Out($"public async {Task}<{restApiMethod.ReturnType}> {restApiMethod.MethodName}("); + foreach (var p in restApiMethod.AllParameters) + { + if (p != restApiMethod.AllParameters[0]) + { + Out(", "); + } + + Out($"{p.Type} {p.Name}"); + + if (p.IsCancellationToken) + { + ctParameter = p.Name; + } + } + + Out(")"); + OutLn(); + OutOpenBrace(); + + var pathSb = new StringBuilder(restApiMethod.Path); + + var firstQuery = true; + foreach (var param in restApiMethod.AllParameters.Where(m => m.IsQuery)) + { + if (firstQuery) + { + _ = pathSb.Append($"?{param.QueryKey}={{{param.Name}}}"); + } + else + { + _ = pathSb.Append($"&{param.QueryKey}={{{param.Name}}}"); + } + + firstQuery = false; + } + + var definePath = restApiMethod.FormatParameters.Count > 0 || !firstQuery; + if (definePath) + { + OutLn(@$"var _path = {Invariant}($""{pathSb}"");"); + OutLn(); + } + + var body = restApiMethod.AllParameters.FirstOrDefault(m => m.IsBody); + + var requestName = restApiMethod.RequestName; + + OutLn($"var _httpRequestMessage = new {HttpRequestMessage}()"); + OutOpenBrace(); + if (restApiMethod.HttpMethod == "Patch") + { + OutPP("#if NETCOREAPP2_1_OR_GREATER"); + OutLn($"Method = {HttpMethod}.{restApiMethod.HttpMethod},"); + OutPP("#else"); + OutLn(@$"Method = new {HttpMethod}(""PATCH""),"); + OutPP("#endif"); + } + else + { + OutLn($"Method = {HttpMethod}.{restApiMethod.HttpMethod},"); + } + + if (definePath) + { + OutLn($"RequestUri = new {Uri}(_path, {UriKind}.Relative),"); + } + else + { + OutLn($"RequestUri = _uri{requestName},"); + } + + OutCloseBraceWithExtra(";"); + + OutLn(); + + if (body != null) + { + switch (body.BodyType) + { + case BodyContentTypeParam.ApplicationJson: + OutLn($@"_httpRequestMessage.Content = {JsonContent}.Create({body.Name}, _applicationJsonHeader, _restAutoClientOptions.JsonSerializerOptions);"); + OutLn(); + break; + + case BodyContentTypeParam.TextPlain: + OutLn(@$"string _payload = {body.Name}.ToString() ?? """";"); + OutPP("#if NET7_0_OR_GREATER"); + OutLn($@"_httpRequestMessage.Content = new {StringContent}(_payload, {Encoding}.UTF8, _textPlainHeader);"); + OutPP("#else"); + OutLn($@"_httpRequestMessage.Content = new {StringContent}(_payload, {Encoding}.UTF8, _textPlainHeader.MediaType);"); + OutPP("#endif"); + OutLn(); + break; + } + } + + OutLn("try"); + OutOpenBrace(); + + OutLn($"{TelemetryExtensions}.SetRequestMetadata(_httpRequestMessage, _requestMetadata{requestName});"); + + foreach (var header in restApiType.StaticHeaders.OrderBy(static h => h.Key)) + { + OutLn(@$"_httpRequestMessage.Headers.Add(""{header.Key}"", ""{header.Value}"");"); + } + + foreach (var header in restApiMethod.StaticHeaders.OrderBy(static h => h.Key)) + { + OutLn(@$"_httpRequestMessage.Headers.Add(""{header.Key}"", ""{header.Value}"");"); + } + + foreach (var param in restApiMethod.AllParameters.Where(m => m.IsHeader)) + { + OutLn(@$"_httpRequestMessage.Headers.Add(""{param.HeaderName}"", {param.Name}?.ToString() ?? """");"); + } + + OutLn(); + OutLn(@$"var _response = await SendRequest<{restApiMethod.ReturnType}>(""{dependencyName}"", _requestMetadata{requestName}.RequestRoute, _httpRequestMessage, {ctParameter ?? "default"}) + .ConfigureAwait(false);"); + + OutLn($"return _response;"); + OutCloseBrace(); + OutLn("finally"); + OutOpenBrace(); + OutLn("_httpRequestMessage.Dispose();"); + OutCloseBrace(); + + OutCloseBrace(); + } + + private void EmitSendRequestMethod() + { + OutLn(); + + OutLn(@$"private async {Task} SendRequest( + string dependencyName, + string path, + {HttpRequestMessage} httpRequestMessage, + {CancellationToken} cancellationToken)"); + + Indent(); + OutLn("where TResponse : class"); + Unindent(); + OutOpenBrace(); + + OutLn(); + OutLn("var response = await _httpClient.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false);"); + OutLn(); + + OutLn($"if (typeof(TResponse) == typeof({HttpResponseMessage}))"); + OutOpenBrace(); + OutLn("return (response as TResponse)!;"); + OutCloseBrace(); + + OutLn(); + OutLn("try"); + OutOpenBrace(); + + OutLn("if (!response.IsSuccessStatusCode)"); + OutOpenBrace(); + OutLn($"var error = await {RestApiHttpError}.CreateAsync(response, cancellationToken).ConfigureAwait(false);"); + OutLn($@"throw new {RestApiException}({Invariant}($""The '{{dependencyName}}' HTTP client failed with '{{response.StatusCode}}' status code.""), path, error);"); + OutCloseBrace(); + + OutLn(); + OutLn(@"if (typeof(TResponse) == typeof(string))"); + OutOpenBrace(); + OutPP("#if NET5_0_OR_GREATER"); + OutLn($"var rawContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);"); + OutPP("#else"); + OutLn("cancellationToken.ThrowIfCancellationRequested();"); + OutLn($"var rawContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);"); + OutPP("#endif"); + OutLn(); + OutLn("return (rawContent as TResponse)!;"); + OutCloseBrace(); + + OutLn(); + OutLn("var mediaType = response.Content.Headers.ContentType?.MediaType;"); + OutLn(@"if (mediaType == ""application/json"")"); + OutOpenBrace(); + OutLn(@$"var deserializedResponse = await {HttpContentJsonExtensions}.ReadFromJsonAsync(response.Content, _restAutoClientOptions.JsonSerializerOptions, cancellationToken) + .ConfigureAwait(false);"); + OutLn("if (deserializedResponse == null)"); + OutOpenBrace(); + OutLn($"var error = await {RestApiHttpError}.CreateAsync(response, cancellationToken).ConfigureAwait(false);"); + OutLn($@"throw new {RestApiException}({Invariant}($""The '{{dependencyName}}' REST API failed to deserialize response.""), path, error);"); + OutCloseBrace(); + OutLn(); + OutLn("return deserializedResponse;"); + OutCloseBrace(); + + OutLn(); + OutLn($"var err = await {RestApiHttpError}.CreateAsync(response, cancellationToken).ConfigureAwait(false);"); + OutLn(@$"throw new {RestApiException}({Invariant}($""The '{{dependencyName}}' REST API returned an unsupported content type ('{{mediaType}}').""), path, err);"); + + OutLn(); + OutCloseBrace(); + OutLn("finally"); + OutOpenBrace(); + OutLn("response.Dispose();"); + OutCloseBrace(); + + OutCloseBrace(); + } + + private void EmitClassVariablesAndConstructor(RestApiType restApiType) + { + OutLn(@$"private static readonly {MediaTypeHeaderValue} _applicationJsonHeader = new(""application/json"")"); + OutOpenBrace(); + OutLn($@"CharSet = {Encoding}.UTF8.WebName"); + OutCloseBraceWithExtra(";"); + OutLn(); + + OutLn(@$"private static readonly {MediaTypeHeaderValue} _textPlainHeader = new(""text/plain"")"); + OutOpenBrace(); + OutLn($@"CharSet = {Encoding}.UTF8.WebName"); + OutCloseBraceWithExtra(";"); + + OutLn(); + var simpleMethods = restApiType.Methods.Where(m => m.FormatParameters.Count == 0 && !m.AllParameters.Any(p => p.IsQuery)); + var dependencyName = restApiType.DependencyName; + foreach (var restApiMethod in restApiType.Methods.OrderBy(static x => x.MethodName)) + { + var requestName = restApiMethod.RequestName; + var path = GetPathTemplate(restApiMethod); + + if (restApiMethod.FormatParameters.Count == 0 && !restApiMethod.AllParameters.Any(p => p.IsQuery)) + { + OutLn(@$"private static readonly {Uri} _uri{requestName} = new(""{restApiMethod.Path}"", {UriKind}.Relative);"); + } + + OutLn(@$"private static readonly {RequestMetadata} _requestMetadata{requestName} = new()"); + OutOpenBrace(); + OutLn(@$"DependencyName = ""{dependencyName}"","); + OutLn(@$"RequestName = ""{requestName}"","); + OutLn(@$"RequestRoute = ""{path}"""); + OutCloseBraceWithExtra(";"); + + OutLn(); + } + + OutLn($"private readonly {HttpClient} _httpClient;"); + OutLn($"private readonly {RestApiClientOptions} _restAutoClientOptions;"); + OutLn(); + + OutLn($"public {restApiType.Name}({HttpClient} httpClient, {RestApiClientOptions} restAutoClientOptions)"); + OutOpenBrace(); + OutLn(@$"_httpClient = httpClient;"); + OutLn($"_restAutoClientOptions = restAutoClientOptions;"); + OutCloseBrace(); + } +} diff --git a/src/Generators/Microsoft.Gen.AutoClient/Common/Generator.cs b/src/Generators/Microsoft.Gen.AutoClient/Common/Generator.cs new file mode 100644 index 0000000000..8ecea9ee42 --- /dev/null +++ b/src/Generators/Microsoft.Gen.AutoClient/Common/Generator.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if ROSLYN_4_0_OR_GREATER + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.AutoClient; + +[Generator] +[ExcludeFromCodeCoverage] +public class Generator : IIncrementalGenerator +{ + private static readonly HashSet _attributeNames = new() + { + SymbolLoader.RestApiAttribute, + + SymbolLoader.RestGetAttribute, + SymbolLoader.RestPostAttribute, + SymbolLoader.RestPutAttribute, + SymbolLoader.RestDeleteAttribute, + SymbolLoader.RestPatchAttribute, + SymbolLoader.RestOptionsAttribute, + SymbolLoader.RestHeadAttribute, + + SymbolLoader.RestStaticHeaderAttribute, + SymbolLoader.RestHeaderAttribute, + SymbolLoader.RestQueryAttribute, + SymbolLoader.RestBodyAttribute, + SymbolLoader.RestRequestNameAttribute + }; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + GeneratorUtilities.Initialize(context, _attributeNames, HandleAnnotatedTypes); + } + + private static void HandleAnnotatedTypes(Compilation compilation, IEnumerable nodes, SourceProductionContext context) + { + var p = new Parser(compilation, context.ReportDiagnostic, context.CancellationToken); + + var restApiClasses = p.GetRestApiClasses(nodes.OfType()); + + if (restApiClasses.Count > 0) + { + var emitter = new Emitter(); + + var restApiCode = emitter.EmitRestApis(restApiClasses, context.CancellationToken); + context.AddSource($"AutoClients.g.cs", SourceText.From(restApiCode, Encoding.UTF8)); + } + } +} + +#else + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.AutoClient; + +[Generator] +[ExcludeFromCodeCoverage] +public class Generator : ISourceGenerator +{ + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(TypeDeclarationSyntaxReceiver.Create); + } + + public void Execute(GeneratorExecutionContext context) + { + var receiver = context.SyntaxReceiver as TypeDeclarationSyntaxReceiver; + if (receiver == null || receiver.TypeDeclarations.Count == 0) + { + // nothing to do yet + return; + } + + var parser = new Parser(context.Compilation, context.ReportDiagnostic, context.CancellationToken); + + var restApiClasses = parser.GetRestApiClasses(receiver.TypeDeclarations.OfType()); + + if (restApiClasses.Count > 0) + { + var emitter = new Emitter(); + + var restApiCode = emitter.EmitRestApis(restApiClasses, context.CancellationToken); + context.AddSource($"AutoClients.g.cs", SourceText.From(restApiCode, Encoding.UTF8)); + } + } +} + +#endif diff --git a/src/Generators/Microsoft.Gen.AutoClient/Common/Model/BodyContentTypeParam.cs b/src/Generators/Microsoft.Gen.AutoClient/Common/Model/BodyContentTypeParam.cs new file mode 100644 index 0000000000..7f493ed569 --- /dev/null +++ b/src/Generators/Microsoft.Gen.AutoClient/Common/Model/BodyContentTypeParam.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Gen.AutoClient.Model; + +internal enum BodyContentTypeParam +{ + ApplicationJson, + TextPlain +} diff --git a/src/Generators/Microsoft.Gen.AutoClient/Common/Model/BodyContentTypeParamExtensions.cs b/src/Generators/Microsoft.Gen.AutoClient/Common/Model/BodyContentTypeParamExtensions.cs new file mode 100644 index 0000000000..47e2441f8f --- /dev/null +++ b/src/Generators/Microsoft.Gen.AutoClient/Common/Model/BodyContentTypeParamExtensions.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Gen.AutoClient.Model; + +internal static class BodyContentTypeParamExtensions +{ + public static string ConvertToString(this BodyContentTypeParam? param) + { + return param switch + { + BodyContentTypeParam.ApplicationJson => "application/json", + BodyContentTypeParam.TextPlain => "text/plain", + _ => string.Empty, + }; + } +} diff --git a/src/Generators/Microsoft.Gen.AutoClient/Common/Model/RestApiMethod.cs b/src/Generators/Microsoft.Gen.AutoClient/Common/Model/RestApiMethod.cs new file mode 100644 index 0000000000..2a6da88209 --- /dev/null +++ b/src/Generators/Microsoft.Gen.AutoClient/Common/Model/RestApiMethod.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Gen.AutoClient.Model; + +internal sealed class RestApiMethod +{ + public readonly List AllParameters = new(); + public readonly List FormatParameters = new(); + public string MethodName = string.Empty; + public string? HttpMethod = string.Empty; + public string? Path = string.Empty; + public string? ReturnType = string.Empty; + public string RequestName = string.Empty; + public Dictionary StaticHeaders = new(); +} diff --git a/src/Generators/Microsoft.Gen.AutoClient/Common/Model/RestApiMethodParameter.cs b/src/Generators/Microsoft.Gen.AutoClient/Common/Model/RestApiMethodParameter.cs new file mode 100644 index 0000000000..ac48c3ad8d --- /dev/null +++ b/src/Generators/Microsoft.Gen.AutoClient/Common/Model/RestApiMethodParameter.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Gen.AutoClient.Model; + +internal sealed class RestApiMethodParameter +{ + public string Name = string.Empty; + public string Type = string.Empty; + public string? HeaderName; + public string? QueryKey; + public BodyContentTypeParam? BodyType; + public bool IsCancellationToken; + + public bool IsHeader => HeaderName != null; + public bool IsQuery => QueryKey != null; + public bool IsBody => BodyType != null; +} diff --git a/src/Generators/Microsoft.Gen.AutoClient/Common/Model/RestApiType.cs b/src/Generators/Microsoft.Gen.AutoClient/Common/Model/RestApiType.cs new file mode 100644 index 0000000000..ea9a5ee37d --- /dev/null +++ b/src/Generators/Microsoft.Gen.AutoClient/Common/Model/RestApiType.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Gen.AutoClient.Model; + +internal sealed class RestApiType +{ + public readonly List Methods = new(); + public string Namespace = string.Empty; + public string Name = string.Empty; + public string Constraints = string.Empty; + public string Modifiers = string.Empty; + public string Keyword = string.Empty; + public string HttpClientName = string.Empty; + public Dictionary StaticHeaders = new(); + public string DependencyName = string.Empty; +} diff --git a/src/Generators/Microsoft.Gen.AutoClient/Common/Parser.cs b/src/Generators/Microsoft.Gen.AutoClient/Common/Parser.cs new file mode 100644 index 0000000000..a77e863844 --- /dev/null +++ b/src/Generators/Microsoft.Gen.AutoClient/Common/Parser.cs @@ -0,0 +1,643 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Gen.AutoClient.Model; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.AutoClient; + +internal sealed class Parser +{ + private const string ReturnTypePrefix = "System.Threading.Tasks.Task<"; + + private static readonly string[] _dependencyNameTrimEndings = new[] { "Api", "Client" }; + private static readonly string[] _requestNameTrimEndings = new[] { "Async" }; + + private readonly CancellationToken _cancellationToken; + private readonly Compilation _compilation; + private readonly Action _reportDiagnostic; + + private readonly SymbolDisplayFormat _globalDisplayFormat = SymbolDisplayFormat + .FullyQualifiedFormat + .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Included); + + public Parser(Compilation compilation, Action reportDiagnostic, CancellationToken cancellationToken) + { + _compilation = compilation; + _cancellationToken = cancellationToken; + _reportDiagnostic = reportDiagnostic; + } + + public IReadOnlyList GetRestApiClasses(IEnumerable types) + { + var symbols = SymbolLoader.LoadSymbols(_compilation); + if (symbols == null) + { + return Array.Empty(); + } + + var results = new List(); + + foreach (var typeDeclarationGroup in types.GroupBy(x => x.SyntaxTree)) + { + SemanticModel? semanticModel = null; + foreach (var typeDeclaration in typeDeclarationGroup) + { + // stop if we're asked to + _cancellationToken.ThrowIfCancellationRequested(); + semanticModel ??= _compilation.GetSemanticModel(typeDeclaration.SyntaxTree); + + var classSymbol = semanticModel.GetDeclaredSymbol(typeDeclaration, _cancellationToken); + if (classSymbol == null) + { + continue; + } + + var classAttributes = classSymbol.GetAttributes(); + if (classAttributes.Length == 0) + { + continue; + } + + var attrResult = ParseInterfaceAttributes(classAttributes, symbols); + if (attrResult.HttpClientName == null) + { + continue; + } + + if (typeDeclaration.Arity > 0) + { + Diag(DiagDescriptors.ErrorInterfaceIsGeneric, typeDeclaration.GetLocation()); + continue; + } + + if (typeDeclaration.Identifier.ToString()[0] != 'I') + { + Diag(DiagDescriptors.ErrorInterfaceName, typeDeclaration.GetLocation()); + continue; + } + + if (typeDeclaration.Parent != null && + (typeDeclaration.Parent!.IsKind(SyntaxKind.ClassDeclaration) || + typeDeclaration.Parent!.IsKind(SyntaxKind.StructDeclaration) || + typeDeclaration.Parent!.IsKind(SyntaxKind.RecordDeclaration))) + { + Diag(DiagDescriptors.ErrorClientMustNotBeNested, typeDeclaration.Parent.GetLocation()); + continue; + } + + var nspace = GetNamespace(typeDeclaration); + var className = typeDeclaration.Identifier.ToString().Substring(1); + var restApiType = new RestApiType + { + Namespace = nspace, + Name = className, + Constraints = typeDeclaration.ConstraintClauses.ToString(), + Modifiers = typeDeclaration.Modifiers.ToString(), + Keyword = "class", + HttpClientName = attrResult.HttpClientName, + StaticHeaders = attrResult.StaticHeaders, + DependencyName = attrResult.CustomDependencyName ?? GetDependencyName(className), + }; + + var requestNames = new HashSet(); + + foreach (var memberSyntax in typeDeclaration.Members.Where(x => x.IsKind(SyntaxKind.MethodDeclaration))) + { + var methodSyntax = (MethodDeclarationSyntax)memberSyntax; + var methodSymbol = semanticModel.GetDeclaredSymbol(methodSyntax, _cancellationToken); + if (methodSymbol == null) + { + continue; + } + + var clientMethod = ProcessMethod(methodSymbol, symbols, requestNames); + if (clientMethod == null) + { + continue; + } + + restApiType.Methods.Add(clientMethod); + } + + if (restApiType.Methods.Count == 0) + { + Diag(DiagDescriptors.WarningRestClientWithoutRestMethods, typeDeclaration.GetLocation()); + } + + results.Add(restApiType); + } + } + + return results; + } + + private static string GetDependencyName(string className) + { + return TryRemoveFromEnd(className, _dependencyNameTrimEndings); + } + + private static string GetRequestName(string methodName) + { + return TryRemoveFromEnd(methodName, _requestNameTrimEndings); + } + + private static string TryRemoveFromEnd(string value, string[] endings) + { + foreach (var ending in endings) + { + if (value.EndsWith(ending, StringComparison.Ordinal)) + { + return value.Substring(0, value.Length - ending.Length); + } + } + + return value; + } + + private static ParseParameterAttributesResult ParseParameterAttributes(ImmutableArray attributes, SymbolHolder symbols, string paramName) + { + string? headerName = null; + string? queryKey = null; + BodyContentTypeParam? bodyType = null; + + foreach (var attribute in attributes) + { + var attributeSymbol = attribute.AttributeClass; + if (attributeSymbol == null) + { + continue; + } + + if (attributeSymbol.Equals(symbols.RestHeaderAttribute, SymbolEqualityComparer.Default)) + { + if (attribute.ConstructorArguments.Length != 1) + { + continue; + } + + headerName = attribute.ConstructorArguments[0].Value as string; + } + else if (attributeSymbol.Equals(symbols.RestQueryAttribute, SymbolEqualityComparer.Default)) + { + int argLength = attribute.ConstructorArguments.Length; + if (argLength == 0) + { + queryKey = paramName; + } + else if (argLength == 1) + { + queryKey = attribute.ConstructorArguments[0].Value as string; + } + } + else if (attributeSymbol.Equals(symbols.RestBodyAttribute, SymbolEqualityComparer.Default)) + { + int argLength = attribute.ConstructorArguments.Length; + if (argLength == 0) + { + bodyType = BodyContentTypeParam.ApplicationJson; + } + else if (argLength == 1) + { + var intValue = attribute.ConstructorArguments[0].Value as int?; + if (intValue != null) + { + bodyType = (BodyContentTypeParam)intValue; + } + } + } + } + + return new ParseParameterAttributesResult(headerName, queryKey, bodyType); + } + + private static ParseInterfaceAttributesResult ParseInterfaceAttributes(ImmutableArray classAttributes, SymbolHolder symbols) + { + string? httpClientName = null; + string? customDependencyName = null; + Dictionary staticHeaders = new(); + + foreach (var classAttribute in classAttributes) + { + var attributeSymbol = classAttribute.AttributeClass; + if (attributeSymbol == null) + { + continue; + } + + if (attributeSymbol.Equals(symbols.RestApiAttribute, SymbolEqualityComparer.Default)) + { + if (classAttribute.ConstructorArguments.Length == 1) + { + httpClientName = classAttribute.ConstructorArguments[0].Value as string; + } + else if (classAttribute.ConstructorArguments.Length == 2) + { + httpClientName = classAttribute.ConstructorArguments[0].Value as string; + customDependencyName = classAttribute.ConstructorArguments[1].Value as string; + } + } + else if (attributeSymbol.Equals(symbols.RestStaticHeaderAttribute, SymbolEqualityComparer.Default)) + { + if (classAttribute.ConstructorArguments.Length != 2) + { + continue; + } + + var key = classAttribute.ConstructorArguments[0].Value as string; + var value = classAttribute.ConstructorArguments[1].Value as string; + + if (key == null || value == null) + { + continue; + } + + staticHeaders.Add(key, value); + } + } + + return new(httpClientName, customDependencyName, staticHeaders); + } + + private static string GetNamespace(TypeDeclarationSyntax typeDeclaration) + { + var result = string.Empty; + + // determine the namespace the class is declared in, if any + SyntaxNode? potentialNamespaceParent = typeDeclaration.Parent; + while (potentialNamespaceParent != null && +#if ROSLYN_4_0_OR_GREATER + potentialNamespaceParent is not NamespaceDeclarationSyntax && + potentialNamespaceParent is not FileScopedNamespaceDeclarationSyntax) +#else + potentialNamespaceParent is not NamespaceDeclarationSyntax) +#endif + { + potentialNamespaceParent = potentialNamespaceParent.Parent; + } + +#if ROSLYN_4_0_OR_GREATER + var ns = potentialNamespaceParent as BaseNamespaceDeclarationSyntax; +#else + var ns = potentialNamespaceParent as NamespaceDeclarationSyntax; +#endif + + if (ns != null) + { + result = ns.Name.ToString(); + while (true) + { + ns = ns.Parent as NamespaceDeclarationSyntax; + if (ns == null) + { + break; + } + + result = $"{ns.Name}.{result}"; + } + } + + return result; + } + + private static ParseMethodAttributesResult ParseMethodAttributes(ImmutableArray methodAttributes, SymbolHolder symbols) + { + List httpMethods = new(); + string? requestName = null; + string? path = null; + Dictionary staticHeaders = new(); + + foreach (var methodAttribute in methodAttributes) + { + var attributeSymbol = methodAttribute.AttributeClass; + if (attributeSymbol == null) + { + continue; + } + + if (attributeSymbol.Equals(symbols.RestGetAttribute, SymbolEqualityComparer.Default)) + { + httpMethods.Add("Get"); + + if (methodAttribute.ConstructorArguments.Length != 1) + { + continue; + } + + path = methodAttribute.ConstructorArguments[0].Value as string; + } + else if (attributeSymbol.Equals(symbols.RestPostAttribute, SymbolEqualityComparer.Default)) + { + httpMethods.Add("Post"); + + if (methodAttribute.ConstructorArguments.Length != 1) + { + continue; + } + + path = methodAttribute.ConstructorArguments[0].Value as string; + } + else if (attributeSymbol.Equals(symbols.RestPutAttribute, SymbolEqualityComparer.Default)) + { + httpMethods.Add("Put"); + + if (methodAttribute.ConstructorArguments.Length != 1) + { + continue; + } + + path = methodAttribute.ConstructorArguments[0].Value as string; + } + else if (attributeSymbol.Equals(symbols.RestDeleteAttribute, SymbolEqualityComparer.Default)) + { + httpMethods.Add("Delete"); + + if (methodAttribute.ConstructorArguments.Length != 1) + { + continue; + } + + path = methodAttribute.ConstructorArguments[0].Value as string; + } + else if (attributeSymbol.Equals(symbols.RestPatchAttribute, SymbolEqualityComparer.Default)) + { + httpMethods.Add("Patch"); + + if (methodAttribute.ConstructorArguments.Length != 1) + { + continue; + } + + path = methodAttribute.ConstructorArguments[0].Value as string; + } + else if (attributeSymbol.Equals(symbols.RestOptionsAttribute, SymbolEqualityComparer.Default)) + { + httpMethods.Add("Options"); + + if (methodAttribute.ConstructorArguments.Length != 1) + { + continue; + } + + path = methodAttribute.ConstructorArguments[0].Value as string; + } + else if (attributeSymbol.Equals(symbols.RestHeadAttribute, SymbolEqualityComparer.Default)) + { + httpMethods.Add("Head"); + + if (methodAttribute.ConstructorArguments.Length != 1) + { + continue; + } + + path = methodAttribute.ConstructorArguments[0].Value as string; + } + else if (attributeSymbol.Equals(symbols.RestRequestNameAttribute, SymbolEqualityComparer.Default)) + { + if (methodAttribute.ConstructorArguments.Length != 1) + { + continue; + } + + requestName = methodAttribute.ConstructorArguments[0].Value as string; + } + else if (attributeSymbol.Equals(symbols.RestStaticHeaderAttribute, SymbolEqualityComparer.Default)) + { + if (methodAttribute.ConstructorArguments.Length != 2) + { + continue; + } + + var key = methodAttribute.ConstructorArguments[0].Value as string; + var value = methodAttribute.ConstructorArguments[1].Value as string; + + if (key == null || value == null) + { + continue; + } + + staticHeaders.Add(key, value); + } + } + + return new(httpMethods, path, requestName, staticHeaders); + } + + private RestApiMethod? ProcessMethod( + IMethodSymbol methodSymbol, + SymbolHolder symbols, + HashSet requestNames) + { + var hasErrors = false; + + if (methodSymbol.Name[0] == '_') + { + // can't have method names that start with _ since that can lead to conflicting symbol names + // because the generated symbols start with _ + Diag(DiagDescriptors.ErrorInvalidMethodName, methodSymbol.GetLocation()); + hasErrors = true; + } + + if (methodSymbol.Arity > 0) + { + // we don't currently support generic methods + Diag(DiagDescriptors.ErrorMethodIsGeneric, methodSymbol.GetLocation()); + hasErrors = true; + } + + if (methodSymbol.IsStatic) + { + Diag(DiagDescriptors.ErrorStaticMethod, methodSymbol.GetLocation()); + hasErrors = true; + } + + var methodAttrResult = ParseMethodAttributes(methodSymbol.GetAttributes(), symbols); + if (methodAttrResult.HttpMethods.Count == 0 || methodAttrResult.Path == null) + { + Diag(DiagDescriptors.ErrorMissingMethodAttribute, methodSymbol.GetLocation()); + hasErrors = true; + } + + if (methodAttrResult.HttpMethods.Count > 1) + { + Diag(DiagDescriptors.ErrorApiMethodMoreThanOneAttribute, methodSymbol.GetLocation()); + hasErrors = true; + } + + var returnTypeSymbol = (INamedTypeSymbol)methodSymbol.ReturnType; + ITypeSymbol? innerType = null; + + var returnType = methodSymbol.ReturnType.ToString(); + if (!returnType.StartsWith(ReturnTypePrefix, StringComparison.Ordinal)) + { + Diag(DiagDescriptors.ErrorInvalidReturnType, methodSymbol.GetLocation()); + hasErrors = true; + } + else + { + if (returnTypeSymbol.TypeArguments.Length != 1) + { + Diag(DiagDescriptors.ErrorInvalidReturnType, methodSymbol.GetLocation()); + hasErrors = true; + } + + innerType = returnTypeSymbol.TypeArguments[0]; + if (innerType.NullableAnnotation == NullableAnnotation.Annotated) + { + Diag(DiagDescriptors.ErrorInvalidReturnType, methodSymbol.GetLocation()); + hasErrors = true; + } + } + + if (methodAttrResult.Path != null && methodAttrResult.Path.Contains("?")) + { + Diag(DiagDescriptors.ErrorPathWithQuery, methodSymbol.GetLocation()); + hasErrors = true; + } + + var requestName = methodAttrResult.RequestName ?? GetRequestName(methodSymbol.Name); + + if (!requestNames.Add(requestName)) + { + Diag(DiagDescriptors.ErrorDuplicateRequestName, methodSymbol.GetLocation()); + hasErrors = true; + } + + var restApiMethod = new RestApiMethod + { + HttpMethod = methodAttrResult.HttpMethods.FirstOrDefault(), + MethodName = methodSymbol.Name, + Path = methodAttrResult.Path, + ReturnType = innerType?.ToDisplayString(_globalDisplayFormat), + RequestName = requestName, + StaticHeaders = methodAttrResult.StaticHeaders, + }; + + bool foundBody = false; + bool foundCancellationToken = false; + foreach (var paramSymbol in methodSymbol.Parameters) + { + var paramName = paramSymbol.Name; + if (string.IsNullOrWhiteSpace(paramName)) + { + // semantic problem, just bail quietly + hasErrors = true; + } + + var paramTypeSymbol = paramSymbol.Type; + if (paramTypeSymbol is IErrorTypeSymbol) + { + // semantic problem, just bail quietly + hasErrors = true; + } + + if (paramName[0] == '_') + { + // can't have method parameter names that start with _ since that can lead to conflicting symbol names + // because all generated symbols start with _ + Diag(DiagDescriptors.ErrorInvalidParameterName, paramSymbol.Locations[0]); + hasErrors = true; + } + + var paramAttributes = paramSymbol.GetAttributes(); + var isCancellationToken = paramTypeSymbol.ToString().Contains("System.Threading.CancellationToken"); + + if (isCancellationToken) + { + if (foundCancellationToken) + { + Diag(DiagDescriptors.ErrorDuplicateCancellationToken, paramSymbol.Locations[0], paramName); + hasErrors = true; + } + + foundCancellationToken = true; + } + + if (paramAttributes.IsEmpty && !isCancellationToken) + { + if (restApiMethod.Path == null || !restApiMethod.Path.Contains($"{{{paramName}}}")) + { + Diag(DiagDescriptors.ErrorMissingParameterUrl, paramSymbol.Locations[0], paramName); + hasErrors = true; + } + else + { + restApiMethod.FormatParameters.Add(paramName); + } + } + + var attrResult = ParseParameterAttributes(paramAttributes, symbols, paramName); + + if (attrResult.BodyType != null) + { + if (restApiMethod.HttpMethod == "Get" || restApiMethod.HttpMethod == "Head") + { + Diag(DiagDescriptors.ErrorUnsupportedMethodBody, paramSymbol.Locations[0], restApiMethod.HttpMethod); + hasErrors = true; + } + + if (foundBody) + { + Diag(DiagDescriptors.ErrorDuplicateBody, paramSymbol.Locations[0]); + hasErrors = true; + } + + foundBody = true; + } + + var restApiMethodParameter = new RestApiMethodParameter + { + Name = paramName, + Type = paramTypeSymbol.ToDisplayString(_globalDisplayFormat), + HeaderName = attrResult.HeaderName, + QueryKey = attrResult.QueryKey, + BodyType = attrResult.BodyType, + IsCancellationToken = isCancellationToken + }; + + restApiMethod.AllParameters.Add(restApiMethodParameter); + } + + if (!foundCancellationToken) + { + Diag(DiagDescriptors.ErrorMissingCancellationToken, methodSymbol.Locations[0]); + hasErrors = true; + } + + return hasErrors ? null : restApiMethod; + } + + private void Diag(DiagnosticDescriptor desc, Location? location) + { + _reportDiagnostic(Diagnostic.Create(desc, location, Array.Empty())); + } + + private void Diag(DiagnosticDescriptor desc, Location? location, params object?[]? messageArgs) + { + _reportDiagnostic(Diagnostic.Create(desc, location, messageArgs)); + } + + private sealed record class ParseInterfaceAttributesResult( + string? HttpClientName, + string? CustomDependencyName, + Dictionary StaticHeaders); + + private sealed record class ParseParameterAttributesResult( + string? HeaderName, + string? QueryKey, + BodyContentTypeParam? BodyType); + + private sealed record class ParseMethodAttributesResult( + List HttpMethods, + string? Path, + string? RequestName, + Dictionary StaticHeaders); +} diff --git a/src/Generators/Microsoft.Gen.AutoClient/Common/Resources.Designer.cs b/src/Generators/Microsoft.Gen.AutoClient/Common/Resources.Designer.cs new file mode 100644 index 0000000000..70b6557ea7 --- /dev/null +++ b/src/Generators/Microsoft.Gen.AutoClient/Common/Resources.Designer.cs @@ -0,0 +1,387 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Gen.AutoClient { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Gen.AutoClient.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to An API method must not contain more than one REST method attribute. + /// + internal static string ErrorApiMethodMoreThanOneAttributeMessage { + get { + return ResourceManager.GetString("ErrorApiMethodMoreThanOneAttributeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An API method must not contain more than one REST method attribute. + /// + internal static string ErrorApiMethodMoreThanOneAttributeTitle { + get { + return ResourceManager.GetString("ErrorApiMethodMoreThanOneAttributeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to API client interfaces must not be nested types. + /// + internal static string ErrorClientMustNotBeNestedMessage { + get { + return ResourceManager.GetString("ErrorClientMustNotBeNestedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to API client interfaces must not be nested types. + /// + internal static string ErrorClientMustNotBeNestedTitle { + get { + return ResourceManager.GetString("ErrorClientMustNotBeNestedTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A Body is already defined for this method. + /// + internal static string ErrorDuplicateBodyMessage { + get { + return ResourceManager.GetString("ErrorDuplicateBodyMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Duplicate body attribute. + /// + internal static string ErrorDuplicateBodyTitle { + get { + return ResourceManager.GetString("ErrorDuplicateBodyTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A REST API method must not receive more than one cancellation token. + /// + internal static string ErrorDuplicateCancellationTokenMessage { + get { + return ResourceManager.GetString("ErrorDuplicateCancellationTokenMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to REST API method has more than one cancellation token. + /// + internal static string ErrorDuplicateCancellationTokenTitle { + get { + return ResourceManager.GetString("ErrorDuplicateCancellationTokenTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The request name '{0}' is already in use within this REST API client.. + /// + internal static string ErrorDuplicateRequestNameMessage { + get { + return ResourceManager.GetString("ErrorDuplicateRequestNameMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A REST API method's request name must be unique. + /// + internal static string ErrorDuplicateRequestNameTitle { + get { + return ResourceManager.GetString("ErrorDuplicateRequestNameTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The API interface cannot be generic. + /// + internal static string ErrorInterfaceIsGenericMessage { + get { + return ResourceManager.GetString("ErrorInterfaceIsGenericMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The API interface cannot be generic. + /// + internal static string ErrorInterfaceIsGenericTitle { + get { + return ResourceManager.GetString("ErrorInterfaceIsGenericTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid API interface name. It should start with an 'I'. + /// + internal static string ErrorInterfaceNameMessage { + get { + return ResourceManager.GetString("ErrorInterfaceNameMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid API interface name. + /// + internal static string ErrorInterfaceNameTitle { + get { + return ResourceManager.GetString("ErrorInterfaceNameTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to API method names cannot start with _. + /// + internal static string ErrorInvalidMethodNameMessage { + get { + return ResourceManager.GetString("ErrorInvalidMethodNameMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to API method names can't start with an underscore. + /// + internal static string ErrorInvalidMethodNameTitle { + get { + return ResourceManager.GetString("ErrorInvalidMethodNameTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to API parameter names cannot start with _. + /// + internal static string ErrorInvalidParameterNameMessage { + get { + return ResourceManager.GetString("ErrorInvalidParameterNameMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to API parameter names can't start with an underscore. + /// + internal static string ErrorInvalidParameterNameTitle { + get { + return ResourceManager.GetString("ErrorInvalidParameterNameTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An API method return type must be of type Task<T>. T must not be nullable.. + /// + internal static string ErrorInvalidReturnTypeMessage { + get { + return ResourceManager.GetString("ErrorInvalidReturnTypeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid API method return type. + /// + internal static string ErrorInvalidReturnTypeTitle { + get { + return ResourceManager.GetString("ErrorInvalidReturnTypeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to API methods cannot be generic. + /// + internal static string ErrorMethodIsGenericMessage { + get { + return ResourceManager.GetString("ErrorMethodIsGenericMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to API methods can't be generic. + /// + internal static string ErrorMethodIsGenericTitle { + get { + return ResourceManager.GetString("ErrorMethodIsGenericTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A REST API method must receive a CancellationToken parameter. + /// + internal static string ErrorMissingCancellationTokenMessage { + get { + return ResourceManager.GetString("ErrorMissingCancellationTokenMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Missing CancellationToken from REST API method. + /// + internal static string ErrorMissingCancellationTokenTitle { + get { + return ResourceManager.GetString("ErrorMissingCancellationTokenTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An HTTP method is missing for this API method. + /// + internal static string ErrorMissingMethodAttributeMessage { + get { + return ResourceManager.GetString("ErrorMissingMethodAttributeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to HTTP method missing. + /// + internal static string ErrorMissingMethodAttributeTitle { + get { + return ResourceManager.GetString("ErrorMissingMethodAttributeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The parameter '{0}' is missing in the URL. + /// + internal static string ErrorMissingParameterUrlMessage { + get { + return ResourceManager.GetString("ErrorMissingParameterUrlMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to URL parameter missing from path. + /// + internal static string ErrorMissingParameterUrlTitle { + get { + return ResourceManager.GetString("ErrorMissingParameterUrlTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An API method path must not contain '?'. Queries should be defined using the [Query] attribute.. + /// + internal static string ErrorPathWithQueryMessage { + get { + return ResourceManager.GetString("ErrorPathWithQueryMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to API method path should not contain query. + /// + internal static string ErrorPathWithQueryTitle { + get { + return ResourceManager.GetString("ErrorPathWithQueryTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to API methods must not be static. + /// + internal static string ErrorStaticMethodMessage { + get { + return ResourceManager.GetString("ErrorStaticMethodMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to API methods must not be static. + /// + internal static string ErrorStaticMethodTitle { + get { + return ResourceManager.GetString("ErrorStaticMethodTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The '{0}' HTTP method does not support the body tag. + /// + internal static string ErrorUnsupportedMethodBodyMessage { + get { + return ResourceManager.GetString("ErrorUnsupportedMethodBodyMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The current HTTP method does not support the body tag. + /// + internal static string ErrorUnsupportedMethodBodyTitle { + get { + return ResourceManager.GetString("ErrorUnsupportedMethodBodyTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to REST API client does not have methods defined. This will render the client class useless.. + /// + internal static string WarningRestClientWithoutRestMethodsMessage { + get { + return ResourceManager.GetString("WarningRestClientWithoutRestMethodsMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to REST API client does not have methods defined. + /// + internal static string WarningRestClientWithoutRestMethodsTitle { + get { + return ResourceManager.GetString("WarningRestClientWithoutRestMethodsTitle", resourceCulture); + } + } + } +} diff --git a/src/Generators/Microsoft.Gen.AutoClient/Common/Resources.resx b/src/Generators/Microsoft.Gen.AutoClient/Common/Resources.resx new file mode 100644 index 0000000000..b287776874 --- /dev/null +++ b/src/Generators/Microsoft.Gen.AutoClient/Common/Resources.resx @@ -0,0 +1,228 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + An API method must not contain more than one REST method attribute + + + An API method must not contain more than one REST method attribute + + + API client interfaces must not be nested types + + + API client interfaces must not be nested types + + + A Body is already defined for this method + + + Duplicate body attribute + + + A REST API method must not receive more than one cancellation token + + + REST API method has more than one cancellation token + + + The request name '{0}' is already in use within this REST API client. + + + A REST API method's request name must be unique + + + The API interface cannot be generic + + + The API interface cannot be generic + + + Invalid API interface name. It should start with an 'I' + + + Invalid API interface name + + + API method names cannot start with _ + + + API method names can't start with an underscore + + + API parameter names cannot start with _ + + + API parameter names can't start with an underscore + + + An API method return type must be of type Task<T>. T must not be nullable. + + + Invalid API method return type + + + API methods cannot be generic + + + API methods can't be generic + + + A REST API method must receive a CancellationToken parameter + + + Missing CancellationToken from REST API method + + + An HTTP method is missing for this API method + + + HTTP method missing + + + The parameter '{0}' is missing in the URL + + + URL parameter missing from path + + + An API method path must not contain '?'. Queries should be defined using the [Query] attribute. + + + API method path should not contain query + + + API methods must not be static + + + API methods must not be static + + + The '{0}' HTTP method does not support the body tag + + + The current HTTP method does not support the body tag + + + REST API client does not have methods defined. This will render the client class useless. + + + REST API client does not have methods defined + + \ No newline at end of file diff --git a/src/Generators/Microsoft.Gen.AutoClient/Common/SymbolHolder.cs b/src/Generators/Microsoft.Gen.AutoClient/Common/SymbolHolder.cs new file mode 100644 index 0000000000..241d8eba92 --- /dev/null +++ b/src/Generators/Microsoft.Gen.AutoClient/Common/SymbolHolder.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Gen.AutoClient; + +internal sealed record class SymbolHolder( + INamedTypeSymbol RestApiAttribute, + INamedTypeSymbol? RestGetAttribute, + INamedTypeSymbol? RestPostAttribute, + INamedTypeSymbol? RestPutAttribute, + INamedTypeSymbol? RestDeleteAttribute, + INamedTypeSymbol? RestPatchAttribute, + INamedTypeSymbol? RestOptionsAttribute, + INamedTypeSymbol? RestHeadAttribute, + INamedTypeSymbol? RestStaticHeaderAttribute, + INamedTypeSymbol? RestHeaderAttribute, + INamedTypeSymbol? RestQueryAttribute, + INamedTypeSymbol? RestBodyAttribute, + INamedTypeSymbol? RestRequestNameAttribute); diff --git a/src/Generators/Microsoft.Gen.AutoClient/Common/SymbolLoader.cs b/src/Generators/Microsoft.Gen.AutoClient/Common/SymbolLoader.cs new file mode 100644 index 0000000000..6bd43ce1e2 --- /dev/null +++ b/src/Generators/Microsoft.Gen.AutoClient/Common/SymbolLoader.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Gen.AutoClient; + +internal static class SymbolLoader +{ + internal const string RestApiAttribute = "Microsoft.Extensions.Http.AutoClient.AutoClientAttribute"; + + internal const string RestGetAttribute = "Microsoft.Extensions.Http.AutoClient.GetAttribute"; + internal const string RestPostAttribute = "Microsoft.Extensions.Http.AutoClient.PostAttribute"; + internal const string RestPutAttribute = "Microsoft.Extensions.Http.AutoClient.PutAttribute"; + internal const string RestDeleteAttribute = "Microsoft.Extensions.Http.AutoClient.DeleteAttribute"; + internal const string RestPatchAttribute = "Microsoft.Extensions.Http.AutoClient.PatchAttribute"; + internal const string RestOptionsAttribute = "Microsoft.Extensions.Http.AutoClient.OptionsAttribute"; + internal const string RestHeadAttribute = "Microsoft.Extensions.Http.AutoClient.HeadAttribute"; + + internal const string RestStaticHeaderAttribute = "Microsoft.Extensions.Http.AutoClient.StaticHeaderAttribute"; + internal const string RestHeaderAttribute = "Microsoft.Extensions.Http.AutoClient.HeaderAttribute"; + internal const string RestQueryAttribute = "Microsoft.Extensions.Http.AutoClient.QueryAttribute"; + internal const string RestBodyAttribute = "Microsoft.Extensions.Http.AutoClient.BodyAttribute"; + internal const string RestRequestNameAttribute = "Microsoft.Extensions.Http.AutoClient.RequestNameAttribute"; + + internal static SymbolHolder? LoadSymbols(Compilation compilation) + { + var restApiAttribute = compilation.GetTypeByMetadataName(RestApiAttribute); + + var restGetAttribute = compilation.GetTypeByMetadataName(RestGetAttribute); + var restPostAttribute = compilation.GetTypeByMetadataName(RestPostAttribute); + var restPutAttribute = compilation.GetTypeByMetadataName(RestPutAttribute); + var restDeleteAttribute = compilation.GetTypeByMetadataName(RestDeleteAttribute); + var restPatchAttribute = compilation.GetTypeByMetadataName(RestPatchAttribute); + var restOptionsAttribute = compilation.GetTypeByMetadataName(RestOptionsAttribute); + var restHeadAttribute = compilation.GetTypeByMetadataName(RestHeadAttribute); + + var restStaticHeaderAttribute = compilation.GetTypeByMetadataName(RestStaticHeaderAttribute); + var restHeaderAttribute = compilation.GetTypeByMetadataName(RestHeaderAttribute); + var restQueryAttribute = compilation.GetTypeByMetadataName(RestQueryAttribute); + var restBodyAttribute = compilation.GetTypeByMetadataName(RestBodyAttribute); + var restRequestNameAttribute = compilation.GetTypeByMetadataName(RestRequestNameAttribute); + + if (restApiAttribute == null) + { + // nothing to do if these types aren't available + return null; + } + + return new( + restApiAttribute, + restGetAttribute, + restPostAttribute, + restPutAttribute, + restDeleteAttribute, + restPatchAttribute, + restOptionsAttribute, + restHeadAttribute, + restStaticHeaderAttribute, + restHeaderAttribute, + restQueryAttribute, + restBodyAttribute, + restRequestNameAttribute); + } +} diff --git a/src/Generators/Microsoft.Gen.AutoClient/Directory.Build.props b/src/Generators/Microsoft.Gen.AutoClient/Directory.Build.props new file mode 100644 index 0000000000..bb438d68cd --- /dev/null +++ b/src/Generators/Microsoft.Gen.AutoClient/Directory.Build.props @@ -0,0 +1,33 @@ + + + + + Microsoft.Gen.AutoClient + Code generator to support Microsoft.Extensions.Http.AutoClient. + Fundamentals + + + + cs + true + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.AutoClient/Roslyn3.8/Microsoft.Gen.AutoClient.Roslyn3.8.csproj b/src/Generators/Microsoft.Gen.AutoClient/Roslyn3.8/Microsoft.Gen.AutoClient.Roslyn3.8.csproj new file mode 100644 index 0000000000..fd6d41a1e1 --- /dev/null +++ b/src/Generators/Microsoft.Gen.AutoClient/Roslyn3.8/Microsoft.Gen.AutoClient.Roslyn3.8.csproj @@ -0,0 +1,28 @@ + + + Microsoft.Gen.AutoClient + 3.8 + $(MicrosoftCodeAnalysisVersion_3_8) + + + + normal + 96 + 74 + 85 + + + + + True + True + Resources.resx + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.AutoClient/Roslyn4.0/Microsoft.Gen.AutoClient.Roslyn4.0.csproj b/src/Generators/Microsoft.Gen.AutoClient/Roslyn4.0/Microsoft.Gen.AutoClient.Roslyn4.0.csproj new file mode 100644 index 0000000000..54718f516d --- /dev/null +++ b/src/Generators/Microsoft.Gen.AutoClient/Roslyn4.0/Microsoft.Gen.AutoClient.Roslyn4.0.csproj @@ -0,0 +1,29 @@ + + + Microsoft.Gen.AutoClient + 4.0 + $(MicrosoftCodeAnalysisVersion_4_0) + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + + + normal + 96 + 74 + 50 + + + + + True + True + Resources.resx + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.ComplianceReports/Common/Emitter.cs b/src/Generators/Microsoft.Gen.ComplianceReports/Common/Emitter.cs new file mode 100644 index 0000000000..094d1c5968 --- /dev/null +++ b/src/Generators/Microsoft.Gen.ComplianceReports/Common/Emitter.cs @@ -0,0 +1,181 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.ComplianceReports; + +internal sealed class Emitter : EmitterBase +{ + private readonly Stack _itemCounts = new(); + private int _itemCount; + + public Emitter() + : base(false) + { + } + + [SuppressMessage("Performance", "R9A036:Use 'Microsoft.Extensions.Text.NumericExtensions.ToInvariantString' for improved performance", Justification = "Can't use that in a generator")] + public string Emit(IReadOnlyCollection classifiedTypes, string assemblyName) + { + OutObject(() => + { + OutNameValue("Name", assemblyName); + + OutArray("Types", () => + { + foreach (var classifiedType in classifiedTypes.OrderBy(ct => ct.TypeName)) + { + OutObject(() => + { + OutNameValue("Name", classifiedType.TypeName); + + if (classifiedType.Members != null) + { + OutArray("Members", () => + { + foreach (var member in classifiedType.Members.OrderBy(m => m.Name)) + { + OutObject(() => + { + OutNameValue("Name", member.Name); + OutNameValue("Type", member.TypeName); + OutNameValue("File", member.SourceFilePath); + OutNameValue("Line", member.SourceLine.ToString(CultureInfo.InvariantCulture)); + + if (member.Classifications.Count > 0) + { + OutArray("Classifications", () => + { + foreach (var c in member.Classifications.OrderBy(c => c.Name)) + { + OutObject(() => + { + OutNameValue("Name", c.Name); + + if (!string.IsNullOrEmpty(c.Notes)) + { + OutNameValue("Notes", c.Notes!); + } + }); + } + }); + } + }); + } + }); + } + + if (classifiedType.LogMethods != null) + { + OutArray("Logging Methods", () => + { + foreach (var method in classifiedType.LogMethods.OrderBy(m => m.MethodName)) + { + OutObject(() => + { + OutNameValue("Name", method.MethodName); + + OutArray("Parameters", () => + { + foreach (var p in method.Parameters) + { + OutObject(() => + { + OutNameValue("Name", p.Name); + OutNameValue("Type", p.TypeName); + OutNameValue("File", p.SourceFilePath); + OutNameValue("Line", p.SourceLine.ToString(CultureInfo.InvariantCulture)); + + if (p.Classifications.Count > 0) + { + OutArray("Classifications", () => + { + foreach (var c in p.Classifications.OrderBy(c => c.Name)) + { + OutObject(() => + { + OutNameValue("Name", c.Name); + + if (!string.IsNullOrEmpty(c.Notes)) + { + OutNameValue("Notes", c.Notes!); + } + }); + } + }); + } + }); + } + }); + }); + } + }); + } + }); + } + }); + }); + + return Capture(); + } + + private void NewItem() + { + if (_itemCount > 0) + { + Out(","); + } + + OutLn(); + _itemCount++; + } + + private void OutObject(Action action) + { + NewItem(); + _itemCounts.Push(_itemCount); + _itemCount = 0; + + OutIndent(); + Out("{"); + Indent(); + action(); + OutLn(); + Unindent(); + OutIndent(); + Out("}"); + + _itemCount = _itemCounts.Pop(); + } + + private void OutArray(string name, Action action) + { + NewItem(); + _itemCounts.Push(_itemCount); + _itemCount = 0; + + OutIndent(); + Out($"\"{name}\": ["); + Indent(); + action(); + OutLn(); + Unindent(); + OutIndent(); + Out("]"); + + _itemCount = _itemCounts.Pop(); + } + + private void OutNameValue(string name, string value) + { + NewItem(); + OutIndent(); + Out($"\"{name}\": \"{value}\""); + } +} diff --git a/src/Generators/Microsoft.Gen.ComplianceReports/Common/Generator.cs b/src/Generators/Microsoft.Gen.ComplianceReports/Common/Generator.cs new file mode 100644 index 0000000000..19bc8b1a3c --- /dev/null +++ b/src/Generators/Microsoft.Gen.ComplianceReports/Common/Generator.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Microsoft.CodeAnalysis; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.ComplianceReports; + +/// +/// Generates reports for compliance annotations. +/// +[Generator] +[ExcludeFromCodeCoverage] +public sealed class Generator : ISourceGenerator +{ + private const string GenerateComplianceReportsMSBuildProperty = "build_property.GenerateComplianceReport"; + private const string ComplianceReportOutputPathMSBuildProperty = "build_property.ComplianceReportOutputPath"; + + private string? _reportOutputPath; + + public Generator() + : this(null) + { + } + + public Generator(string? reportOutputPath) + { + _reportOutputPath = reportOutputPath; + } + + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(TypeDeclarationSyntaxReceiver.Create); + } + + public void Execute(GeneratorExecutionContext context) + { + var receiver = context.SyntaxReceiver as TypeDeclarationSyntaxReceiver; + if (receiver == null || receiver.TypeDeclarations.Count == 0) + { + // nothing to do yet + return; + } + + if (!GeneratorUtilities.ShouldGenerateReport(context, GenerateComplianceReportsMSBuildProperty)) + { + // By default, compliance reports are only generated only during build time and not during design time to prevent the file being written on every keystroke in VS. + return; + } + + if (!SymbolLoader.TryLoad(context.Compilation, out var symbolHolder)) + { + // Not eligible compilation + return; + } + + var parser = new Parser(context.Compilation, symbolHolder!, context.CancellationToken); + var classifiedTypes = parser.GetClassifiedTypes(receiver.TypeDeclarations); + if (classifiedTypes.Count == 0) + { + // nothing to do + return; + } + + var emitter = new Emitter(); + string report = emitter.Emit(classifiedTypes, context.Compilation.AssemblyName!); + + context.CancellationToken.ThrowIfCancellationRequested(); + + if (_reportOutputPath == null) + { + _ = context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(ComplianceReportOutputPathMSBuildProperty, out _reportOutputPath); + if (string.IsNullOrWhiteSpace(_reportOutputPath)) + { + // no valid output path + return; + } + } + +#pragma warning disable R9A017 // Switch to an asynchronous method for increased performance. + _ = Directory.CreateDirectory(Path.GetDirectoryName(_reportOutputPath)); + + // Write properties to CSV file. + File.WriteAllText(_reportOutputPath, report); +#pragma warning restore R9A017 // Switch to an asynchronous method for increased performance. + } +} diff --git a/src/Generators/Microsoft.Gen.ComplianceReports/Common/Model/Classification.cs b/src/Generators/Microsoft.Gen.ComplianceReports/Common/Model/Classification.cs new file mode 100644 index 0000000000..ff4a2d1e3a --- /dev/null +++ b/src/Generators/Microsoft.Gen.ComplianceReports/Common/Model/Classification.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Gen.ComplianceReports; + +/// +/// A classified field or property. +/// +internal sealed class Classification +{ + public string Name = string.Empty; + public string? Notes; +} diff --git a/src/Generators/Microsoft.Gen.ComplianceReports/Common/Model/ClassifiedItem.cs b/src/Generators/Microsoft.Gen.ComplianceReports/Common/Model/ClassifiedItem.cs new file mode 100644 index 0000000000..5c47c9e587 --- /dev/null +++ b/src/Generators/Microsoft.Gen.ComplianceReports/Common/Model/ClassifiedItem.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Gen.ComplianceReports; + +/// +/// A classified field or property. +/// +internal sealed class ClassifiedItem +{ + public string SourceFilePath = string.Empty; + public int SourceLine; + + public string Name = string.Empty; + public string TypeName = string.Empty; + public List Classifications = new(); +} diff --git a/src/Generators/Microsoft.Gen.ComplianceReports/Common/Model/ClassifiedLogMethod.cs b/src/Generators/Microsoft.Gen.ComplianceReports/Common/Model/ClassifiedLogMethod.cs new file mode 100644 index 0000000000..7348c2f45a --- /dev/null +++ b/src/Generators/Microsoft.Gen.ComplianceReports/Common/Model/ClassifiedLogMethod.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Gen.ComplianceReports; + +/// +/// A log method containing classified members. +/// +internal sealed class ClassifiedLogMethod +{ + public string MethodName = string.Empty; + public string LogMethodMessage = string.Empty; + public List Parameters = new(); +} diff --git a/src/Generators/Microsoft.Gen.ComplianceReports/Common/Model/ClassifiedType.cs b/src/Generators/Microsoft.Gen.ComplianceReports/Common/Model/ClassifiedType.cs new file mode 100644 index 0000000000..70d438db67 --- /dev/null +++ b/src/Generators/Microsoft.Gen.ComplianceReports/Common/Model/ClassifiedType.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Gen.ComplianceReports; + +/// +/// A type holding classified members and/or log methods. +/// +internal sealed class ClassifiedType +{ + public string TypeName = string.Empty; + public List? Members; + public List? LogMethods; +} diff --git a/src/Generators/Microsoft.Gen.ComplianceReports/Common/Parser.cs b/src/Generators/Microsoft.Gen.ComplianceReports/Common/Parser.cs new file mode 100644 index 0000000000..0e40c72664 --- /dev/null +++ b/src/Generators/Microsoft.Gen.ComplianceReports/Common/Parser.cs @@ -0,0 +1,247 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Gen.ComplianceReports; + +internal sealed class Parser +{ + private readonly Compilation _compilation; + private readonly SymbolHolder _symbolHolder; + private readonly CancellationToken _cancellationToken; + + public Parser(Compilation compilation, SymbolHolder symbolHolder, CancellationToken cancellationToken) + { + _compilation = compilation; + _symbolHolder = symbolHolder; + _cancellationToken = cancellationToken; + } + + /// + /// Gets the set of data classification classes containing properties and method parameters to output. + /// + public IReadOnlyList GetClassifiedTypes(IEnumerable classes) + { + var result = new List(); + + // We enumerate by syntax tree, to minimize the need to instantiate semantic models (since they're expensive) + IEnumerable> typesBySyntaxTree = classes.GroupBy(x => x.SyntaxTree); + foreach (var typeForSyntaxTree in typesBySyntaxTree) + { + SemanticModel? sm = null; + foreach (TypeDeclarationSyntax typeSyntax in typeForSyntaxTree.Where(n => !n.IsKind(SyntaxKind.InterfaceDeclaration))) + { + _cancellationToken.ThrowIfCancellationRequested(); + + sm ??= _compilation.GetSemanticModel(typeSyntax.SyntaxTree); + + INamedTypeSymbol? typeSymbol = sm.GetDeclaredSymbol(typeSyntax, _cancellationToken); + if (typeSymbol != null) + { + Dictionary? classifiedMembers = null; + + // grab the annotated members + classifiedMembers = GetClassifiedMembers(typeSymbol, classifiedMembers); + + // include annotations applied via an implemented interface + foreach (var iface in typeSymbol.AllInterfaces) + { + classifiedMembers = GetClassifiedMembers(iface, classifiedMembers); + } + + // include annotations from base classes + var parent = typeSymbol.BaseType; + while (parent != null) + { + classifiedMembers = GetClassifiedMembers(parent, classifiedMembers); + parent = parent.BaseType; + } + + // grab the logging methods + var classifiedLogMethods = GetClassifiedLogMethods(typeSymbol); + + if (classifiedMembers != null || classifiedLogMethods != null) + { + result.Add(new ClassifiedType + { + TypeName = FormatType(typeSymbol), + Members = classifiedMembers?.Values.ToList(), + LogMethods = classifiedLogMethods, + }); + } + } + } + } + + return result; + } + + private static string FormatType(ITypeSymbol typeSymbol) + { + var result = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + if (result.StartsWith("global::", StringComparison.Ordinal)) + { + result = result.Substring("global::".Length); + } + + return result; + } + + private Dictionary? GetClassifiedMembers(ITypeSymbol typeSymbol, Dictionary? classifiedMembers) + { + foreach (var property in typeSymbol.GetMembers().OfType()) + { + classifiedMembers = ClassifyMember(classifiedMembers, property, property.Type); + } + + foreach (var field in typeSymbol.GetMembers().OfType()) + { + if (!field.IsImplicitlyDeclared) + { + classifiedMembers = ClassifyMember(classifiedMembers, field, field.Type); + } + } + + return classifiedMembers; + + Dictionary? ClassifyMember(Dictionary? classifiedMembers, ISymbol member, ITypeSymbol memberType) + { + ClassifiedItem? ci = null; + if (classifiedMembers != null) + { + _ = classifiedMembers.TryGetValue(member.Name, out ci); + } + + // classification coming from the member's container + foreach (var attribute in typeSymbol.GetAttributes()) + { + ci = AppendAttributeClassifications(ci, attribute); + } + + // classification coming from the member's type + foreach (var attribute in memberType.GetAttributes()) + { + ci = AppendAttributeClassifications(ci, attribute); + } + + // classificaiton coming from the member's attributes + foreach (AttributeData attribute in member.GetAttributes()) + { + ci = AppendAttributeClassifications(ci, attribute); + } + + if (ci != null) + { + FileLinePositionSpan fileLine = member.Locations[0].GetLineSpan(); + ci.SourceFilePath = fileLine.Path; + ci.SourceLine = fileLine.StartLinePosition.Line + 1; + ci.Name = member.Name; + ci.TypeName = FormatType(memberType); + + classifiedMembers ??= new(); + classifiedMembers[ci.Name] = ci; + } + + return classifiedMembers; + } + } + + private List? GetClassifiedLogMethods(ITypeSymbol typeSymbol) + { + List? classifiedLogMethods = null; + if (_symbolHolder.LogMethodAttribute != null) + { + var methods = typeSymbol.GetMembers().OfType(); + foreach (IMethodSymbol method in methods) + { + foreach (var a in method.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(_symbolHolder.LogMethodAttribute, a.AttributeClass)) + { + var clm = new ClassifiedLogMethod + { + MethodName = method.Name, + LogMethodMessage = "Not Implemented", + }; + + foreach (var p in method.Parameters) + { + FileLinePositionSpan fileLine = p.Locations[0].GetLineSpan(); + var ci = new ClassifiedItem + { + SourceFilePath = fileLine.Path, + SourceLine = fileLine.StartLinePosition.Line + 1, + Name = p.Name, + TypeName = FormatType(p.Type), + }; + + // classification coming from the parameter's type + foreach (var attribute in p.Type.GetAttributes()) + { + ci = AppendAttributeClassifications(ci, attribute); + } + + // classificaiton coming from the parameter's attributes + foreach (AttributeData attribute in p.GetAttributes()) + { + ci = AppendAttributeClassifications(ci, attribute); + } + + clm.Parameters.Add(ci); + } + + classifiedLogMethods ??= new(); + classifiedLogMethods.Add(clm); + } + } + } + } + + return classifiedLogMethods; + } + + private ClassifiedItem? AppendAttributeClassifications(ClassifiedItem? ci, AttributeData attribute) + { + if (DerivesFrom(attribute.AttributeClass!, _symbolHolder.DataClassificationAttributeSymbol)) + { + string name = attribute.AttributeClass!.Name; + if (name.EndsWith("Attribute", StringComparison.Ordinal)) + { + name = name.Substring(0, name.Length - "Attribute".Length); + } + + string? notes = null; + foreach (var namedArg in attribute.NamedArguments) + { + if (namedArg.Key == "Notes") + { + notes = namedArg.Value.Value?.ToString(); + break; + } + } + + ci ??= new(); + + ci.Classifications.Add(new Classification + { + Name = name, + Notes = notes, + }); + } + + return ci; + } + + private bool DerivesFrom(ITypeSymbol source, ITypeSymbol dest) + { + var conversion = _compilation.ClassifyConversion(source, dest); + return conversion.IsReference && conversion.IsImplicit; + } +} diff --git a/src/Generators/Microsoft.Gen.ComplianceReports/Common/SymbolHolder.cs b/src/Generators/Microsoft.Gen.ComplianceReports/Common/SymbolHolder.cs new file mode 100644 index 0000000000..dd8f5c2781 --- /dev/null +++ b/src/Generators/Microsoft.Gen.ComplianceReports/Common/SymbolHolder.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Gen.ComplianceReports; + +/// +/// Holds required symbols for the . +/// +internal sealed record class SymbolHolder( + INamedTypeSymbol DataClassificationAttributeSymbol, + INamedTypeSymbol? LogMethodAttribute); diff --git a/src/Generators/Microsoft.Gen.ComplianceReports/Common/SymbolLoader.cs b/src/Generators/Microsoft.Gen.ComplianceReports/Common/SymbolLoader.cs new file mode 100644 index 0000000000..d67f82cd4c --- /dev/null +++ b/src/Generators/Microsoft.Gen.ComplianceReports/Common/SymbolLoader.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Gen.ComplianceReports; + +internal static class SymbolLoader +{ + private const string DataClassificationAttribute = "Microsoft.Extensions.Compliance.Classification.DataClassificationAttribute"; + private const string LogMethodAttribute = "Microsoft.Extensions.Telemetry.Logging.LogMethodAttribute"; + + public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHolder) + { + // required + var dataClassificationAttributeSymbol = compilation.GetTypeByMetadataName(DataClassificationAttribute); + + if (dataClassificationAttributeSymbol == null) + { + symbolHolder = default; + return false; + } + + symbolHolder = new( + dataClassificationAttributeSymbol, + compilation.GetTypeByMetadataName(LogMethodAttribute)); + + return true; + } +} diff --git a/src/Generators/Microsoft.Gen.ComplianceReports/Directory.Build.props b/src/Generators/Microsoft.Gen.ComplianceReports/Directory.Build.props new file mode 100644 index 0000000000..b371031876 --- /dev/null +++ b/src/Generators/Microsoft.Gen.ComplianceReports/Directory.Build.props @@ -0,0 +1,30 @@ + + + + + Microsoft.Gen.ComplianceReports + Produces compliance reports based on data classification annotations in the code. + Fundamentals + + + + cs + true + + + + + + + + + + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.ComplianceReports/Roslyn3.8/Microsoft.Gen.ComplianceReports.Roslyn3.8.csproj b/src/Generators/Microsoft.Gen.ComplianceReports/Roslyn3.8/Microsoft.Gen.ComplianceReports.Roslyn3.8.csproj new file mode 100644 index 0000000000..ddb93aeb70 --- /dev/null +++ b/src/Generators/Microsoft.Gen.ComplianceReports/Roslyn3.8/Microsoft.Gen.ComplianceReports.Roslyn3.8.csproj @@ -0,0 +1,19 @@ + + + Microsoft.Gen.ComplianceReports + 3.8 + $(MicrosoftCodeAnalysisVersion_3_8) + + + + dev + 99 + 85 + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.ComplianceReports/Roslyn4.0/Microsoft.Gen.ComplianceReports.Roslyn4.0.csproj b/src/Generators/Microsoft.Gen.ComplianceReports/Roslyn4.0/Microsoft.Gen.ComplianceReports.Roslyn4.0.csproj new file mode 100644 index 0000000000..1206b1a4ac --- /dev/null +++ b/src/Generators/Microsoft.Gen.ComplianceReports/Roslyn4.0/Microsoft.Gen.ComplianceReports.Roslyn4.0.csproj @@ -0,0 +1,20 @@ + + + Microsoft.Gen.ComplianceReports + 4.0 + $(MicrosoftCodeAnalysisVersion_4_0) + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + + + dev + 99 + 85 + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.ContextualOptions/Common/ContextReceiver.cs b/src/Generators/Microsoft.Gen.ContextualOptions/Common/ContextReceiver.cs new file mode 100644 index 0000000000..8c3bbe53d9 --- /dev/null +++ b/src/Generators/Microsoft.Gen.ContextualOptions/Common/ContextReceiver.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Gen.ContextualOptions; + +/// +/// Type declaration syntax receiver for generators. +/// +internal sealed class ContextReceiver : ISyntaxReceiver +{ + private readonly CancellationToken _token; + + public ContextReceiver(CancellationToken token) + { + _token = token; + } + + private readonly List _typeDeclarations = new(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + _token.ThrowIfCancellationRequested(); + + if (syntaxNode is TypeDeclarationSyntax type + && type is not InterfaceDeclarationSyntax) + { + _typeDeclarations.Add(type); + } + } + + public bool TryGetTypeDeclarations(Compilation compilation, out Dictionary>? typeDeclarations) + { + if (!SymbolLoader.TryLoad(compilation, out var holder)) + { + typeDeclarations = default; + return false; + } + + typeDeclarations = _typeDeclarations + .ToLookup(declaration => declaration.SyntaxTree) + .SelectMany(declarations => declarations.Select(declaration => (symbol: compilation.GetSemanticModel(declarations.Key).GetDeclaredSymbol(declaration), declaration))) + .Where(_ => _.symbol is INamedTypeSymbol) + .Where(_ => _.symbol!.GetAttributes().Any(attribute => SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, holder!.OptionsContextAttribute))) + .ToLookup(_ => _.symbol, _ => _.declaration, comparer: SymbolEqualityComparer.Default) + .ToDictionary, INamedTypeSymbol, List>( + group => (INamedTypeSymbol)group.Key!, group => group.ToList(), comparer: SymbolEqualityComparer.Default); + + return true; + } +} diff --git a/src/Generators/Microsoft.Gen.ContextualOptions/Common/DiagDescriptors.cs b/src/Generators/Microsoft.Gen.ContextualOptions/Common/DiagDescriptors.cs new file mode 100644 index 0000000000..0749e17159 --- /dev/null +++ b/src/Generators/Microsoft.Gen.ContextualOptions/Common/DiagDescriptors.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.ContextualOptions; + +internal sealed class DiagDescriptors : DiagDescriptorsBase +{ + private const string Category = "ContextualOptions"; + + public static DiagnosticDescriptor ContextCannotBeStatic { get; } = Make( + id: "R9G200", + title: Resources.ContextCannotBeStaticTitle, + messageFormat: Resources.ContextCannotBeStaticMessage, + category: Category); + + public static DiagnosticDescriptor ContextMustBePartial { get; } = Make( + id: "R9G201", + title: Resources.ContextMustBePartialTitle, + messageFormat: Resources.ContextMustBePartialMessage, + category: Category); + + public static DiagnosticDescriptor ContextDoesNotHaveValidProperties { get; } = Make( + id: "R9G202", + title: Resources.ContextDoesNotHaveValidPropertiesTitle, + messageFormat: Resources.ContextDoesNotHaveValidPropertiesMessage, + category: Category, + defaultSeverity: DiagnosticSeverity.Warning); + + public static DiagnosticDescriptor ContextCannotBeRefLike { get; } = Make( + id: "R9G203", + title: Resources.ContextCannotBeRefLikeTitle, + messageFormat: Resources.ContextCannotBeRefLikeMessage, + category: Category); +} diff --git a/src/Generators/Microsoft.Gen.ContextualOptions/Common/Emitter.cs b/src/Generators/Microsoft.Gen.ContextualOptions/Common/Emitter.cs new file mode 100644 index 0000000000..653af7664e --- /dev/null +++ b/src/Generators/Microsoft.Gen.ContextualOptions/Common/Emitter.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using Microsoft.Gen.ContextualOptions.Model; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.ContextualOptions; + +internal sealed class Emitter : EmitterBase +{ + public string Emit(IEnumerable list) + { + foreach (var optionsContextType in list) + { + OutLn(FormatClass(optionsContextType).ToString(CultureInfo.InvariantCulture)); + } + + return Capture(); + } + + [SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1513:Closing brace should be followed by blank line", + Justification = "The spacing here is done intentionally to better reflect what the generated code will look like.")] + private static FormattableString FormatClass(OptionsContextType optionsContextType) => + $@"{(!string.IsNullOrEmpty(optionsContextType.Namespace) ? $"namespace {optionsContextType.Namespace}" + + "{" : string.Empty)} + [{GeneratorUtilities.GeneratedCodeAttribute}] + partial {optionsContextType.Keyword} {optionsContextType.Name} : global::Microsoft.Extensions.Options.Contextual.IOptionsContext + {{ + [{GeneratorUtilities.GeneratedCodeAttribute}] + void global::Microsoft.Extensions.Options.Contextual.IOptionsContext.PopulateReceiver(T receiver) + {{{string.Concat(optionsContextType.OptionsContextProperties.OrderBy(x => x).Select(property => $@" + receiver.Receive(nameof({property}), {property});"))} + }} + }} +{(!string.IsNullOrEmpty(optionsContextType.Namespace) ? "}" : string.Empty)}"; +} diff --git a/src/Generators/Microsoft.Gen.ContextualOptions/Common/Generator.cs b/src/Generators/Microsoft.Gen.ContextualOptions/Common/Generator.cs new file mode 100644 index 0000000000..7d731c67f8 --- /dev/null +++ b/src/Generators/Microsoft.Gen.ContextualOptions/Common/Generator.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if ROSLYN_4_0_OR_GREATER + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Gen.ContextualOptions.Model; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.ContextualOptions; + +[Generator] +[ExcludeFromCodeCoverage] +public class Generator : IIncrementalGenerator +{ + private static readonly HashSet _attributeNames = new() + { + "Microsoft.Extensions.Options.Contextual.OptionsContextAttribute", + }; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + GeneratorUtilities.Initialize(context, _attributeNames, HandleAnnotatedTypes); + } + + private static void HandleAnnotatedTypes(Compilation compilation, IEnumerable nodes, SourceProductionContext context) + { + if (!SymbolLoader.TryLoad(compilation, out var holder)) + { + return; + } + + var typeDeclarations = nodes.OfType() + .ToLookup(declaration => declaration.SyntaxTree) + .SelectMany(declarations => declarations.Select(declaration => (symbol: compilation.GetSemanticModel(declarations.Key).GetDeclaredSymbol(declaration), declaration))) + .Where(_ => _.symbol is INamedTypeSymbol) + .Where(_ => _.symbol!.GetAttributes().Any(attribute => SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, holder!.OptionsContextAttribute))) + .ToLookup(_ => _.symbol, _ => _.declaration, comparer: SymbolEqualityComparer.Default) + .ToDictionary, INamedTypeSymbol, List>( + group => (INamedTypeSymbol)group.Key!, group => group.ToList(), comparer: SymbolEqualityComparer.Default); + + var list = new List(); + foreach (var type in Parser.GetContextualOptionTypes(typeDeclarations)) + { + context.CancellationToken.ThrowIfCancellationRequested(); + type.Diagnostics.ForEach(context.ReportDiagnostic); + + if (type.ShouldEmit) + { + list.Add(type); + } + } + + if (list.Count > 0) + { + var emitter = new Emitter(); + context.AddSource($"ContextualOptions.g.cs", emitter.Emit(list.OrderBy(x => x.Namespace + "." + x.Name))); + } + } +} + +#else + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.Gen.ContextualOptions.Model; + +namespace Microsoft.Gen.ContextualOptions; + +/// +/// Generates options context implementations for user annotated objects. +/// +[Generator] +[ExcludeFromCodeCoverage] +public class Generator : ISourceGenerator +{ + public void Initialize(GeneratorInitializationContext context) => + context.RegisterForSyntaxNotifications(() => new ContextReceiver(context.CancellationToken)); + + public void Execute(GeneratorExecutionContext context) + { + var receiver = context.SyntaxReceiver as ContextReceiver; + if (receiver is null || !receiver.TryGetTypeDeclarations(context.Compilation, out var typeDeclarations)) + { + // nothing to do yet + return; + } + + var list = new List(); + foreach (var type in Parser.GetContextualOptionTypes(typeDeclarations!)) + { + context.CancellationToken.ThrowIfCancellationRequested(); + type.Diagnostics.ForEach(context.ReportDiagnostic); + + if (type.ShouldEmit) + { + list.Add(type); + } + } + + if (list.Count > 0) + { + var emitter = new Emitter(); + context.AddSource($"ContextualOptions.g.cs", emitter.Emit(list.OrderBy(x => x.Namespace + "." + x.Name))); + } + } +} + +#endif diff --git a/src/Generators/Microsoft.Gen.ContextualOptions/Common/Model/OptionsContextType.cs b/src/Generators/Microsoft.Gen.ContextualOptions/Common/Model/OptionsContextType.cs new file mode 100644 index 0000000000..798a383e1c --- /dev/null +++ b/src/Generators/Microsoft.Gen.ContextualOptions/Common/Model/OptionsContextType.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Gen.ContextualOptions.Model; + +internal sealed class OptionsContextType +{ + public readonly List Diagnostics = new(); + public readonly INamedTypeSymbol Symbol; + public readonly ImmutableArray Definitions; + public readonly ImmutableArray OptionsContextProperties; + public string Keyword => Definitions[0].Keyword.Text; + public string? Namespace => Symbol.ContainingNamespace.IsGlobalNamespace ? null : Symbol.ContainingNamespace.ToString(); + public string Name => Symbol.Name; + + public bool ShouldEmit => Diagnostics.TrueForAll(diag => diag.Severity != DiagnosticSeverity.Error); + + public string HintName => $"{Namespace}.{Name}"; + + public OptionsContextType( + INamedTypeSymbol symbol, + ImmutableArray definitions, + ImmutableArray optionsContextProperties) + { + Symbol = symbol; + Definitions = definitions; + OptionsContextProperties = optionsContextProperties; + } +} diff --git a/src/Generators/Microsoft.Gen.ContextualOptions/Common/Parser.cs b/src/Generators/Microsoft.Gen.ContextualOptions/Common/Parser.cs new file mode 100644 index 0000000000..ba4fad5557 --- /dev/null +++ b/src/Generators/Microsoft.Gen.ContextualOptions/Common/Parser.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Gen.ContextualOptions.Model; + +namespace Microsoft.Gen.ContextualOptions; + +internal static class Parser +{ + public static IEnumerable GetContextualOptionTypes(Dictionary> types) => + types + .Select(type => new OptionsContextType(type.Key, type.Value.ToImmutableArray(), GetContextProperties(type.Key))) + .Select(CheckInstantiable) + .Select(CheckPartial) + .Select(CheckRefLikeType) + .Select(CheckHasProperties); + + private static OptionsContextType CheckInstantiable(OptionsContextType type) + { + if (type.Symbol.IsStatic) + { + type.Diagnostics.AddRange( + type.Definitions + .SelectMany(def => def.Modifiers) + .Where(modifier => modifier.IsKind(SyntaxKind.StaticKeyword)) + .Select(modifier => Diagnostic.Create(DiagDescriptors.ContextCannotBeStatic, modifier.GetLocation(), type.Name))); + } + + return type; + } + + private static OptionsContextType CheckRefLikeType(OptionsContextType type) + { + if (type.Symbol.IsRefLikeType) + { + type.Diagnostics.AddRange( + type.Definitions + .SelectMany(def => def.Modifiers) + .Where(modifier => modifier.IsKind(SyntaxKind.RefKeyword)) + .Select(modifier => Diagnostic.Create(DiagDescriptors.ContextCannotBeRefLike, modifier.GetLocation(), type.Name))); + } + + return type; + } + + private static OptionsContextType CheckPartial(OptionsContextType type) + { + if (!type.Definitions.Any(def => def.Modifiers.Any(static token => token.IsKind(SyntaxKind.PartialKeyword)))) + { + type.Diagnostics.AddRange( + type.Definitions.Select(def => Diagnostic.Create(DiagDescriptors.ContextMustBePartial, def.Identifier.GetLocation(), type.Name))); + } + + return type; + } + + private static OptionsContextType CheckHasProperties(OptionsContextType type) + { + if (type.OptionsContextProperties.IsEmpty) + { + type.Diagnostics.AddRange( + type.Definitions.Select(def => Diagnostic.Create(DiagDescriptors.ContextDoesNotHaveValidProperties, def.Identifier.GetLocation(), type.Name))); + } + + return type; + } + + private static ImmutableArray GetContextProperties(INamedTypeSymbol symbol) + { + return symbol + .GetMembers() + .OfType() + .Where(prop => !prop.IsStatic) + .Where(prop => !prop.IsWriteOnly) + .Where(prop => !prop.Type.IsRefLikeType) + .Where(prop => prop.Type.TypeKind != TypeKind.Pointer) + .Where(prop => prop.Type.TypeKind != TypeKind.FunctionPointer) + .Where(prop => prop.Parameters.IsEmpty) + .Where(prop => prop.ExplicitInterfaceImplementations.IsEmpty) + .Where(GetterIsPublic) + .Select(prop => prop.Name) + .ToImmutableArray(); + + static bool GetterIsPublic(IPropertySymbol prop) => + prop.GetMethod!.DeclaredAccessibility == Accessibility.Public; + } +} diff --git a/src/Generators/Microsoft.Gen.ContextualOptions/Common/Resources.Designer.cs b/src/Generators/Microsoft.Gen.ContextualOptions/Common/Resources.Designer.cs new file mode 100644 index 0000000000..24939195c2 --- /dev/null +++ b/src/Generators/Microsoft.Gen.ContextualOptions/Common/Resources.Designer.cs @@ -0,0 +1,135 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Gen.ContextualOptions { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Gen.ContextualOptions.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Remove the ref modifier from {0}, because it is annotated with OptionsContextAttribute.. + /// + internal static string ContextCannotBeRefLikeMessage { + get { + return ResourceManager.GetString("ContextCannotBeRefLikeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The options context cannot be a ref-like type. + /// + internal static string ContextCannotBeRefLikeTitle { + get { + return ResourceManager.GetString("ContextCannotBeRefLikeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove the static modifier from {0}, because it is annotated with the OptionsContextAttribute.. + /// + internal static string ContextCannotBeStaticMessage { + get { + return ResourceManager.GetString("ContextCannotBeStaticMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Options context classes can't be static. + /// + internal static string ContextCannotBeStaticTitle { + get { + return ResourceManager.GetString("ContextCannotBeStaticTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add a valid property to {0}, because it is annotated with OptionsContextAttribute. Valid properties are parameterless, non-static, have public getters, and don't return ref-like type. Contextual options providers cannot use invalid properties.. + /// + internal static string ContextDoesNotHaveValidPropertiesMessage { + get { + return ResourceManager.GetString("ContextDoesNotHaveValidPropertiesMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The options context type does not have usable properties. + /// + internal static string ContextDoesNotHaveValidPropertiesTitle { + get { + return ResourceManager.GetString("ContextDoesNotHaveValidPropertiesTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add the partial modifier from {0}, because it is annotated with the OptionsContextAttribute.. + /// + internal static string ContextMustBePartialMessage { + get { + return ResourceManager.GetString("ContextMustBePartialMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Options context types must be partial. + /// + internal static string ContextMustBePartialTitle { + get { + return ResourceManager.GetString("ContextMustBePartialTitle", resourceCulture); + } + } + } +} diff --git a/src/Generators/Microsoft.Gen.ContextualOptions/Common/Resources.resx b/src/Generators/Microsoft.Gen.ContextualOptions/Common/Resources.resx new file mode 100644 index 0000000000..099bfb1b08 --- /dev/null +++ b/src/Generators/Microsoft.Gen.ContextualOptions/Common/Resources.resx @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Remove the ref modifier from {0}, because it is annotated with OptionsContextAttribute. + + + The options context cannot be a ref-like type + + + Remove the static modifier from {0}, because it is annotated with the OptionsContextAttribute. + + + Options context classes can't be static + + + Add a valid property to {0}, because it is annotated with OptionsContextAttribute. Valid properties are parameterless, non-static, have public getters, and don't return ref-like type. Contextual options providers cannot use invalid properties. + + + The options context type does not have usable properties + + + Add the partial modifier from {0}, because it is annotated with the OptionsContextAttribute. + + + Options context types must be partial + + \ No newline at end of file diff --git a/src/Generators/Microsoft.Gen.ContextualOptions/Common/SymbolHolder.cs b/src/Generators/Microsoft.Gen.ContextualOptions/Common/SymbolHolder.cs new file mode 100644 index 0000000000..c55d857cd6 --- /dev/null +++ b/src/Generators/Microsoft.Gen.ContextualOptions/Common/SymbolHolder.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Gen.ContextualOptions; + +internal sealed record class SymbolHolder(INamedTypeSymbol OptionsContextAttribute); diff --git a/src/Generators/Microsoft.Gen.ContextualOptions/Common/SymbolLoader.cs b/src/Generators/Microsoft.Gen.ContextualOptions/Common/SymbolLoader.cs new file mode 100644 index 0000000000..5f9c8d6601 --- /dev/null +++ b/src/Generators/Microsoft.Gen.ContextualOptions/Common/SymbolLoader.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Gen.ContextualOptions; + +internal static class SymbolLoader +{ + public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHolder) + { + symbolHolder = default; + + var optionsContextAttribute = compilation.GetTypeByMetadataName("Microsoft.Extensions.Options.Contextual.OptionsContextAttribute"); + if (optionsContextAttribute is null) + { + return false; + } + + symbolHolder = new SymbolHolder(optionsContextAttribute); + return true; + } +} diff --git a/src/Generators/Microsoft.Gen.ContextualOptions/Directory.Build.props b/src/Generators/Microsoft.Gen.ContextualOptions/Directory.Build.props new file mode 100644 index 0000000000..c3b07b5543 --- /dev/null +++ b/src/Generators/Microsoft.Gen.ContextualOptions/Directory.Build.props @@ -0,0 +1,35 @@ + + + + + Microsoft.Gen.ContextualOptions + Code generator to support Microsoft.Extensions.Options.Contextual. + Fundamentals + + + + cs + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.ContextualOptions/Roslyn3.8/Microsoft.Gen.ContextualOptions.Roslyn3.8.csproj b/src/Generators/Microsoft.Gen.ContextualOptions/Roslyn3.8/Microsoft.Gen.ContextualOptions.Roslyn3.8.csproj new file mode 100644 index 0000000000..8442c870bc --- /dev/null +++ b/src/Generators/Microsoft.Gen.ContextualOptions/Roslyn3.8/Microsoft.Gen.ContextualOptions.Roslyn3.8.csproj @@ -0,0 +1,27 @@ + + + Microsoft.Gen.ContextualOptions + 3.8 + $(MicrosoftCodeAnalysisVersion_3_8) + + + + normal + 98 + 85 + + + + + True + True + Resources.resx + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.ContextualOptions/Roslyn4.0/Microsoft.Gen.ContextualOptions.Roslyn4.0.csproj b/src/Generators/Microsoft.Gen.ContextualOptions/Roslyn4.0/Microsoft.Gen.ContextualOptions.Roslyn4.0.csproj new file mode 100644 index 0000000000..dade0d0bb1 --- /dev/null +++ b/src/Generators/Microsoft.Gen.ContextualOptions/Roslyn4.0/Microsoft.Gen.ContextualOptions.Roslyn4.0.csproj @@ -0,0 +1,28 @@ + + + Microsoft.Gen.ContextualOptions + 4.0 + $(MicrosoftCodeAnalysisVersion_4_0) + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + + + normal + 98 + 50 + + + + + True + True + Resources.resx + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.EnumStrings/Common/DiagDescriptors.cs b/src/Generators/Microsoft.Gen.EnumStrings/Common/DiagDescriptors.cs new file mode 100644 index 0000000000..e0ecd54cb1 --- /dev/null +++ b/src/Generators/Microsoft.Gen.EnumStrings/Common/DiagDescriptors.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.EnumStrings; + +internal sealed class DiagDescriptors : DiagDescriptorsBase +{ + private const string Category = "EnumStrings"; + + public static DiagnosticDescriptor InvalidExtensionNamespace { get; } = Make( + id: "R9G250", + title: Resources.InvalidExtensionNamespaceTitle, + messageFormat: Resources.InvalidExtensionNamespaceMessage, + category: Category); + + public static DiagnosticDescriptor IncorrectOverload { get; } = Make( + id: "R9G251", + title: Resources.IncorrectOverloadTitle, + messageFormat: Resources.IncorrectOverloadMessage, + category: Category); + + public static DiagnosticDescriptor InvalidExtensionClassName { get; } = Make( + id: "R9G252", + title: Resources.InvalidExtensionClassNameTitle, + messageFormat: Resources.InvalidExtensionClassNameMessage, + category: Category); + + public static DiagnosticDescriptor InvalidExtensionMethodName { get; } = Make( + id: "R9G253", + title: Resources.InvalidExtensionMethodNameTitle, + messageFormat: Resources.InvalidExtensionMethodNameMessage, + category: Category); + + public static DiagnosticDescriptor InvalidEnumType { get; } = Make( + id: "R9G254", + title: Resources.InvalidEnumTypeTitle, + messageFormat: Resources.InvalidEnumTypeMessage, + category: Category); +} diff --git a/src/Generators/Microsoft.Gen.EnumStrings/Common/Emitter.cs b/src/Generators/Microsoft.Gen.EnumStrings/Common/Emitter.cs new file mode 100644 index 0000000000..468d2202f8 --- /dev/null +++ b/src/Generators/Microsoft.Gen.EnumStrings/Common/Emitter.cs @@ -0,0 +1,474 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using Microsoft.Gen.EnumStrings.Model; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.EnumStrings; + +#pragma warning disable R9A036 // Use 'Microsoft.Extensions.Text.NumericExtensions.ToInvariantString' for improved performance +#pragma warning disable S109 // Magic numbers should not be used + +// Stryker disable all + +/// +/// Emits fast ToInvariantString extension methods for enums. +/// +/// +/// The generated code uses different strategies depending on the shape of the enum, and depending on what +/// symbols are available at compile time. +/// +/// * If an enum has 1 or 2 entries, the lookup is done with explicit "if" statements. +/// +/// * If an enum has mostly contiguous values (a common case), then the lookup is done via an array +/// +/// * If an enum has a set of discontiguous values, then the lookup is done via a dictionary. This will be a frozen dictionary if the +/// frozen collections are available at compile time, otherwise a classic dictionary. +/// +/// In all cases, if the initial lookup fails, then we lookup in a static concurrent dictionary as a cache of values from +/// the original Enum.ToString(). +/// +internal sealed class Emitter : EmitterBase +{ + // max # entries we keep in the concurrent dictionary + private const int MaxCacheEntries = 256; + + // flags and sparse arrays can grow to this size no questions asked + private const int ArrayLookupThreshold = 1024; + + // sparse arrays can get bigger than the threshold only if they don't exceed this percentage of sparseness. + private const int MaxSparsePercent = 25; + + public string Emit( + IEnumerable toStringMethods, + bool frozenDictionaryAvailable, + CancellationToken cancellationToken) + { + foreach (var tsm in toStringMethods.OrderBy(static t => t.ExtensionNamespace + "." + t.ExtensionClass)) + { + cancellationToken.ThrowIfCancellationRequested(); + GenExtension(tsm, frozenDictionaryAvailable); + } + + return Capture(); + } + + // This code was stolen from .NET 6's implementation of Enum.ToString, and adapted for the circumstances + private static string FlagsName(List names, List enumMemberValues, ulong valueToFormat) + { + ulong originalValueToFormat = valueToFormat; + + // Values are sorted, so if the incoming value is 0, we can check to see whether + // the first entry matches it, in which case we can return its name; otherwise, + // we can just return "0". + if (valueToFormat == 0) + { + return enumMemberValues.Count > 0 && enumMemberValues[0] == 0 ? names[0] : "0"; + } + + // With a ulong result value, regardless of the enum's base type, the maximum + // possible number of consistent name/values we could have is 64, since every + // value is made up of one or more bits, and when we see values and incorporate + // their names, we effectively switch off those bits. + Span foundItems = stackalloc int[64]; + + // Walk from largest to smallest. It's common to have a flags enum with a single + // value that matches a single entry, in which case we can just return the existing + // name string. + int index = enumMemberValues.Count - 1; + while (index >= 0) + { + if (enumMemberValues[index] == valueToFormat) + { + return names[index]; + } + + if (enumMemberValues[index] < valueToFormat) + { + break; + } + + index--; + } + + // Now look for multiple matches, storing the indices of the values + // into our span. + int resultLength = 0; + int foundItemsCount = 0; + while (index >= 0) + { + ulong currentValue = enumMemberValues[index]; + if (index == 0 && currentValue == 0) + { + break; + } + + if ((valueToFormat & currentValue) == currentValue) + { + valueToFormat -= currentValue; + foundItems[foundItemsCount++] = index; + resultLength += names[index].Length; + } + + index--; + } + + // If we exhausted looking through all the values and we still have + // a non-zero result, we couldn't match the result to only named values. + // In that case, we return null and let the call site just generate + // a string for the integral value. + if (valueToFormat != 0) + { + return originalValueToFormat.ToString(CultureInfo.InvariantCulture); + } + + // We know what strings to concatenate. Do so. + + const int SeparatorStringLength = 2; // ", " + resultLength += SeparatorStringLength * (foundItemsCount - 1); + char[] result = new char[resultLength]; + + Span resultSpan = result.AsSpan(); + string name = names[foundItems[--foundItemsCount]]; + for (int i = 0; i < name.Length; i++) + { + resultSpan[i] = name[i]; + } + + resultSpan = resultSpan.Slice(name.Length); + while (--foundItemsCount >= 0) + { + resultSpan[0] = ','; + resultSpan[1] = ' '; + resultSpan = resultSpan.Slice(SeparatorStringLength); + + name = names[foundItems[foundItemsCount]]; + for (int i = 0; i < name.Length; i++) + { + resultSpan[i] = name[i]; + } + + resultSpan = resultSpan.Slice(name.Length); + } + + return new string(result); + } + + private static bool IsBigEnum(ToStringMethod tsm) => tsm.UnderlyingType is "ulong" or "long"; + + private void GenExtension(ToStringMethod tsm, bool frozenDictionaryAvailable) + { + if (tsm.ExtensionNamespace.Length > 0) + { + OutLn($"namespace {tsm.ExtensionNamespace}"); + OutOpenBrace(); + } + + OutLn(); + OutLn($"/// "); + OutLn($"/// Extension methods for the enum."); + OutLn($"/// "); + OutLn($"{tsm.ExtensionClassModifiers} class {tsm.ExtensionClass}"); + OutOpenBrace(); + + var names = tsm.MemberNames; + var values = tsm.MemberValues; + var lookupType = PickLookupType(tsm, out var flagRange, values); + var fieldPrefix = "__" + tsm.ExtensionMethod + "_"; + + GenFields(); + GenMethod(); + + OutCloseBrace(); + + if (tsm.ExtensionNamespace.Length > 0) + { + OutCloseBrace(); + } + + static LookupType PickLookupType(ToStringMethod tsm, out ulong flagRange, List values) + { + flagRange = 0; + var lookupType = LookupType.Nothing; + + if (tsm.FlagsEnum) + { + foreach (var v in values) + { + flagRange |= v; + } + + if (values.Count == 1) + { + lookupType = LookupType.Conditionals; + } + else if (flagRange < ArrayLookupThreshold) + { + lookupType = LookupType.Array; + } + else + { + lookupType = LookupType.Dictionary; + } + } + else + { + if (values.Count < 3) + { + lookupType = LookupType.Conditionals; + } + else + { + var delta = values[values.Count - 1] - values[0] + 1; + if (delta == (ulong)values.Count) + { + lookupType = LookupType.Array; + } + else if (values[values.Count - 1] < ArrayLookupThreshold) + { + lookupType = LookupType.Array; + } + else + { + lookupType = LookupType.Array; + + var numEmptySlots = delta - (ulong)values.Count; + var percenEmptySlots = (numEmptySlots * 100) / (ulong)values.Count; + if (percenEmptySlots > MaxSparsePercent) + { + lookupType = LookupType.Dictionary; + } + } + } + } + + return lookupType; + } + + void GenMethod() + { + OutLn($"/// "); + OutLn($"/// Efficiently returns a string representation for a value of the enum."); + OutLn($"/// "); + OutLn($"/// The value to use."); + OutLn($"/// A string representation of the value, equivalent to what ToString would return."); + OutLn($"/// This function is equivalent to calling ToString on an enum's value, except that it is considerably faster."); + OutGeneratedCodeAttribute(); + OutLn($"public static string {tsm.ExtensionMethod}(this {tsm.EnumTypeName} value)"); + OutOpenBrace(); + + var valueType = IsBigEnum(tsm) ? "ulong" : "uint"; + var valueText = IsBigEnum(tsm) ? "v" : "(int)v"; + + OutLn($"var v = ({valueType})value;"); + + switch (lookupType) + { + case LookupType.Conditionals: + { + for (int i = 0; i < values.Count; i++) + { + var e = (i > 0) ? "else " : string.Empty; + OutLn($"{e}if (v == {GetLiteral(values[i])})"); + OutOpenBrace(); + OutLn($"return \"{names[i]}\";"); + OutCloseBrace(); + } + + break; + } + + case LookupType.Array: + { + if (tsm.FlagsEnum) + { + OutLn($"if (v <= {flagRange})"); + OutOpenBrace(); + OutLn($"return {fieldPrefix}LookupArray[v];"); + OutCloseBrace(); + } + else + { + var upper = GetLiteral(values[values.Count - 1]); + if (values[0] > 0) + { + var lower = GetLiteral(values[0]); + OutLn($"if (v >= {lower} && v <= {upper})"); + OutOpenBrace(); + OutLn($"return {fieldPrefix}LookupArray[v - {lower}];"); + } + else + { + if (IsBigEnum(tsm)) + { + OutLn($"if (v <= {upper})"); + } + else + { + OutLn($"if (v < {fieldPrefix}LookupArray.Length)"); + } + + OutOpenBrace(); + OutLn($"return {fieldPrefix}LookupArray[v];"); + } + + OutCloseBrace(); + } + + break; + } + + case LookupType.Dictionary: + { + OutLn($"if ({fieldPrefix}LookupDictionary.TryGetValue({valueText}, out var lookupResult))"); + OutOpenBrace(); + OutLn($"return lookupResult;"); + OutCloseBrace(); + break; + } + } + + OutLn(); + OutLn($"{fieldPrefix}CacheDictionary ??= new();"); + OutLn($"if ({fieldPrefix}CacheDictionary.TryGetValue({valueText}, out var cachedResult))"); + OutOpenBrace(); + OutLn("return cachedResult;"); + OutCloseBrace(); + + OutLn(); + OutLn($"var result = value.ToString();"); + + OutLn(); + OutLn($"if ({fieldPrefix}ApproximateCacheCount < {MaxCacheEntries})"); + OutOpenBrace(); + OutLn($"_ = global::System.Threading.Interlocked.Increment(ref {fieldPrefix}ApproximateCacheCount);"); + OutLn($"{fieldPrefix}CacheDictionary[{valueText}] = result;"); + OutCloseBrace(); + + OutLn(); + OutLn($"return result;"); + + OutCloseBrace(); + + string GetLiteral(ulong value) => IsBigEnum(tsm) ? value.ToString(CultureInfo.InvariantCulture) + "UL" : ((uint)value).ToString(CultureInfo.InvariantCulture) + "U"; + } + + void GenFields() + { + if (lookupType == LookupType.Array) + { + OutGeneratedCodeAttribute(); + OutLn($"private static readonly string[] {fieldPrefix}LookupArray = new string[]"); + OutOpenBrace(); + + if (tsm.FlagsEnum) + { + for (ulong i = 0; i <= flagRange; i++) + { + OutLn($"\"{FlagsName(names, values, i)}\","); + } + } + else + { + OutLn($"\"{names[0]}\","); + + ulong previous = values[0]; + for (int i = 1; i < values.Count; i++) + { + while (previous < values[i] - 1) + { + previous++; + OutLn($"\"{previous.ToString(CultureInfo.InvariantCulture)}\","); + } + + OutLn($"\"{names[i]}\","); + previous = values[i]; + } + } + + OutCloseBraceWithExtra(";"); + } + else if (lookupType == LookupType.Dictionary) + { + OutGeneratedCodeAttribute(); + + var isBigEnum = IsBigEnum(tsm); + +#pragma warning disable S3358 // Ternary operators should not be nested +#pragma warning disable S103 // Lines should not be too long + var decl = frozenDictionaryAvailable + ? isBigEnum + ? $"private static readonly global::System.Collections.Frozen.FrozenDictionary {fieldPrefix}LookupDictionary = global::System.Collections.Frozen.FrozenDictionary.ToFrozenDictionary(new global::System.Collections.Generic.Dictionary({values.Count})" + : $"private static readonly global::System.Collections.Frozen.FrozenDictionary {fieldPrefix}LookupDictionary = global::System.Collections.Frozen.FrozenDictionary.ToFrozenDictionary(new global::System.Collections.Generic.Dictionary({values.Count})" + : isBigEnum + ? $"private static readonly global::System.Collections.Generic.Dictionary {fieldPrefix}LookupDictionary = new({values.Count})" + : $"private static readonly global::System.Collections.Generic.Dictionary {fieldPrefix}LookupDictionary = new({values.Count})"; +#pragma warning restore S103 // Lines should not be too long +#pragma warning restore S3358 // Ternary operators should not be nested + + OutLn(decl); + OutOpenBrace(); + + if (tsm.FlagsEnum) + { + for (int i = 0; i < ArrayLookupThreshold; i++) + { + OutLn($"{{ {i.ToString(CultureInfo.InvariantCulture)}, \"{FlagsName(names, values, (ulong)i)}\" }},"); + } + + for (int i = 0; i < values.Count; i++) + { + if (values[i] >= ArrayLookupThreshold) + { + if (isBigEnum) + { + OutLn($"{{ {values[i].ToString(CultureInfo.InvariantCulture)}, \"{names[i]}\" }},"); + } + else + { + OutLn($"{{ unchecked((int){(values[i] & 0xffffffff).ToString(CultureInfo.InvariantCulture)}), \"{names[i]}\" }},"); + } + } + } + } + else + { + for (int i = 0; i < values.Count; i++) + { + if (isBigEnum) + { + OutLn($"{{ {values[i].ToString(CultureInfo.InvariantCulture)}, \"{names[i]}\" }},"); + } + else + { + OutLn($"{{ unchecked((int){(values[i] & 0xffffffff).ToString(CultureInfo.InvariantCulture)}), \"{names[i]}\" }},"); + } + } + } + + OutCloseBraceWithExtra(frozenDictionaryAvailable ? ", optimizeForReading: true);" : ";"); + } + + var keyType = IsBigEnum(tsm) ? "ulong" : "int"; + + OutLn(); + OutGeneratedCodeAttribute(); + OutLn($"private static global::System.Collections.Concurrent.ConcurrentDictionary<{keyType}, string>? {fieldPrefix}CacheDictionary;"); + OutLn($"private static volatile int {fieldPrefix}ApproximateCacheCount;"); + OutLn(); + } + } + + private enum LookupType + { + Nothing, + Conditionals, + Array, + Dictionary, + } +} diff --git a/src/Generators/Microsoft.Gen.EnumStrings/Common/Generator.cs b/src/Generators/Microsoft.Gen.EnumStrings/Common/Generator.cs new file mode 100644 index 0000000000..7fdd5b45b8 --- /dev/null +++ b/src/Generators/Microsoft.Gen.EnumStrings/Common/Generator.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if ROSLYN_4_0_OR_GREATER + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.EnumStrings; + +[Generator] +[ExcludeFromCodeCoverage] +public class Generator : IIncrementalGenerator +{ + private static readonly HashSet _attributeNames = new() + { + SymbolLoader.EnumStringsAttribute, + }; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + GeneratorUtilities.Initialize(context, _attributeNames, HandleAnnotatedNodes); + } + + private static void HandleAnnotatedNodes(Compilation compilation, IEnumerable nodes, SourceProductionContext context) + { + if (!SymbolLoader.TryLoad(compilation, out var symbolHolder)) + { + // Not eligible compilation + return; + } + + var parser = new Parser(compilation, context.ReportDiagnostic, symbolHolder!, context.CancellationToken); + + var toStringMethods = parser.GetToStringMethods(nodes); + if (toStringMethods.Count > 0) + { + var emitter = new Emitter(); + var result = emitter.Emit(toStringMethods, symbolHolder!.FreezerSymbol != null, context.CancellationToken); + + context.AddSource("EnumStrings.g.cs", SourceText.From(result, Encoding.UTF8)); + } + } +} + +#else + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.Gen.EnumStrings; + +[Generator] +[ExcludeFromCodeCoverage] +public class Generator : ISourceGenerator +{ + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new Receiver()); + } + + public void Execute(GeneratorExecutionContext context) + { + var receiver = context.SyntaxReceiver as Receiver; + if (receiver == null || receiver.Nodes.Count == 0) + { + // nothing to do yet + return; + } + + if (!SymbolLoader.TryLoad(context.Compilation, out var symbolHolder)) + { + // Not eligible compilation + return; + } + + var parser = new Parser(context.Compilation, context.ReportDiagnostic, symbolHolder!, context.CancellationToken); + var toStringMethods = parser.GetToStringMethods(receiver.Nodes); + if (toStringMethods.Count > 0) + { + var emitter = new Emitter(); + var result = emitter.Emit(toStringMethods, symbolHolder!.FreezerSymbol != null, context.CancellationToken); + context.AddSource("EnumStrings.g.cs", SourceText.From(result, Encoding.UTF8)); + } + } + + private sealed class Receiver : ISyntaxReceiver + { + public ICollection Nodes { get; } = new List(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is EnumDeclarationSyntax enumSyntax) + { + Nodes.Add(enumSyntax); + } + else if (syntaxNode is CompilationUnitSyntax compUnitSyntax) + { + Nodes.Add(compUnitSyntax); + } + } + } +} + +#endif diff --git a/src/Generators/Microsoft.Gen.EnumStrings/Common/Model/ToStringMethod.cs b/src/Generators/Microsoft.Gen.EnumStrings/Common/Model/ToStringMethod.cs new file mode 100644 index 0000000000..129111830c --- /dev/null +++ b/src/Generators/Microsoft.Gen.EnumStrings/Common/Model/ToStringMethod.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Gen.EnumStrings.Model; + +internal sealed record class ToStringMethod( + string EnumTypeName, + List MemberNames, + List MemberValues, + bool FlagsEnum, + string ExtensionNamespace, + string ExtensionClass, + string ExtensionMethod, + string ExtensionClassModifiers, + string UnderlyingType); diff --git a/src/Generators/Microsoft.Gen.EnumStrings/Common/Parser.cs b/src/Generators/Microsoft.Gen.EnumStrings/Common/Parser.cs new file mode 100644 index 0000000000..b27857ba1e --- /dev/null +++ b/src/Generators/Microsoft.Gen.EnumStrings/Common/Parser.cs @@ -0,0 +1,246 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Gen.EnumStrings.Model; + +namespace Microsoft.Gen.EnumStrings; + +/// +/// Holds an internal parser class that extracts necessary information for generating IValidateOptions. +/// +internal sealed class Parser +{ + private readonly CancellationToken _cancellationToken; + private readonly Compilation _compilation; + private readonly Action _reportDiagnostic; + private readonly SymbolHolder _symbolHolder; + + public Parser( + Compilation compilation, + Action reportDiagnostic, + SymbolHolder symbolHolder, + CancellationToken cancellationToken) + { + _compilation = compilation; + _cancellationToken = cancellationToken; + _reportDiagnostic = reportDiagnostic; + _symbolHolder = symbolHolder; + } + + public IReadOnlyList GetToStringMethods(IEnumerable nodes) + { + var results = new List(); + + foreach (var group in nodes.GroupBy(x => x.SyntaxTree)) + { + SemanticModel? sm = null; + foreach (var node in group) + { + _cancellationToken.ThrowIfCancellationRequested(); + sm ??= _compilation.GetSemanticModel(node.SyntaxTree); + + if (node.IsKind(SyntaxKind.EnumDeclaration)) + { + // enum-level attribute usage + var enumDecl = (EnumDeclarationSyntax)node; + var enumSym = sm.GetDeclaredSymbol(node) as INamedTypeSymbol; + if (enumSym != null) + { + ParseAttributeList( + enumSym, + enumSym!.GetAttributes(), + results); + } + } + else if (node.IsKind(SyntaxKind.CompilationUnit)) + { + // assembly-level attribute usage + var compUnitDecl = (CompilationUnitSyntax)node; + ParseAttributeList( + null, + sm.Compilation.Assembly.GetAttributes().Where(ad => ad.ApplicationSyntaxReference!.SyntaxTree == node.SyntaxTree), + results); + } + } + } + + return results; + } + + private static (INamedTypeSymbol? explicitEnumType, string? nspace, string? className, string? methodName, string? classModifiers) + ExtractAttributeValues(AttributeData args) + { + INamedTypeSymbol? explicitEnumType = null; + string? nspace = null; + string? className = null; + string? methodName = null; + string? classModifiers = null; + + // Two constructor shapes: + // + // () + // (Type enumType) + if (args.ConstructorArguments.Length > 0) + { + explicitEnumType = args.ConstructorArguments[0].Value as INamedTypeSymbol; + } + + foreach (var a in args.NamedArguments) + { + switch (a.Key) + { + case "ExtensionClassModifiers": + classModifiers = a.Value.Value as string; + break; + + case "ExtensionNamespace": + nspace = a.Value.Value as string; + break; + + case "ExtensionClassName": + className = a.Value.Value as string; + break; + + case "ExtensionMethodName": + methodName = a.Value.Value as string; + break; + } + } + + return (explicitEnumType, nspace, className, methodName, classModifiers); + } + + private static ulong ConvertValue(object obj) => + obj switch + { + sbyte or short or int or long => (ulong)Convert.ToInt64(obj, CultureInfo.InvariantCulture), + byte or ushort or uint or ulong => Convert.ToUInt64(obj, CultureInfo.InvariantCulture), + _ => 0, + }; + + private static bool IsValidNamespace(string nspace) + { + var source = $"namespace {nspace} {{ }}"; + var st = CSharpSyntaxTree.ParseText(source); + return !st.GetDiagnostics().Any(); + } + + private static bool IsValidClassName(string className) + { + var source = $"class {className} {{ }}"; + var st = CSharpSyntaxTree.ParseText(source); + return !st.GetDiagnostics().Any(); + } + + private static bool IsValidMethodName(string methodName) + { + var source = $"class ___XYZ {{ public void {methodName}() {{ }} }}"; + var st = CSharpSyntaxTree.ParseText(source); + return !(st.GetDiagnostics().Any() || methodName.Contains('.')); + } + + private void ParseAttributeList(INamedTypeSymbol? implicitEnumType, IEnumerable attrDataList, List results) + { + foreach (var ad in attrDataList) + { + if (SymbolEqualityComparer.Default.Equals(ad.AttributeClass, _symbolHolder.EnumStringsAttributeSymbol)) + { + var attrData = ad; + var attrSyntax = attrData.ApplicationSyntaxReference?.GetSyntax(_cancellationToken) as AttributeSyntax; + + if (attrData != null && attrSyntax != null) + { + var (explicitEnumType, nspace, className, methodName, classModifiers) = ExtractAttributeValues(attrData); + + if (nspace != null && !IsValidNamespace(nspace)) + { + Diag(DiagDescriptors.InvalidExtensionNamespace, attrSyntax.GetLocation(), nspace); + } + + if (className != null && !IsValidClassName(className)) + { + Diag(DiagDescriptors.InvalidExtensionClassName, attrSyntax.GetLocation(), className); + } + + if (methodName != null && !IsValidMethodName(methodName)) + { + Diag(DiagDescriptors.InvalidExtensionMethodName, attrSyntax.GetLocation(), methodName); + } + + var enumType = implicitEnumType ?? explicitEnumType; + + if ((implicitEnumType != null && explicitEnumType != null) || enumType == null) + { + // must have one and only one enum type + Diag(DiagDescriptors.IncorrectOverload, attrSyntax.GetLocation()); + return; + } + + if (enumType.TypeKind != TypeKind.Enum) + { + Diag(DiagDescriptors.InvalidEnumType, attrSyntax.GetLocation(), enumType); + return; + } + + var flags = enumType.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, _symbolHolder.FlagsAttributeSymbol)); + + // get a sorted list of enum members + IEnumerable> members = enumType + .GetMembers() + .OfType() + .Where(f => f.IsConst) + .Select(f => new KeyValuePair(f.Name, ConvertValue(f.ConstantValue!))) + .OrderBy(kvp => kvp.Value); + + if (flags) + { + members = members + .Reverse() // flip it so Distinct keeps the last instance of duplicates instead of the first to match what Enum.ToString does + .Distinct(new EntryComparer()) + .Reverse(); // flip it back to natural order + } + else + { + members = members.Distinct(new EntryComparer()); + } + + results.Add(new ToStringMethod( + enumType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + members.Select(kvp => kvp.Key).ToList(), + members.Select(kvp => kvp.Value).ToList(), + flags, + nspace ?? enumType.ContainingNamespace.ToString(), + className ?? enumType.Name + "Extensions", + methodName ?? "ToInvariantString", + classModifiers ?? "internal static", + enumType.EnumUnderlyingType!.ToString())); + } + } + } + } + + private void Diag(DiagnosticDescriptor desc, Location? location) + { + _reportDiagnostic(Diagnostic.Create(desc, location, Array.Empty())); + } + + private void Diag(DiagnosticDescriptor desc, Location? location, params object?[]? messageArgs) + { + _reportDiagnostic(Diagnostic.Create(desc, location, messageArgs)); + } + + private sealed class EntryComparer : IEqualityComparer> + { + public bool Equals(KeyValuePair x, KeyValuePair y) => x.Value == y.Value; + public int GetHashCode(KeyValuePair obj) => obj.Value.GetHashCode(); + } +} diff --git a/src/Generators/Microsoft.Gen.EnumStrings/Common/Resources.Designer.cs b/src/Generators/Microsoft.Gen.EnumStrings/Common/Resources.Designer.cs new file mode 100644 index 0000000000..54b8cf2d97 --- /dev/null +++ b/src/Generators/Microsoft.Gen.EnumStrings/Common/Resources.Designer.cs @@ -0,0 +1,153 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Gen.EnumStrings { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Gen.EnumStrings.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to This attribute constructor is not valid in this context, please try a different constructor. + /// + internal static string IncorrectOverloadMessage { + get { + return ResourceManager.GetString("IncorrectOverloadMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid attribute constructor used. + /// + internal static string IncorrectOverloadTitle { + get { + return ResourceManager.GetString("IncorrectOverloadTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} is not an enum. + /// + internal static string InvalidEnumTypeMessage { + get { + return ResourceManager.GetString("InvalidEnumTypeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid enum type specified. + /// + internal static string InvalidEnumTypeTitle { + get { + return ResourceManager.GetString("InvalidEnumTypeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} is not a valid class name. + /// + internal static string InvalidExtensionClassNameMessage { + get { + return ResourceManager.GetString("InvalidExtensionClassNameMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid extension class name. + /// + internal static string InvalidExtensionClassNameTitle { + get { + return ResourceManager.GetString("InvalidExtensionClassNameTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} is not a valid method name. + /// + internal static string InvalidExtensionMethodNameMessage { + get { + return ResourceManager.GetString("InvalidExtensionMethodNameMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid extension method name. + /// + internal static string InvalidExtensionMethodNameTitle { + get { + return ResourceManager.GetString("InvalidExtensionMethodNameTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} is not a valid namespace. + /// + internal static string InvalidExtensionNamespaceMessage { + get { + return ResourceManager.GetString("InvalidExtensionNamespaceMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid extension namespace. + /// + internal static string InvalidExtensionNamespaceTitle { + get { + return ResourceManager.GetString("InvalidExtensionNamespaceTitle", resourceCulture); + } + } + } +} diff --git a/src/Generators/Microsoft.Gen.EnumStrings/Common/Resources.resx b/src/Generators/Microsoft.Gen.EnumStrings/Common/Resources.resx new file mode 100644 index 0000000000..32d6fde1aa --- /dev/null +++ b/src/Generators/Microsoft.Gen.EnumStrings/Common/Resources.resx @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + This attribute constructor is not valid in this context, please try a different constructor + + + Invalid attribute constructor used + + + {0} is not an enum + + + Invalid enum type specified + + + {0} is not a valid class name + + + Invalid extension class name + + + {0} is not a valid method name + + + Invalid extension method name + + + {0} is not a valid namespace + + + Invalid extension namespace + + \ No newline at end of file diff --git a/src/Generators/Microsoft.Gen.EnumStrings/Common/SymbolHolder.cs b/src/Generators/Microsoft.Gen.EnumStrings/Common/SymbolHolder.cs new file mode 100644 index 0000000000..e767b52244 --- /dev/null +++ b/src/Generators/Microsoft.Gen.EnumStrings/Common/SymbolHolder.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Gen.EnumStrings; + +/// +/// Holds required symbols for the . +/// +internal sealed record class SymbolHolder( + INamedTypeSymbol FlagsAttributeSymbol, + INamedTypeSymbol EnumStringsAttributeSymbol, + INamedTypeSymbol? FreezerSymbol); diff --git a/src/Generators/Microsoft.Gen.EnumStrings/Common/SymbolLoader.cs b/src/Generators/Microsoft.Gen.EnumStrings/Common/SymbolLoader.cs new file mode 100644 index 0000000000..628995d0d8 --- /dev/null +++ b/src/Generators/Microsoft.Gen.EnumStrings/Common/SymbolLoader.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Gen.EnumStrings; + +internal static class SymbolLoader +{ + public const string EnumStringsAttribute = "Microsoft.Extensions.EnumStrings.EnumStringsAttribute"; + public const string FlagsAttribute = "System.FlagsAttribute"; + public const string FreezerClass = "System.Collections.Frozen.FrozenDictionary"; + + public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHolder) + { + INamedTypeSymbol? GetSymbol(string metadataName, bool optional = false) + { + var symbol = compilation.GetTypeByMetadataName(metadataName); + if (symbol == null && !optional) + { + return null; + } + + return symbol; + } + + // required + var flagsAttributeSymbol = GetSymbol(FlagsAttribute); + var enumStringsAttributeSymbol = GetSymbol(EnumStringsAttribute); + + if (flagsAttributeSymbol == null || enumStringsAttributeSymbol == null) + { + symbolHolder = default; + return false; + } + + symbolHolder = new( + flagsAttributeSymbol, + enumStringsAttributeSymbol, + GetSymbol(FreezerClass)); + return true; + } +} diff --git a/src/Generators/Microsoft.Gen.EnumStrings/Directory.Build.props b/src/Generators/Microsoft.Gen.EnumStrings/Directory.Build.props new file mode 100644 index 0000000000..bf903a3c55 --- /dev/null +++ b/src/Generators/Microsoft.Gen.EnumStrings/Directory.Build.props @@ -0,0 +1,33 @@ + + + + + Microsoft.Gen.EnumStrings + Code generator to support Microsoft.Extensions.EnumStrings. + Fundamentals + + + + cs + true + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.EnumStrings/Roslyn3.8/Microsoft.Gen.EnumStrings.Roslyn3.8.csproj b/src/Generators/Microsoft.Gen.EnumStrings/Roslyn3.8/Microsoft.Gen.EnumStrings.Roslyn3.8.csproj new file mode 100644 index 0000000000..b068907b0a --- /dev/null +++ b/src/Generators/Microsoft.Gen.EnumStrings/Roslyn3.8/Microsoft.Gen.EnumStrings.Roslyn3.8.csproj @@ -0,0 +1,28 @@ + + + Microsoft.Gen.EnumStrings + 3.8 + $(MicrosoftCodeAnalysisVersion_3_8) + + + + normal + 96 + 51 + 85 + + + + + True + True + Resources.resx + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.EnumStrings/Roslyn4.0/Microsoft.Gen.EnumStrings.Roslyn4.0.csproj b/src/Generators/Microsoft.Gen.EnumStrings/Roslyn4.0/Microsoft.Gen.EnumStrings.Roslyn4.0.csproj new file mode 100644 index 0000000000..43c2418c4e --- /dev/null +++ b/src/Generators/Microsoft.Gen.EnumStrings/Roslyn4.0/Microsoft.Gen.EnumStrings.Roslyn4.0.csproj @@ -0,0 +1,29 @@ + + + Microsoft.Gen.EnumStrings + 4.0 + $(MicrosoftCodeAnalysisVersion_4_0) + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + + + normal + 96 + 51 + 50 + + + + + True + True + Resources.resx + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Emission/Emitter.Method.cs b/src/Generators/Microsoft.Gen.Logging/Common/Emission/Emitter.Method.cs new file mode 100644 index 0000000000..33285b5be3 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Emission/Emitter.Method.cs @@ -0,0 +1,489 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.Gen.Logging.Model; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.Logging.Emission; + +// Stryker disable all + +internal sealed partial class Emitter : EmitterBase +{ + [SuppressMessage("Major Code Smell", "S103:Lines should not be too long", Justification = "Long strings are easier to read in this function")] + private void GenLogMethod(LoggingMethod lm) + { + var logPropsDataClasses = GetLogPropertiesAttributes(lm); + string level = GetLoggerMethodLogLevel(lm); + string extension = lm.IsExtensionMethod ? "this " : string.Empty; + string eventName = string.IsNullOrWhiteSpace(lm.EventName) ? $"nameof({lm.Name})" : $"\"{lm.EventName}\""; + string exceptionArg = GetException(lm); + (string logger, bool isNullableLogger) = GetLogger(lm); + + OutLn(); + + if (!lm.HasXmlDocumentation) + { + var l = GetLoggerMethodLogLevelForXmlDocumentation(lm); + var lvl = string.Empty; + if (l != null) + { + lvl = $" at \"{l}\" level"; + } + + OutLn($"/// "); + + if (!string.IsNullOrEmpty(lm.Message)) + { + OutLn($"/// Logs \"{EscapeMessageStringForXmlDocumentation(lm.Message)}\" {lvl}."); + } + else + { + OutLn($"/// Emits a structured log entry{lvl}."); + } + + OutLn($"/// "); + } + + OutGeneratedCodeAttribute(); + + OutIndent(); + Out($"{lm.Modifiers} void {lm.Name}({extension}"); + GenParameters(lm); + Out(")\n"); + + OutOpenBrace(); + + if (isNullableLogger) + { + OutLn($"if ({logger} == null)"); + OutOpenBrace(); + OutLn("return;"); + OutCloseBrace(); + OutLn(); + } + + if (!lm.SkipEnabledCheck) + { + OutLn($"if (!{logger}.IsEnabled({level}))"); + OutOpenBrace(); + OutLn("return;"); + OutCloseBrace(); + OutLn(); + } + + var parametersToRedact = lm.AllParameters.Where(lp => lp.ClassificationAttributeType != null).ToArray(); + + if (parametersToRedact.Length > 0 || logPropsDataClasses.Count > 0) + { + (string redactorProvider, bool isNullableRedactorProvider) = GetRedactorProvider(lm); + + var classifications = parametersToRedact + .Select(static p => p.ClassificationAttributeType) + .Concat(logPropsDataClasses) + .Distinct() + .Where(static p => p != null) + .Select(static p => p!); + + GenRedactorsFetchingCode(_isRedactorProviderInTheInstance, classifications, redactorProvider, isNullableRedactorProvider); + OutLn(); + } + + Dictionary holderMap = new(); + + OutLn($"var _helper = {LogMethodHelperType}.GetHelper();"); + + foreach (var p in lm.AllParameters) + { + if (!p.HasPropsProvider && !p.HasProperties && p.IsNormalParameter) + { + GenHelperAdd(lm, holderMap, p); + } + } + + foreach (var p in lm.TemplateParameters) + { + if (!holderMap.ContainsKey(p)) + { + GenHelperAdd(lm, holderMap, p); + } + } + + if (!string.IsNullOrEmpty(lm.Message)) + { + OutLn($"_helper.Add(\"{{OriginalFormat}}\", \"{EscapeMessageString(lm.Message)}\");"); + } + + foreach (var p in lm.AllParameters) + { + if (p.HasPropsProvider) + { + if (p.OmitParameterName) + { + OutLn($"_helper.ParameterName = string.Empty;"); + } + else + { + OutLn($"_helper.ParameterName = nameof({p.NameWithAt});"); + } + + OutLn($"{p.LogPropertiesProvider!.ContainingType}.{p.LogPropertiesProvider.MethodName}(_helper, {p.NameWithAt});"); + } + else if (p.HasProperties) + { + OutLn($"_helper.ParameterName = string.Empty;"); + +#pragma warning disable S1067 // Expressions should not be too complex + p.TraverseParameterPropertiesTransitively((propertyChain, member) => + { + var propName = PropertyChainToString(propertyChain, member, "_", omitParameterName: p.OmitParameterName); + var accessExpression = PropertyChainToString(propertyChain, member, "?.", nonNullSeparator: "."); + + var skipNull = p.SkipNullProperties && member.PotentiallyNull + ? $"if ({accessExpression} != null) " + : string.Empty; + + if (member.ClassificationAttributeType != null) + { + var value = $"_{EncodeTypeName(member.ClassificationAttributeType)}Redactor?.Redact(global::System.MemoryExtensions.AsSpan({ConvertPropertyToString(member, accessExpression)}))"; + + if (member.PotentiallyNull) + { + if (p.SkipNullProperties || accessExpression == value) + { + OutLn($"{skipNull}_helper.Add(\"{propName}\", {value});"); + } + else + { + OutLn($"_helper.Add(\"{propName}\", {accessExpression} != null ? {value} : null);"); + } + } + else + { + OutLn($"_helper.Add(\"{propName}\", {value});"); + } + } + else + { + var ts = ShouldStringify(member.Type) ? ConvertPropertyToString(member, accessExpression) : accessExpression; + + var value = member.IsEnumerable + ? $"global::Microsoft.Extensions.Telemetry.Logging.LogMethodHelper.Stringify({accessExpression})" + : ts; + + OutLn($"{skipNull}_helper.Add(\"{propName}\", {value});"); + } + }); +#pragma warning restore S1067 // Expressions should not be too complex + } + } + + OutLn(); + + OutLn($"{logger}.Log("); + + Indent(); + OutLn($"{level},"); + + if (lm.EventId != null) + { + OutLn($"new({lm.EventId}, {eventName}),"); + } + else + { + OutLn($"new(0, {eventName}),"); + } + + OutLn($"_helper,"); + OutLn($"{exceptionArg},"); + OutLn($"__FUNC_{_memberCounter}_{lm.Name});"); + Unindent(); + + OutLn(); + OutLn($"{LogMethodHelperType}.ReturnHelper(_helper);"); + + OutCloseBrace(); + + OutLn(); + OutGeneratedCodeAttribute(); + OutLn($"private static string __FMT_{_memberCounter}_{lm.Name}(global::Microsoft.Extensions.Telemetry.Logging.LogMethodHelper _h, global::System.Exception? _)"); + OutOpenBrace(); + + if (GenVariableAssignments(lm, holderMap)) + { + OutLn($@"return global::System.FormattableString.Invariant($""{EscapeMessageString(lm.Message)}"");"); + } + else if (string.IsNullOrEmpty(lm.Message)) + { + OutLn($@"return string.Empty;"); + } + else + { + OutLn($@"return ""{EscapeMessageString(lm.Message)}"";"); + } + + OutCloseBrace(); + + OutLn(); + OutGeneratedCodeAttribute(); + OutLn($"private static readonly global::System.Func" + + $" __FUNC_{_memberCounter}_{lm.Name} = new(__FMT_{_memberCounter}_{lm.Name});"); + + static bool ShouldStringify(string typeName) + { + // well-known system types should not be stringified, since the logger may have special encodings for these + if (typeName.Contains(".")) + { + return !typeName.StartsWith("global::System", StringComparison.Ordinal); + } + + // a primitive type... + return false; + } + + static string ConvertToString(LoggingMethodParameter lp, string arg) + { + var question = lp.PotentiallyNull ? "?" : string.Empty; + if (lp.ImplementsIConvertible) + { + return $"{arg}{question}.ToString(global::System.Globalization.CultureInfo.InvariantCulture)"; + } + else if (lp.ImplementsIFormatable) + { + return $"{arg}{question}.ToString(null, global::System.Globalization.CultureInfo.InvariantCulture)"; + } + + return $"{arg}{question}.ToString()"; + } + + static string ConvertPropertyToString(LoggingProperty lp, string arg) + { + var question = lp.PotentiallyNull ? "?" : string.Empty; + if (lp.ImplementsIConvertible) + { + return $"{arg}{question}.ToString(global::System.Globalization.CultureInfo.InvariantCulture)"; + } + else if (lp.ImplementsIFormatable) + { + return $"{arg}{question}.ToString(null, global::System.Globalization.CultureInfo.InvariantCulture)"; + } + + return $"{arg}{question}.ToString()"; + } + + static string GetException(LoggingMethod lm) + { + string exceptionArg = "null"; + foreach (var p in lm.AllParameters) + { + if (p.IsException) + { + exceptionArg = p.Name; + break; + } + } + + return exceptionArg; + } + + bool GenVariableAssignments(LoggingMethod lm, Dictionary holderMap) + { + var result = false; + + foreach (var t in lm.TemplateMap) + { + int index = 0; + foreach (var p in lm.TemplateParameters) + { + if (t.Key.Equals(p.Name, StringComparison.OrdinalIgnoreCase)) + { + break; + } + + index++; + } + + // check for an index that's too big, this can happen in some cases of malformed input + if (index < lm.TemplateParameters.Count) + { + var parameter = lm.TemplateParameters[index]; + var atSign = parameter.NeedsAtSign ? "@" : string.Empty; + if (parameter.PotentiallyNull) + { + const string Null = "\"(null)\""; + OutLn($"var {atSign}{t.Key} = _h[{holderMap[parameter]}].Value ?? {Null};"); + result = true; + } + else + { + OutLn($"var {atSign}{t.Key} = _h[{holderMap[parameter]}].Value;"); + result = true; + } + } + } + + return result; + } + + static (string name, bool isNullable) GetLogger(LoggingMethod lm) + { + string logger = lm.LoggerField; + bool isNullable = lm.LoggerFieldNullable; + + foreach (var p in lm.AllParameters) + { + if (p.IsLogger) + { + logger = p.Name; + isNullable = p.IsNullable; + break; + } + } + + return (logger, isNullable); + } + + static (string name, bool isNullable) GetRedactorProvider(LoggingMethod lm) + { + string redactorProvider = lm.RedactorProviderField; + bool isNullable = lm.RedactorProviderFieldNullable; + + foreach (var p in lm.AllParameters) + { + if (p.IsRedactorProvider) + { + redactorProvider = p.Name; + isNullable = p.IsNullable; + break; + } + } + + return (redactorProvider, isNullable); + } + + void GenHelperAdd(LoggingMethod lm, Dictionary holderMap, LoggingMethodParameter p) + { + string key = $"\"{lm.GetParameterNameInTemplate(p)}\""; + + if (p.ClassificationAttributeType != null) + { + var dataClassVariableName = EncodeTypeName(p.ClassificationAttributeType); + + OutOpenBrace(); + OutLn($"var _v = {ConvertToString(p, p.NameWithAt)};"); + var value = $"_v != null ? _{dataClassVariableName}Redactor?.Redact(global::System.MemoryExtensions.AsSpan(_v)) : null"; + OutLn($"_helper.Add({key}, {value});"); + OutCloseBrace(); + } + else + { + if (p.IsEnumerable) + { + var value = p.PotentiallyNull + ? $"{p.NameWithAt} != null ? global::Microsoft.Extensions.Telemetry.Logging.LogMethodHelper.Stringify({p.NameWithAt}) : null" + : $"global::Microsoft.Extensions.Telemetry.Logging.LogMethodHelper.Stringify({p.NameWithAt})"; + + OutLn($"_helper.Add({key}, {value});"); + } + else + { + var value = ShouldStringify(p.Type) + ? ConvertToString(p, p.NameWithAt) + : p.NameWithAt; + + OutLn($"_helper.Add({key}, {value});"); + } + } + + holderMap.Add(p, holderMap.Count); + } + } + + private void GenParameters(LoggingMethod lm) + { + OutEnumeration(lm.AllParameters.Select(static p => + { + if (p.Qualifier != null) + { + return $"{p.Qualifier} {p.Type} {p.NameWithAt}"; + } + + return $"{p.Type} {p.NameWithAt}"; + })); + } + + private string PropertyChainToString( + IEnumerable propertyChain, + LoggingProperty leafProperty, + string separator, + string? nonNullSeparator = null, + bool omitParameterName = false) + { + bool needAts = nonNullSeparator == "."; + var adjustedNonNullSeparator = nonNullSeparator ?? separator; + var localStringBuilder = _sbPool.GetStringBuilder(); + try + { + int count = 0; + foreach (var property in propertyChain) + { + count++; + if (omitParameterName && count == 1) + { + continue; + } + + _ = localStringBuilder + .Append(needAts ? property.NameWithAt : property.Name) + .Append(property.PotentiallyNull ? separator : adjustedNonNullSeparator); + } + + // Last item: + _ = localStringBuilder.Append(needAts ? leafProperty.NameWithAt : leafProperty.Name); + + return localStringBuilder.ToString(); + } + finally + { + _sbPool.ReturnStringBuilder(localStringBuilder); + } + } + + private void GenRedactorsFetchingCode( + bool isRedactorProviderInTheInstance, + IEnumerable classificationAttributeTypes, + string redactorProvider, + bool isNullableRedactorProvider) + { + if (isRedactorProviderInTheInstance) + { + foreach (var classificationAttributeType in classificationAttributeTypes) + { + var dataClassVariableName = EncodeTypeName(classificationAttributeType); + + OutLn($"var _{dataClassVariableName}Redactor = __{dataClassVariableName}Redactor;"); + } + } + else + { + foreach (var classificationAttributeType in classificationAttributeTypes) + { + var classificationVariableName = EncodeTypeName(classificationAttributeType); + var attrClassificationFieldName = GetAttributeClassification(classificationAttributeType); + + if (isNullableRedactorProvider) + { + OutLn($"var _{classificationVariableName}Redactor = {redactorProvider}?.GetRedactor({attrClassificationFieldName});"); + } + else + { + OutLn($"var _{classificationVariableName}Redactor = {redactorProvider}.GetRedactor({attrClassificationFieldName});"); + } + } + } + } +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Emission/Emitter.Utils.cs b/src/Generators/Microsoft.Gen.Logging/Common/Emission/Emitter.Utils.cs new file mode 100644 index 0000000000..37b252d8a7 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Emission/Emitter.Utils.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Gen.Logging.Model; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.Logging.Emission; + +// Stryker disable all + +internal sealed partial class Emitter : EmitterBase +{ + private static readonly char[] _specialChars = { '\n', '\r', '"', '\\' }; + + internal static string EscapeMessageString(string s) + { + int index = s.IndexOfAny(_specialChars); + if (index < 0) + { + return s; + } + + var sb = new StringBuilder(s.Length); + _ = sb.Append(s, 0, index); + + while (index < s.Length) + { + _ = s[index] switch + { + '\n' => sb.Append("\\n"), + '\r' => sb.Append("\\r"), + '"' => sb.Append("\\\""), + '\\' => sb.Append("\\\\"), + var other => sb.Append(other), + }; + + index++; + } + + return sb.ToString(); + } + + private static readonly char[] _specialCharsForXmlDocumentation = { '\n', '\r', '<', '>' }; + + internal static string EscapeMessageStringForXmlDocumentation(string s) + { + int index = s.IndexOfAny(_specialCharsForXmlDocumentation); + if (index < 0) + { + return s; + } + + var sb = new StringBuilder(s.Length); + _ = sb.Append(s, 0, index); + + while (index < s.Length) + { + _ = s[index] switch + { + '\n' => sb.Append("\\n"), + '\r' => sb.Append("\\r"), + '<' => sb.Append("<"), + '>' => sb.Append(">"), + var other => sb.Append(other), + }; + + index++; + } + + return sb.ToString(); + } + + internal static IReadOnlyCollection GetLogPropertiesAttributes(LoggingMethod lm) + { + var result = new HashSet(); + var parametersWithLogProps = lm.AllParameters.Where(x => x.HasProperties && !x.HasPropsProvider); + foreach (var parameter in parametersWithLogProps) + { + parameter.TraverseParameterPropertiesTransitively((_, property) => result.Add(property.ClassificationAttributeType)); + } + + // Remove null values (no data classification attribute) + return result + .Where(x => x != null) + .Select(x => x!) + .ToArray(); + } + + internal static string GetLoggerMethodLogLevel(LoggingMethod lm) + { + string level = string.Empty; + + if (lm.Level == null) + { + foreach (var p in lm.AllParameters) + { + if (p.IsLogLevel) + { + level = p.Name; + break; + } + } + } + else + { + level = lm.Level switch + { +#pragma warning disable S109 // Magic numbers should not be used + 0 => "global::Microsoft.Extensions.Logging.LogLevel.Trace", + 1 => "global::Microsoft.Extensions.Logging.LogLevel.Debug", + 2 => "global::Microsoft.Extensions.Logging.LogLevel.Information", + 3 => "global::Microsoft.Extensions.Logging.LogLevel.Warning", + 4 => "global::Microsoft.Extensions.Logging.LogLevel.Error", + 5 => "global::Microsoft.Extensions.Logging.LogLevel.Critical", + 6 => "global::Microsoft.Extensions.Logging.LogLevel.None", + _ => $"(global::Microsoft.Extensions.Logging.LogLevel){lm.Level}", +#pragma warning restore S109 // Magic numbers should not be used + }; + } + + return level; + } + + internal static string? GetLoggerMethodLogLevelForXmlDocumentation(LoggingMethod lm) + { + string level = string.Empty; + + if (lm.Level == null) + { + return null; + } + + return lm.Level switch + { +#pragma warning disable S109 // Magic numbers should not be used + 0 => "Trace", + 1 => "Debug", + 2 => "Information", + 3 => "Warning", + 4 => "Error", + 5 => "Critical", + 6 => "None", + _ => $"{lm.Level}", +#pragma warning restore S109 // Magic numbers should not be used + }; + } + + internal static string EncodeTypeName(string typeName) => typeName.Replace("_", "__").Replace('.', '_'); +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Emission/Emitter.cs b/src/Generators/Microsoft.Gen.Logging/Common/Emission/Emitter.cs new file mode 100644 index 0000000000..482dc06373 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Emission/Emitter.cs @@ -0,0 +1,179 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Microsoft.Gen.Logging.Model; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.Logging.Emission; + +// Stryker disable all + +internal sealed partial class Emitter : EmitterBase +{ + private const string LogMethodHelperType = "global::Microsoft.Extensions.Telemetry.Logging.LogMethodHelper"; + + private readonly StringBuilderPool _sbPool = new(); + private bool _isRedactorProviderInTheInstance; + private int _memberCounter; + + public string Emit(IEnumerable logTypes, CancellationToken cancellationToken) + { + _isRedactorProviderInTheInstance = false; + _memberCounter = 0; + + foreach (var lt in logTypes.OrderBy(static lt => lt.Namespace + "." + lt.Name)) + { + cancellationToken.ThrowIfCancellationRequested(); + GenType(lt); + } + + return Capture(); + } + + private static string GetAttributeClassification(string classificationAttributeType) + { + var classificationVariableName = EncodeTypeName(classificationAttributeType); + + return $"_{classificationVariableName}_Classification"; + } + + private void GenType(LoggingType lt) + { + if (!string.IsNullOrWhiteSpace(lt.Namespace)) + { + OutLn(); + OutLn($"namespace {lt.Namespace}"); + OutOpenBrace(); + } + + var parent = lt.Parent; + var parentTypes = new List(); + + // loop until you find top level nested class + while (parent != null) + { + parentTypes.Add($"partial {parent.Keyword} {parent.Name}"); + parent = parent.Parent; + } + + // write down top level nested class first + for (int i = parentTypes.Count - 1; i >= 0; i--) + { + OutLn(parentTypes[i]); + OutOpenBrace(); + } + + OutLn($"partial {lt.Keyword} {lt.Name}"); + OutOpenBrace(); + + var isRedactionRequired = + lt.Methods + .SelectMany(static lm => lm.AllParameters) + .Any(static lp => lp.ClassificationAttributeType != null) + || lt.Methods + .SelectMany(static lm => GetLogPropertiesAttributes(lm)) + .Any(); + + if (isRedactionRequired) + { + _isRedactorProviderInTheInstance = lt.Methods + .SelectMany(static lm => lm.AllParameters) + .All(static lp => !lp.IsRedactorProvider); + + if (_isRedactorProviderInTheInstance) + { + GenRedactorProperties(lt); + } + + GenAttributeClassifications(lt); + } + + foreach (LoggingMethod lm in lt.Methods.OrderBy(static x => x.Name)) + { + GenLogMethod(lm); + _memberCounter++; + } + + OutCloseBrace(); + + parent = lt.Parent; + while (parent != null) + { + OutCloseBrace(); + parent = parent.Parent; + } + + if (!string.IsNullOrWhiteSpace(lt.Namespace)) + { + OutCloseBrace(); + } + } + + private void GenAttributeClassifications(LoggingType lt) + { + // Generates fields which contain the data clasification associated with each attribute used in the type + + var logPropsDataClasses = lt.Methods.SelectMany(lm => GetLogPropertiesAttributes(lm)); + var classificationAttributeTypes = lt.Methods + .SelectMany(static lm => lm.AllParameters) + .Where(static lp => lp.ClassificationAttributeType is not null) + .Select(static lp => lp.ClassificationAttributeType!) + .Concat(logPropsDataClasses) + .Distinct(); + + foreach (var classificationAttributeType in classificationAttributeTypes.OrderBy(static x => x)) + { + var attrClassificationFieldName = GetAttributeClassification(classificationAttributeType); + + OutGeneratedCodeAttribute(); + OutLn($"private static readonly Microsoft.Extensions.Compliance.Classification.DataClassification {attrClassificationFieldName} = new {classificationAttributeType}().Classification;"); + OutLn(); + } + } + + private void GenRedactorProperties(LoggingType lt) + { + const string RedactorType = "global::Microsoft.Extensions.Compliance.Redaction.Redactor"; + + var logPropsDataClasses = lt.Methods.SelectMany(lm => GetLogPropertiesAttributes(lm)); + var classificationAttributeTypes = lt.Methods + .SelectMany(static lm => lm.AllParameters) + .Where(static lp => lp.ClassificationAttributeType is not null) + .Select(static lp => lp.ClassificationAttributeType!) + .Concat(logPropsDataClasses) + .Distinct(); + + var redactorProviderVariableName = lt.Methods + .Select(static lm => lm.RedactorProviderField) + .Distinct() + .Single(); + + foreach (var classificationAttributeType in classificationAttributeTypes.OrderBy(static x => x)) + { + var classificationVariableName = EncodeTypeName(classificationAttributeType); + var attrClassificationFieldName = GetAttributeClassification(classificationAttributeType); + + OutGeneratedCodeAttribute(); + OutLn($"private {RedactorType}? ___{classificationVariableName}Redactor;"); + OutLn(); + + OutGeneratedCodeAttribute(); + OutLn($"private {RedactorType} __{classificationVariableName}Redactor"); + OutLn($"{{"); + OutLn($" get"); + OutLn($" {{"); + OutLn($" if (___{classificationVariableName}Redactor == null)"); + OutLn($" {{"); + OutLn($" ___{classificationVariableName}Redactor = {redactorProviderVariableName}?.GetRedactor({attrClassificationFieldName});"); + OutLn($" }}"); + OutLn(); + OutLn($" return ___{classificationVariableName}Redactor!;"); + OutLn($" }}"); + OutLn($"}}"); + } + } +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Emission/StringBuilderPool.cs b/src/Generators/Microsoft.Gen.Logging/Common/Emission/StringBuilderPool.cs new file mode 100644 index 0000000000..27b1d6b235 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Emission/StringBuilderPool.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Gen.Logging.Emission; + +internal sealed class StringBuilderPool +{ + private readonly Stack _builders = new(); + + public StringBuilder GetStringBuilder() + { + const int DefaultStringBuilderCapacity = 1024; + + if (_builders.Count == 0) + { + return new StringBuilder(DefaultStringBuilderCapacity); + } + + var sb = _builders.Pop(); + _ = sb.Clear(); + return sb; + } + + public void ReturnStringBuilder(StringBuilder sb) + { + _builders.Push(sb); + } +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Generator.cs b/src/Generators/Microsoft.Gen.Logging/Common/Generator.cs new file mode 100644 index 0000000000..1d7c0abde7 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Generator.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if ROSLYN_4_0_OR_GREATER + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.Logging; + +[Generator] +[ExcludeFromCodeCoverage] +public class Generator : IIncrementalGenerator +{ + private static readonly HashSet _attributeNames = new() + { + Parsing.SymbolLoader.LogMethodAttribute, + }; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + GeneratorUtilities.Initialize(context, _attributeNames, m => m.Parent as TypeDeclarationSyntax, HandleAnnotatedTypes); + } + + private static void HandleAnnotatedTypes(Compilation compilation, IEnumerable nodes, SourceProductionContext context) + { + var p = new Parsing.Parser(compilation, context.ReportDiagnostic, context.CancellationToken); + + var logTypes = p.GetLogTypes(nodes.OfType()); + if (logTypes.Count > 0) + { + var e = new Emission.Emitter(); + var result = e.Emit(logTypes, context.CancellationToken); + + context.AddSource("Logging.g.cs", SourceText.From(result, Encoding.UTF8)); + } + } +} + +#else + +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.Logging; + +[Generator] +[ExcludeFromCodeCoverage] +public class Generator : ISourceGenerator +{ + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(TypeDeclarationSyntaxReceiver.Create); + } + + public void Execute(GeneratorExecutionContext context) + { + var receiver = context.SyntaxReceiver as TypeDeclarationSyntaxReceiver; + if (receiver == null || receiver.TypeDeclarations.Count == 0) + { + // nothing to do yet + return; + } + + var p = new Parsing.Parser(context.Compilation, context.ReportDiagnostic, context.CancellationToken); + var logTypes = p.GetLogTypes(receiver.TypeDeclarations); + if (logTypes.Count > 0) + { + var e = new Emission.Emitter(); + var result = e.Emit(logTypes, context.CancellationToken); + + context.AddSource("Logging.g.cs", SourceText.From(result, Encoding.UTF8)); + } + } +} + +#endif diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingMethod.cs b/src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingMethod.cs new file mode 100644 index 0000000000..1b6be7098d --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingMethod.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Gen.Logging.Model; + +/// +/// A logger method in a logger type. +/// +internal sealed class LoggingMethod +{ + public readonly List AllParameters = new(); + public readonly List TemplateParameters = new(); + public readonly Dictionary TemplateMap = new(StringComparer.OrdinalIgnoreCase); + public readonly List TemplateList = new(); + public string Name = string.Empty; + public string Message = string.Empty; + public int? Level; + public int? EventId; + public string? EventName; + public bool SkipEnabledCheck; + public bool IsExtensionMethod; + public bool IsStatic; + public string Modifiers = string.Empty; + public string LoggerField = "_logger"; + public string RedactorProviderField = "_redactorProvider"; + public bool LoggerFieldNullable; + public bool RedactorProviderFieldNullable; + public bool HasXmlDocumentation; + + public string GetParameterNameInTemplate(LoggingMethodParameter parameter) + => TemplateMap.TryGetValue(parameter.Name, out var value) + ? value + : parameter.Name; +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingMethodParameter.cs b/src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingMethodParameter.cs new file mode 100644 index 0000000000..18a5836723 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingMethodParameter.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.Gen.Logging.Model; + +/// +/// A single parameter to a logger method. +/// +[DebuggerDisplay("{Name}")] +internal sealed class LoggingMethodParameter +{ + public string Name = string.Empty; + public string Type = string.Empty; + public string? Qualifier; + public bool NeedsAtSign; + public bool IsLogger; + public bool IsRedactorProvider; + public bool IsException; + public bool IsLogLevel; + public bool IsEnumerable; + public bool IsNullable; + public bool IsReference; + public bool ImplementsIConvertible; + public bool ImplementsIFormatable; + public bool SkipNullProperties; + public bool OmitParameterName; + public string? ClassificationAttributeType; + public List PropertiesToLog = new(); + public LoggingPropertyProvider? LogPropertiesProvider; + + public string NameWithAt => NeedsAtSign ? "@" + Name : Name; + public string PotentiallyNullableType => (IsReference && !IsNullable) ? Type + "?" : Type; + + // A parameter flagged as 'normal' is not going to be taken care of specially as an argument to ILogger.Log + // but instead is supposed to be taken as a normal parameter. + public bool IsNormalParameter => !IsLogger && !IsRedactorProvider && !IsException && !IsLogLevel; + + public bool HasProperties => PropertiesToLog.Count > 0; + public bool HasPropsProvider => LogPropertiesProvider is not null; + public bool PotentiallyNull => (IsReference && !IsLogger && !IsRedactorProvider) || IsNullable; +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingMethodParameterExtensions.cs b/src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingMethodParameterExtensions.cs new file mode 100644 index 0000000000..442b1cc916 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingMethodParameterExtensions.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Gen.Logging.Model; + +internal static class LoggingMethodParameterExtensions +{ + internal static void TraverseParameterPropertiesTransitively( + this LoggingMethodParameter parameter, + Action, LoggingProperty> callback) + { + var propertyChain = new LinkedList(); + + LoggingProperty firstProperty = new( + parameter.NameWithAt, + parameter.Type, + null, + false, + parameter.IsNullable, + parameter.IsReference, + parameter.IsEnumerable, + false, + false, + Array.Empty()); + + _ = propertyChain.AddFirst(firstProperty); + + TraverseParameterPropertiesTransitively(propertyChain, parameter.PropertiesToLog, callback); + } + + private static void TraverseParameterPropertiesTransitively( + LinkedList propertyChain, + IReadOnlyCollection propertiesToLog, + Action, LoggingProperty> callback) + { + foreach (var propertyToLog in propertiesToLog) + { + if (propertyToLog.TransitiveMembers.Count > 0) + { + _ = propertyChain.AddLast(propertyToLog); + TraverseParameterPropertiesTransitively(propertyChain, propertyToLog.TransitiveMembers, callback); + propertyChain.RemoveLast(); + } + else + { + callback(propertyChain, propertyToLog); + } + } + } +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingProperty.cs b/src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingProperty.cs new file mode 100644 index 0000000000..e80a1a1123 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingProperty.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Gen.Logging.Model; + +[DebuggerDisplay("{Name}")] +[ExcludeFromCodeCoverage] +internal sealed record LoggingProperty( + string Name, + string Type, + string? ClassificationAttributeType, + bool NeedsAtSign, + bool IsNullable, + bool IsReference, + bool IsEnumerable, + bool ImplementsIConvertible, + bool ImplementsIFormatable, + IReadOnlyCollection TransitiveMembers) +{ + public string NameWithAt => NeedsAtSign ? "@" + Name : Name; + public bool PotentiallyNull => IsReference || IsNullable; +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingPropertyProvider.cs b/src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingPropertyProvider.cs new file mode 100644 index 0000000000..fecafe5a78 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingPropertyProvider.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Gen.Logging.Model; + +[ExcludeFromCodeCoverage] +internal sealed record class LoggingPropertyProvider( + string MethodName, + string ContainingType); diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingType.cs b/src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingType.cs new file mode 100644 index 0000000000..93bbca43ad --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Model/LoggingType.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Gen.Logging.Model; + +/// +/// A logger class/struct/record holding a bunch of logger methods. +/// +internal sealed class LoggingType +{ + public readonly List Methods = new(); + public string Keyword = string.Empty; + public string Namespace = string.Empty; + public string Name = string.Empty; + public LoggingType? Parent; +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Parsing/AttributeProcessors.cs b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/AttributeProcessors.cs new file mode 100644 index 0000000000..92c70b1b96 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/AttributeProcessors.cs @@ -0,0 +1,117 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Gen.Logging.Parsing; + +internal static class AttributeProcessors +{ + private const string EventNameProperty = "EventName"; + private const string SkipEnabledCheckProperty = "SkipEnabledCheck"; + private const string SkipNullProperties = "SkipNullProperties"; + private const string OmitParameterName = "OmitParameterName"; + + private const int LogLevelError = 4; + private const int LogLevelCritical = 5; + + public static (int? eventId, int? level, string message, string? eventName, bool skipEnabledCheck) + ExtractLogMethodAttributeValues(AttributeData attr, SymbolHolder symbols) + { + // seven constructor arg shapes: + // + // (int eventId, LogLevel level, string message) + // (int eventId, LogLevel level) + // (LogLevel level, string message) + // (LogLevel level) + // (string message) + // (int eventId, string message) + // () + + int? eventId = null; + int? level = null; + string? eventName = null; + string message = string.Empty; + bool skipEnabledCheck = false; + bool useDefaultForSkipEnabledCheck = true; + + foreach (var a in attr.ConstructorArguments) + { + if (SymbolEqualityComparer.Default.Equals(a.Type, symbols.LogLevelSymbol)) + { + var v = a.Value; + if (v is int l) + { + level = l; + } + } + else if (a.Type != null && a.Type.SpecialType == SpecialType.System_Int32) + { + var v = a.Value; + if (v is int l) + { + eventId = l; + } + } + else + { + message = a.Value as string ?? string.Empty; + } + } + + foreach (var a in attr.NamedArguments) + { + switch (a.Key) + { + case EventNameProperty: + eventName = a.Value.Value as string; + break; + + case SkipEnabledCheckProperty: + skipEnabledCheck = (bool)a.Value.Value!; + useDefaultForSkipEnabledCheck = false; + break; + } + } + + if (level != null) + { + if (useDefaultForSkipEnabledCheck && (level == LogLevelError || level == LogLevelCritical)) + { + // unless explicitly set by the user, by default we disable the Enabled check when the log level is Error or Critical + skipEnabledCheck = true; + } + } + + return (eventId, level, message, eventName, skipEnabledCheck); + } + + public static (bool skipNullProperties, bool omitParameterName, ITypeSymbol? providerType, string? providerMethodName) + ExtractLogPropertiesAttributeValues(AttributeData attr) + { + bool skipNullProperties = false; + bool omitParameterName = false; + ITypeSymbol? providerType = null; + string? providerMethodName = null; + + foreach (var a in attr.NamedArguments) + { + if (a.Key == SkipNullProperties) + { + skipNullProperties = (bool)a.Value.Value!; + } + else if (a.Key == OmitParameterName) + { + omitParameterName = (bool)a.Value.Value!; + } + } + + if (attr.ConstructorArguments.Length == 2) + { + providerType = attr.ConstructorArguments[0].Value as ITypeSymbol; + providerMethodName = attr.ConstructorArguments[1].Value as string; + } + + return (skipNullProperties, omitParameterName, providerType, providerMethodName); + } +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Parsing/DiagDescriptors.cs b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/DiagDescriptors.cs new file mode 100644 index 0000000000..2571676d72 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/DiagDescriptors.cs @@ -0,0 +1,260 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.Logging.Parsing; + +internal sealed class DiagDescriptors : DiagDescriptorsBase +{ + private const string Category = "LogMethod"; + + public static DiagnosticDescriptor InvalidLoggingMethodName { get; } = Make( + id: "R9G000", + title: Resources.InvalidLoggingMethodNameTitle, + messageFormat: Resources.InvalidLoggingMethodNameMessage, + category: Category); + + public static DiagnosticDescriptor ShouldntMentionLogLevelInMessage { get; } = Make( + id: "R9G001", + title: Resources.ShouldntMentionLogLevelInMessageTitle, + messageFormat: Resources.ShouldntMentionLogLevelInMessageMessage, + category: Category, + DiagnosticSeverity.Warning); + + public static DiagnosticDescriptor InvalidLoggingMethodParameterName { get; } = Make( + id: "R9G002", + title: Resources.InvalidLoggingMethodParameterNameTitle, + messageFormat: Resources.InvalidLoggingMethodParameterNameMessage, + category: Category); + + // R9G003 is no longer in use + + public static DiagnosticDescriptor MissingRequiredType { get; } = Make( + id: "R9G004", + title: Resources.MissingRequiredTypeTitle, + messageFormat: Resources.MissingRequiredTypeMessage, + category: Category); + + public static DiagnosticDescriptor ShouldntReuseEventIds { get; } = Make( + id: "R9G005", + title: Resources.ShouldntReuseEventIdsTitle, + messageFormat: Resources.ShouldntReuseEventIdsMessage, + category: Category, + DiagnosticSeverity.Warning); + + public static DiagnosticDescriptor LoggingMethodMustReturnVoid { get; } = Make( + id: "R9G006", + title: Resources.LoggingMethodMustReturnVoidTitle, + messageFormat: Resources.LoggingMethodMustReturnVoidMessage, + category: Category); + + public static DiagnosticDescriptor MissingLoggerArgument { get; } = Make( + id: "R9G007", + title: Resources.MissingLoggerArgumentTitle, + messageFormat: Resources.MissingLoggerArgumentMessage, + category: Category); + + public static DiagnosticDescriptor LoggingMethodShouldBeStatic { get; } = Make( + id: "R9G008", + title: Resources.LoggingMethodShouldBeStaticTitle, + messageFormat: Resources.LoggingMethodShouldBeStaticMessage, + category: Category, + DiagnosticSeverity.Warning); + + public static DiagnosticDescriptor LoggingMethodMustBePartial { get; } = Make( + id: "R9G009", + title: Resources.LoggingMethodMustBePartialTitle, + messageFormat: Resources.LoggingMethodMustBePartialMessage, + category: Category); + + public static DiagnosticDescriptor LoggingMethodIsGeneric { get; } = Make( + id: "R9G010", + title: Resources.LoggingMethodIsGenericTitle, + messageFormat: Resources.LoggingMethodIsGenericMessage, + category: Category); + + public static DiagnosticDescriptor RedundantQualifierInMessage { get; } = Make( + id: "R9G011", + title: Resources.RedundantQualifierInMessageTitle, + messageFormat: Resources.RedundantQualifierInMessageMessage, + category: Category, + DiagnosticSeverity.Warning); + + public static DiagnosticDescriptor ShouldntMentionExceptionInMessage { get; } = Make( + id: "R9G012", + title: Resources.ShouldntMentionExceptionInMessageTitle, + messageFormat: Resources.ShouldntMentionExceptionInMessageMessage, + category: Category, + DiagnosticSeverity.Warning); + + public static DiagnosticDescriptor TemplateHasNoCorrespondingArgument { get; } = Make( + id: "R9G013", + title: Resources.TemplateHasNoCorrespondingArgumentTitle, + messageFormat: Resources.TemplateHasNoCorrespondingArgumentMessage, + category: Category); + + public static DiagnosticDescriptor ArgumentHasNoCorrespondingTemplate { get; } = Make( + id: "R9G014", + title: Resources.ArgumentHasNoCorrespondingTemplateTitle, + messageFormat: Resources.ArgumentHasNoCorrespondingTemplateMessage, + category: Category, + DiagnosticSeverity.Info); + + public static DiagnosticDescriptor LoggingMethodHasBody { get; } = Make( + id: "R9G015", + title: Resources.LoggingMethodHasBodyTitle, + messageFormat: Resources.LoggingMethodHasBodyMessage, + category: Category); + + public static DiagnosticDescriptor MissingLogLevel { get; } = Make( + id: "R9G016", + title: Resources.MissingLogLevelTitle, + messageFormat: Resources.MissingLogLevelMessage, + category: Category); + + public static DiagnosticDescriptor ShouldntMentionLoggerInMessage { get; } = Make( + id: "R9G017", + title: Resources.ShouldntMentionLoggerInMessageTitle, + messageFormat: Resources.ShouldntMentionLoggerInMessageMessage, + category: Category, + DiagnosticSeverity.Warning); + + public static DiagnosticDescriptor MissingLoggerField { get; } = Make( + id: "R9G018", + title: Resources.MissingLoggerFieldTitle, + messageFormat: Resources.MissingLoggerFieldMessage, + category: Category); + + public static DiagnosticDescriptor MultipleLoggerFields { get; } = Make( + id: "R9G019", + title: Resources.MultipleLoggerFieldsTitle, + messageFormat: Resources.MultipleLoggerFieldsMessage, + category: Category); + + public static DiagnosticDescriptor MultipleDataClassificationAttributes { get; } = Make( + id: "R9G020", + title: Resources.MultipleDataClassificationAttributesTitle, + messageFormat: Resources.MultipleDataClassificationAttributesMessage, + category: Category); + + public static DiagnosticDescriptor MissingRedactorProviderArgument { get; } = Make( + id: "R9G021", + title: Resources.MissingRedactorProviderArgumentTitle, + messageFormat: Resources.MissingRedactorProviderArgumentMessage, + category: Category); + + public static DiagnosticDescriptor MissingDataClassificationArgument { get; } = Make( + id: "R9G022", + title: Resources.MissingDataClassificationArgumentTitle, + messageFormat: Resources.MissingDataClassificationArgumentMessage, + category: Category, + DiagnosticSeverity.Warning); + + public static DiagnosticDescriptor MissingRedactorProviderField { get; } = Make( + id: "R9G023", + title: Resources.MissingRedactorProviderFieldTitle, + messageFormat: Resources.MissingRedactorProviderFieldMessage, + category: Category); + + public static DiagnosticDescriptor MultipleRedactorProviderFields { get; } = Make( + id: "R9G024", + title: Resources.MultipleRedactorProviderFieldsTitle, + messageFormat: Resources.MultipleRedactorProviderFieldsMessage, + category: Category); + + public static DiagnosticDescriptor InvalidTypeToLogProperties { get; } = Make( + id: "R9G025", + title: Resources.InvalidTypeToLogPropertiesTitle, + messageFormat: Resources.InvalidTypeToLogPropertiesMessage, + category: Category, + DiagnosticSeverity.Warning); + + // Skipping R9G026 + + public static DiagnosticDescriptor LogPropertiesInvalidUsage { get; } = Make( + id: "R9G027", + title: Resources.LogPropertiesInvalidUsageTitle, + messageFormat: Resources.LogPropertiesInvalidUsageMessage, + category: Category); + + public static DiagnosticDescriptor LogPropertiesParameterSkipped { get; } = Make( + id: "R9G028", + title: Resources.LogPropertiesParameterSkippedTitle, + messageFormat: Resources.LogPropertiesParameterSkippedMessage, + category: Category, + DiagnosticSeverity.Warning); + + public static DiagnosticDescriptor LogPropertiesCycleDetected { get; } = Make( + id: "R9G029", + title: Resources.LogPropertiesCycleDetectedTitle, + messageFormat: Resources.LogPropertiesCycleDetectedMessage, + category: Category); + + // Skipping R9G030 + // Skipping R9G031 + + public static DiagnosticDescriptor LogPropertiesProviderMethodNotFound { get; } = Make( + id: "R9G032", + title: Resources.LogPropertiesProviderMethodNotFoundTitle, + messageFormat: Resources.LogPropertiesProviderMethodNotFoundMessage, + category: Category); + + // Skipping R9G033 + + public static DiagnosticDescriptor LogPropertiesProviderMethodInaccessible { get; } = Make( + id: "R9G034", + title: Resources.LogPropertiesProviderMethodInaccessibleTitle, + messageFormat: Resources.LogPropertiesProviderMethodInaccessibleMessage, + category: Category); + + public static DiagnosticDescriptor LogPropertiesProviderMethodInvalidSignature { get; } = Make( + id: "R9G035", + title: Resources.LogPropertiesProviderMethodInvalidSignatureTitle, + messageFormat: Resources.LogPropertiesProviderMethodInvalidSignatureMessage, + category: Category); + + // Skipping R9G036 + // Skipping R9G037 + + public static DiagnosticDescriptor LoggingMethodParameterRefKind { get; } = Make( + id: "R9G038", + title: Resources.LoggingMethodParameterRefKindTitle, + messageFormat: Resources.LoggingMethodParameterRefKindMessage, + category: Category); + + public static DiagnosticDescriptor LogPropertiesProviderWithRedaction { get; } = Make( + id: "R9G039", + title: Resources.LogPropertiesProviderWithRedactionTitle, + messageFormat: Resources.LogPropertiesProviderWithRedactionMessage, + category: Category, + DiagnosticSeverity.Warning); + + public static DiagnosticDescriptor ShouldntReuseEventNames { get; } = Make( + id: "R9G040", + title: Resources.ShouldntReuseEventNamesTitle, + messageFormat: Resources.ShouldntReuseEventNamesMessage, + category: Category, + DiagnosticSeverity.Warning); + + // R9G041 is no longer in use + + public static DiagnosticDescriptor LogPropertiesHiddenPropertyDetected { get; } = Make( + id: "R9G042", + title: Resources.LogPropertiesHiddenPropertyDetectedTitle, + messageFormat: Resources.LogPropertiesHiddenPropertyDetectedMessage, + category: Category); + + public static DiagnosticDescriptor LogPropertiesNameCollision { get; } = Make( + id: "R9G043", + title: Resources.LogPropertiesNameCollisionTitle, + messageFormat: Resources.LogPropertiesNameCollisionMessage, + category: Category); + + public static DiagnosticDescriptor EmptyLoggingMethod { get; } = Make( + id: "R9G044", + title: Resources.EmptyLoggingMethodTitle, + messageFormat: Resources.EmptyLoggingMethodMessage, + category: Category); +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Parsing/LogParserUtilities.cs b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/LogParserUtilities.cs new file mode 100644 index 0000000000..05241b2061 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/LogParserUtilities.cs @@ -0,0 +1,388 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Gen.Logging.Model; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.Logging.Parsing; + +internal static class LogParserUtilities +{ + private static readonly HashSet _allowedParameterTypeKinds = new() { TypeKind.Class, TypeKind.Struct, TypeKind.Interface }; + + internal static bool IsEnumerable(ITypeSymbol sym, SymbolHolder symbols) + => sym.ImplementsInterface(symbols.EnumerableSymbol) && sym.SpecialType != SpecialType.System_String; + + internal static bool ImplementsIConvertible(ITypeSymbol sym, SymbolHolder symbols) + { + foreach (var member in sym.GetMembers("ToString")) + { + if (member is IMethodSymbol ts) + { + if (ts.DeclaredAccessibility == Accessibility.Public) + { + if (ts.Arity == 0 + && ts.Parameters.Length == 1 + && SymbolEqualityComparer.Default.Equals(ts.Parameters[0].Type, symbols.FormatProviderSymbol)) + { + return true; + } + } + } + } + + return false; + } + + internal static bool ImplementsIFormatable(ITypeSymbol sym, SymbolHolder symbols) + { + foreach (var member in sym.GetMembers("ToString")) + { + if (member is IMethodSymbol ts) + { + if (ts.DeclaredAccessibility == Accessibility.Public) + { + if (ts.Arity == 0 + && ts.Parameters.Length == 2 + && ts.Parameters[0].Type.SpecialType == SpecialType.System_String + && SymbolEqualityComparer.Default.Equals(ts.Parameters[1].Type, symbols.FormatProviderSymbol)) + { + return true; + } + } + } + } + + return false; + } + +#pragma warning disable S107 // Methods should not have too many parameters + internal static LogPropertiesProcessingResult ProcessLogPropertiesForParameter( + AttributeData logPropertiesAttribute, + LoggingMethod lm, + LoggingMethodParameter lp, + IParameterSymbol paramSymbol, + SymbolHolder symbols, + Action diagCallback, + Compilation comp, + CancellationToken token) +#pragma warning restore S107 // Methods should not have too many parameters + { + var paramName = paramSymbol.Name; + if (!lp.IsNormalParameter) + { + Diag(DiagDescriptors.LogPropertiesInvalidUsage, paramSymbol.GetLocation(), paramName); + return LogPropertiesProcessingResult.Fail; + } + + var paramTypeSymbol = paramSymbol.Type; + var isRegularType = + paramTypeSymbol.Kind == SymbolKind.NamedType && + _allowedParameterTypeKinds.Contains(paramTypeSymbol.TypeKind) && + !paramTypeSymbol.IsStatic; + + if (paramTypeSymbol.IsNullableOfT()) + { + // extract the T from a Nullable + paramTypeSymbol = ((INamedTypeSymbol)paramTypeSymbol).TypeArguments[0]; + } + + var isObjectWithPropsProvider = IsObjectType(paramTypeSymbol) && + !logPropertiesAttribute.ConstructorArguments.IsDefaultOrEmpty; + + if (!isRegularType || + (IsSpecialType(paramTypeSymbol, symbols) && !isObjectWithPropsProvider)) + { + Diag(DiagDescriptors.InvalidTypeToLogProperties, paramSymbol.GetLocation(), paramTypeSymbol.ToDisplayString()); + return LogPropertiesProcessingResult.Fail; + } + + (lp.SkipNullProperties, lp.OmitParameterName, var providerType, var providerMethodName) = AttributeProcessors.ExtractLogPropertiesAttributeValues(logPropertiesAttribute); + + // is there a custom property provider? + if (providerType != null) + { + var providerMethod = LogPropertiesProviderValidator.Validate( + providerType, + providerMethodName, + symbols.ILogPropertyCollectorSymbol, + paramTypeSymbol, + Diag, + logPropertiesAttribute.ApplicationSyntaxReference!.GetSyntax(token).GetLocation(), + comp); + + if (providerMethod is not null) + { + lp.LogPropertiesProvider = + new LoggingPropertyProvider( + providerMethod.Name, + providerType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + + return LogPropertiesProcessingResult.Succeeded; + } + + return LogPropertiesProcessingResult.Fail; + } + + var foundDataClassificationAnnotation = false; + + // No custom properties provider, we'll emit properties by ourselves: + var typesChain = new HashSet(SymbolEqualityComparer.Default); + _ = typesChain.Add(paramTypeSymbol); // Add itself + try + { + var props = GetTypePropertiesToLog(paramTypeSymbol, typesChain, symbols, ref foundDataClassificationAnnotation, token); + if (props.Count > 0) + { + lp.PropertiesToLog.AddRange(props); + } + else + { + Diag(DiagDescriptors.LogPropertiesParameterSkipped, paramSymbol.GetLocation(), paramTypeSymbol.Name, paramName); + return LogPropertiesProcessingResult.Succeeded; + } + } + catch (TransitiveTypeCycleException ex) + { + Diag(DiagDescriptors.LogPropertiesCycleDetected, paramSymbol.GetLocation(), paramName, ex.NamedType.ToDisplayString(), ex.Property.Type.ToDisplayString(), lm.Name); + return LogPropertiesProcessingResult.Fail; + } + catch (MultipleDataClassesAppliedException ex) + { + Diag(DiagDescriptors.MultipleDataClassificationAttributes, ex.Property.GetLocation(), null); + return LogPropertiesProcessingResult.Fail; + } + catch (PropertyHiddenException ex) + { + Diag(DiagDescriptors.LogPropertiesHiddenPropertyDetected, paramSymbol.GetLocation(), paramName, lm.Name, ex.Property.Name); + return LogPropertiesProcessingResult.Fail; + } + + return foundDataClassificationAnnotation + ? LogPropertiesProcessingResult.SucceededWithRedaction + : LogPropertiesProcessingResult.Succeeded; + + void Diag(DiagnosticDescriptor desc, Location? loc, params object?[]? args) => diagCallback(desc, loc, args); + } + + internal static void CheckMethodParametersAreUnique( + LoggingMethod lm, + Action diagCallback, + Dictionary parameterSymbols) + { + var names = new HashSet(StringComparer.Ordinal); + foreach (var parameter in lm.AllParameters) + { + var parameterName = lm.GetParameterNameInTemplate(parameter); + if (!names.Add(parameterName)) + { + Diag(DiagDescriptors.LogPropertiesNameCollision, parameterSymbols[parameter].GetLocation(), parameter.Name, parameterName, lm.Name); + } + + if (parameter.HasProperties) + { + parameter.TraverseParameterPropertiesTransitively((chain, leaf) => + { + if (parameter.OmitParameterName) + { + chain = chain.Skip(1); + } + + var fullName = string.Join("_", chain.Concat(new[] { leaf }).Select(static x => x.Name)); + if (!names.Add(fullName)) + { + Diag(DiagDescriptors.LogPropertiesNameCollision, parameterSymbols[parameter].GetLocation(), parameter.Name, fullName, lm.Name); + } + }); + } + } + + void Diag(DiagnosticDescriptor desc, Location? loc, params object[]? args) => diagCallback(desc, loc, args); + } + + // Returns all the classification attributes attached to a symbol. + internal static IReadOnlyList GetDataClassificationAttributes(this ISymbol symbol, SymbolHolder symbols) + => symbol + .GetAttributes() + .Select(static attribute => attribute.AttributeClass) + .Where(x => x is not null && symbols.DataClassificationAttribute is not null && ParserUtilities.IsBaseOrIdentity(x, symbols.DataClassificationAttribute, symbols.Compilation)) + .Select(static x => x!) + .ToList(); + + private static bool IsSpecialType(ITypeSymbol typeSymbol, SymbolHolder symbols) + => typeSymbol.SpecialType != SpecialType.None || + typeSymbol.OriginalDefinition.SpecialType != SpecialType.None || +#pragma warning disable RS1024 + symbols.IgnorePropertiesSymbols.Contains(typeSymbol); +#pragma warning restore RS1024 + + private static bool IsObjectType(ITypeSymbol typeSymbol) + => typeSymbol.SpecialType == SpecialType.System_Object || + typeSymbol.OriginalDefinition.SpecialType == SpecialType.System_Object; + + private static List GetTypePropertiesToLog( + ITypeSymbol type, + ISet typesChain, + SymbolHolder symbols, + ref bool foundDataClassificationAnnotation, + CancellationToken token) + { + var result = new List(); + var overriddenMembers = new HashSet(); + var namedType = type as INamedTypeSymbol; + while (namedType != null && namedType.SpecialType != SpecialType.System_Object) + { + token.ThrowIfCancellationRequested(); + var members = namedType.GetMembers(); + if (members != null) + { + foreach (var m in members) + { + // Skip static, non-public or non-properties members: + if (m.Kind != SymbolKind.Property || + m.DeclaredAccessibility != Accessibility.Public || + m.IsStatic) + { + continue; + } + + // Skip properties annotated with [LogPropertyIgnore] + if (m.GetAttributes().Any(x => SymbolEqualityComparer.Default.Equals(symbols.LogPropertyIgnoreAttribute, x.AttributeClass))) + { + continue; + } + + // Skip write-only properties and ones with non-public getter: + if (m is not IPropertySymbol property || + property.GetMethod == null || + property.GetMethod.DeclaredAccessibility != Accessibility.Public) + { + continue; + } + + // Skip properties with delegate types: + var propertyType = property.Type; + if (propertyType.TypeKind == TypeKind.Delegate) + { + continue; + } + + // Property is already overridden in derived class, skip it: + if ((property.IsVirtual || property.IsOverride) && + overriddenMembers.Contains(property.Name)) + { + continue; + } + + var extractedType = propertyType; + if (propertyType.IsNullableOfT()) + { + // extract the T from a Nullable + extractedType = ((INamedTypeSymbol)propertyType).TypeArguments[0]; + } + + // Checking "extractedType" here: + if (typesChain.Contains(extractedType)) + { + throw new TransitiveTypeCycleException(property, namedType); // Interrupt the whole traversal + } + + // Adding property that overrides some base class virtual property: + if (property.IsOverride) + { + _ = overriddenMembers.Add(property.Name); + } + + // Check if this property hides a base property: + if (ParserUtilities.PropertyHasModifier(property, SyntaxKind.NewKeyword, token)) + { + throw new PropertyHiddenException(property); // Interrupt the whole traversal + } + + bool isEnumerable = IsEnumerable(propertyType, symbols); + bool implementsIConvertible = ImplementsIConvertible(propertyType, symbols); + bool implementsIFormatable = ImplementsIFormatable(propertyType, symbols); + + bool propertyHasComplexType = +#pragma warning disable CA1508 // Avoid dead conditional code + extractedType.Kind == SymbolKind.NamedType && +#pragma warning restore CA1508 // Avoid dead conditional code + !IsSpecialType(extractedType, symbols) && + !isEnumerable && + extractedType.DeclaredAccessibility == Accessibility.Public; // Ignore non-public types for transitive traversal + + IReadOnlyCollection transitiveMembers = Array.Empty(); + + INamedTypeSymbol? classificationAttributeType = null; + + if (propertyHasComplexType) + { + _ = typesChain.Add(namedType); + transitiveMembers = GetTypePropertiesToLog( + extractedType, + typesChain, + symbols, + ref foundDataClassificationAnnotation, + token); + + _ = typesChain.Remove(namedType); + } + else + { + var propertyDataClassAttributes = property.GetDataClassificationAttributes(symbols); + if (propertyDataClassAttributes.Count > 1) + { + throw new MultipleDataClassesAppliedException(property); // Interrupt the whole traversal + } + else + { + if (propertyDataClassAttributes.Count == 1) + { + classificationAttributeType = propertyDataClassAttributes[0]; + + foundDataClassificationAnnotation = true; + } + } + } + + var needsAtSign = false; + if (!property.DeclaringSyntaxReferences.IsDefaultOrEmpty) + { + var propertySyntax = property.DeclaringSyntaxReferences[0].GetSyntax(token) as PropertyDeclarationSyntax; + if (!string.IsNullOrEmpty(propertySyntax!.Identifier.Text)) + { + needsAtSign = propertySyntax!.Identifier.Text[0] == '@'; + } + } + + // Using here original "propertyType": + LoggingProperty propertyToLog = new( + property.Name, + propertyType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + classificationAttributeType?.ToDisplayString(), + needsAtSign, + propertyType.NullableAnnotation == NullableAnnotation.Annotated, + propertyType.IsReferenceType, + isEnumerable, + implementsIConvertible, + implementsIFormatable, + transitiveMembers); + + result.Add(propertyToLog); + } + } + + // This is to support inheritance, i.e. to fetch public properties of base class(-es) as well: + namedType = namedType.BaseType; + } + + return result; + } +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Parsing/LogPropertiesProcessingResult.cs b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/LogPropertiesProcessingResult.cs new file mode 100644 index 0000000000..9dadb44fe1 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/LogPropertiesProcessingResult.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Gen.Logging.Parsing; + +internal enum LogPropertiesProcessingResult +{ + Fail, + Succeeded, + SucceededWithRedaction, +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Parsing/LogPropertiesProviderValidator.cs b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/LogPropertiesProviderValidator.cs new file mode 100644 index 0000000000..b19ff6a626 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/LogPropertiesProviderValidator.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.Logging.Parsing; + +internal static class LogPropertiesProviderValidator +{ + internal delegate void DiagCallback(DiagnosticDescriptor desc, Location? loc, params object?[]? args); + + public static IMethodSymbol? Validate( + ITypeSymbol providerType, + string? providerMethodName, + ITypeSymbol logPropertyCollectorType, + ITypeSymbol complexObjType, + DiagCallback diagCallback, + Location? attrLocation, + Compilation comp) + { + if (providerType is IErrorTypeSymbol) + { + return null; + } + + if (providerMethodName != null) + { + var methodSymbols = providerType.GetMembers(providerMethodName).Where(m => m.Kind == SymbolKind.Method).Cast(); + bool visitedLoop = false; + foreach (var method in methodSymbols) + { + visitedLoop = true; + +#pragma warning disable S1067 // Expressions should not be too complex + if (method.IsStatic + && method.ReturnsVoid + && !method.IsGenericMethod + && IsParameterCountValid(method) + && method.Parameters[0].RefKind == RefKind.None + && method.Parameters[1].RefKind == RefKind.None + && SymbolEqualityComparer.Default.Equals(logPropertyCollectorType, method.Parameters[0].Type) + && complexObjType.IsAssignableTo(method.Parameters[1].Type, comp)) +#pragma warning restore S1067 // Expressions should not be too complex + { + if (IsProviderMethodVisible(method)) + { + return method; + } + + diagCallback(DiagDescriptors.LogPropertiesProviderMethodInaccessible, attrLocation, providerMethodName, providerType.ToString()); + return null; + } + } + + if (visitedLoop) + { + diagCallback(DiagDescriptors.LogPropertiesProviderMethodInvalidSignature, attrLocation, + providerMethodName, + providerType.ToString(), + $"static void {providerMethodName}(ILogPropertyCollector, {complexObjType.Name})"); + return null; + } + } + + diagCallback(DiagDescriptors.LogPropertiesProviderMethodNotFound, attrLocation, providerMethodName, providerType.ToString()); + return null; + } + + private static bool IsParameterCountValid(IMethodSymbol method) + { + if (method.Parameters.Length == 2) + { + return true; + } + + if (method.Parameters.Length < 2) + { + return false; + } + + for (int i = 2; i < method.Parameters.Length; i++) + { + if (!method.Parameters[i].IsOptional) + { + return false; + } + } + + return true; + } + + private static bool IsAssignableTo(this ITypeSymbol type, ITypeSymbol target, Compilation comp) + { + if (type.NullableAnnotation == NullableAnnotation.Annotated) + { + if (target.NullableAnnotation == NullableAnnotation.NotAnnotated) + { + return false; + } + } + + if (target.TypeKind == TypeKind.Interface) + { + if (SymbolEqualityComparer.Default.Equals(type.WithNullableAnnotation(NullableAnnotation.None), target.WithNullableAnnotation(NullableAnnotation.None))) + { + return true; + } + + foreach (var iface in type.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(target.WithNullableAnnotation(NullableAnnotation.None), iface.WithNullableAnnotation(NullableAnnotation.None))) + { + return true; + } + } + + return false; + } + + return ParserUtilities.IsBaseOrIdentity(type, target, comp); + } + + private static bool IsProviderMethodVisible(this ISymbol symbol) + { + while (symbol != null && symbol.Kind != SymbolKind.Namespace) + { + switch (symbol.DeclaredAccessibility) + { + case Accessibility.NotApplicable: + case Accessibility.Private: + case Accessibility.Protected: + return false; + } + + symbol = symbol.ContainingSymbol; + } + + return true; + } +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Parsing/MultipleDataClassesAppliedException.cs b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/MultipleDataClassesAppliedException.cs new file mode 100644 index 0000000000..6fa71a6a7a --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/MultipleDataClassesAppliedException.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Gen.Logging.Parsing; + +[SuppressMessage("Design", "CA1064:Exceptions should be public", Justification = "Internal exception")] +[SuppressMessage("Design", "CA1032:Implement standard exception constructors", Justification = "Internal exception")] +internal sealed class MultipleDataClassesAppliedException : Exception +{ + public MultipleDataClassesAppliedException(IPropertySymbol property) + { + Property = property; + } + + public IPropertySymbol Property { get; } +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Parsing/Parser.cs b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/Parser.cs new file mode 100644 index 0000000000..5d90b8bede --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/Parser.cs @@ -0,0 +1,657 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Gen.Logging.Model; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.Logging.Parsing; + +internal sealed class Parser +{ + private readonly CancellationToken _cancellationToken; + private readonly Compilation _compilation; + private readonly Action _reportDiagnostic; + + public Parser(Compilation compilation, Action reportDiagnostic, CancellationToken cancellationToken) + { + _compilation = compilation; + _cancellationToken = cancellationToken; + _reportDiagnostic = reportDiagnostic; + } + + /// + /// Gets the set of logging types containing methods to output. + /// + [SuppressMessage("Maintainability", "CA1505:Avoid unmaintainable code", Justification = "Fix this in a follow-up")] + public IReadOnlyList GetLogTypes(IEnumerable types) + { + Action diagReport = Diag; // Keeping one instance of the delegate + var symbols = SymbolLoader.LoadSymbols(_compilation, diagReport); + if (symbols == null) + { + // nothing to do if required symbols aren't available + return Array.Empty(); + } + + var ids = new HashSet(); + var eventNames = new HashSet(); + var results = new List(); + var parameterSymbols = new Dictionary(); + + // we enumerate by syntax tree, to minimize the need to instantiate semantic models (since they're expensive) + foreach (var group in types.GroupBy(x => x.SyntaxTree)) + { + SemanticModel? sm = null; + foreach (var typeDec in group) + { + // stop if we're asked to + _cancellationToken.ThrowIfCancellationRequested(); + + LoggingType? lt = null; + string nspace = string.Empty; + string? loggerField = null; + string? redactorProviderField = null; + bool loggerFieldNullable = false; + bool redactorProviderFieldNullable = false; + IFieldSymbol? secondLoggerField = null; + IFieldSymbol? secondRedactorProviderField = null; + + ids.Clear(); + foreach (var method in typeDec.Members.Where(m => m.IsKind(SyntaxKind.MethodDeclaration)).Cast()) + { + sm ??= _compilation.GetSemanticModel(typeDec.SyntaxTree); + + var attrLoc = GetLogMethodAttribute(method, sm, symbols); + if (attrLoc == null) + { + // doesn't have the magic attribute we like, so ignore + continue; + } + + var methodSymbol = sm.GetDeclaredSymbol(method, _cancellationToken); + if (methodSymbol == null) + { + // we only care about methods + continue; + } + + var (lm, keepMethod) = ProcessMethod(method, methodSymbol, attrLoc); + + bool foundLogger = false; + bool foundRedactorProvider = false; + bool foundException = false; + bool foundLogLevel = false; + bool foundDataClassificationAnnotation = false; + bool foundCustomLogPropertiesProvider = false; + parameterSymbols.Clear(); + + foreach (var paramSymbol in methodSymbol.Parameters) + { + var lp = ProcessParameter(paramSymbol, ref foundLogger, ref foundRedactorProvider, ref foundException, ref foundLogLevel, ref foundDataClassificationAnnotation); + if (lp == null) + { + keepMethod = false; + continue; + } + + parameterSymbols[lp] = paramSymbol; + + // Check if the parameter is annotated with an attribute to enable logging of its properties: + var logPropertiesAttribute = ParserUtilities.GetSymbolAttributeAnnotationOrDefault(symbols.LogPropertiesAttribute, paramSymbol); + if (logPropertiesAttribute is not null) + { + var processingResult = LogParserUtilities.ProcessLogPropertiesForParameter( + logPropertiesAttribute, + lm, + lp, + paramSymbol, + symbols, + diagReport, + _compilation, + _cancellationToken); + + if (processingResult == LogPropertiesProcessingResult.Fail) + { + keepMethod = false; + } + else + { + foundCustomLogPropertiesProvider |= lp.HasPropsProvider; + if (processingResult == LogPropertiesProcessingResult.SucceededWithRedaction) + { + foundDataClassificationAnnotation = true; + } + } + } + + bool forceAsTemplateParam = false; + if (lp.IsLogger && lm.TemplateMap.ContainsKey(lp.Name)) + { + Diag(DiagDescriptors.ShouldntMentionLoggerInMessage, attrLoc, lp.Name); + forceAsTemplateParam = true; + } + else if (lp.IsException && lm.TemplateMap.ContainsKey(lp.Name)) + { + Diag(DiagDescriptors.ShouldntMentionExceptionInMessage, attrLoc, lp.Name); + forceAsTemplateParam = true; + } + else if (lp.IsLogLevel && lm.TemplateMap.ContainsKey(lp.Name)) + { + Diag(DiagDescriptors.ShouldntMentionLogLevelInMessage, attrLoc, lp.Name); + forceAsTemplateParam = true; + } + else if (lp.IsNormalParameter && !lm.TemplateMap.ContainsKey(lp.Name) && logPropertiesAttribute == null && !string.IsNullOrEmpty(lm.Message)) + { + Diag(DiagDescriptors.ArgumentHasNoCorrespondingTemplate, paramSymbol.GetLocation(), lp.Name); + } + + if (lp.Name[0] == '_' || (lp.NeedsAtSign && lp.Name.Length > 1 && lp.Name[1] == '_')) + { + // can't have logging method parameter names that start with _ since that can lead to conflicting symbol names + // because all generated symbols start with _ + Diag(DiagDescriptors.InvalidLoggingMethodParameterName, paramSymbol.GetLocation()); + } + + lm.AllParameters.Add(lp); + if (lp.IsNormalParameter || forceAsTemplateParam) + { + if (lm.TemplateMap.ContainsKey(lp.Name)) + { + lm.TemplateParameters.Add(lp); + } + } + } + + if (keepMethod) + { + if (lm.IsStatic) + { + if (!foundLogger) + { + Diag(DiagDescriptors.MissingLoggerArgument, method.ParameterList.GetLocation(), lm.Name); + keepMethod = false; + } + else if (foundRedactorProvider && !foundDataClassificationAnnotation) + { + Diag(DiagDescriptors.MissingDataClassificationArgument, method.ParameterList.GetLocation(), lm.Name); + } + else if (foundDataClassificationAnnotation && !foundRedactorProvider) + { + Diag(DiagDescriptors.MissingRedactorProviderArgument, method.ParameterList.GetLocation()); + keepMethod = false; + } + else if (foundRedactorProvider && foundCustomLogPropertiesProvider) + { + foreach (var lp in lm.AllParameters) + { + if (lp.HasPropsProvider) + { + Diag(DiagDescriptors.LogPropertiesProviderWithRedaction, parameterSymbols[lp].GetLocation(), lp.Name); + } + } + } + } + else + { + if (!foundLogger) + { + if (loggerField == null) + { + (loggerField, secondLoggerField, loggerFieldNullable) = FindField(sm, typeDec, symbols.ILoggerSymbol); + } + + if (secondLoggerField != null) + { + Diag(DiagDescriptors.MultipleLoggerFields, secondLoggerField.GetLocation(), typeDec.Identifier.Text); + keepMethod = false; + } + else if (loggerField == null) + { + Diag(DiagDescriptors.MissingLoggerField, method.Identifier.GetLocation(), typeDec.Identifier.Text); + keepMethod = false; + } + else + { + lm.LoggerField = loggerField; + lm.LoggerFieldNullable = loggerFieldNullable; + } + } + + if (!foundRedactorProvider && foundDataClassificationAnnotation) + { + if (symbols.RedactorProviderSymbol == null) + { + // nothing to do if this type isn't available + Diag(DiagDescriptors.MissingRequiredType, method.GetLocation(), SymbolLoader.IRedactorProviderType); + return Array.Empty(); + } + + if (redactorProviderField == null) + { + (redactorProviderField, secondRedactorProviderField, redactorProviderFieldNullable) = FindField(sm, typeDec, symbols.RedactorProviderSymbol); + } + + if (secondRedactorProviderField != null) + { + Diag(DiagDescriptors.MultipleRedactorProviderFields, secondRedactorProviderField.GetLocation(), typeDec.Identifier.Text); + keepMethod = false; + } + else if (redactorProviderField == null) + { + Diag(DiagDescriptors.MissingRedactorProviderField, method.GetLocation(), typeDec.Identifier.Text); + keepMethod = false; + } + else + { + lm.RedactorProviderField = redactorProviderField; + lm.RedactorProviderFieldNullable = redactorProviderFieldNullable; + } + } + else if (foundRedactorProvider && !foundDataClassificationAnnotation) + { + Diag(DiagDescriptors.MissingDataClassificationArgument, method.GetLocation(), lm.Name); + } + + // Show this warning only if other checks passed + if (keepMethod && + foundLogger && + (!foundDataClassificationAnnotation || foundRedactorProvider)) + { + Diag(DiagDescriptors.LoggingMethodShouldBeStatic, method.Identifier.GetLocation()); + } + } + + if (lm.Level == null && !foundLogLevel) + { + Diag(DiagDescriptors.MissingLogLevel, method.GetLocation()); + keepMethod = false; + } + + if (keepMethod && + string.IsNullOrWhiteSpace(lm.Message) && + !lm.EventId.HasValue && + lm.AllParameters.All(x => x.IsLogger || x.IsRedactorProvider || x.IsLogLevel)) + { + Diag(DiagDescriptors.EmptyLoggingMethod, method.Identifier.GetLocation(), methodSymbol.Name); + } + + foreach (var t in lm.TemplateMap) + { + bool found = false; + foreach (var p in lm.AllParameters) + { + if (t.Key.Equals(p.Name, StringComparison.OrdinalIgnoreCase)) + { + found = true; + break; + } + } + + if (!found) + { + Diag(DiagDescriptors.TemplateHasNoCorrespondingArgument, attrLoc, t.Key); + } + } + + LogParserUtilities.CheckMethodParametersAreUnique(lm, diagReport, parameterSymbols); + } + + if (lt == null) + { + // determine the namespace the class is declared in, if any + SyntaxNode? potentialNamespaceParent = typeDec.Parent; + while (potentialNamespaceParent != null && +#if ROSLYN_4_0_OR_GREATER + potentialNamespaceParent is not NamespaceDeclarationSyntax && + potentialNamespaceParent is not FileScopedNamespaceDeclarationSyntax) +#else + potentialNamespaceParent is not NamespaceDeclarationSyntax) +#endif + { + potentialNamespaceParent = potentialNamespaceParent.Parent; + } + +#if ROSLYN_4_0_OR_GREATER + BaseNamespaceDeclarationSyntax? namespaceParent = potentialNamespaceParent as BaseNamespaceDeclarationSyntax; +#else + NamespaceDeclarationSyntax? namespaceParent = potentialNamespaceParent as NamespaceDeclarationSyntax; +#endif + if (namespaceParent != null) + { + nspace = namespaceParent.Name.ToString(); + while (true) + { + namespaceParent = namespaceParent.Parent as NamespaceDeclarationSyntax; + if (namespaceParent == null) + { + break; + } + + nspace = $"{namespaceParent.Name}.{nspace}"; + } + } + } + + if (keepMethod) + { + if (lt == null) + { + lt = new LoggingType + { + Keyword = typeDec.Keyword.ValueText, + Namespace = nspace, + Name = typeDec.Identifier.ToString() + typeDec.TypeParameterList, + Parent = null, + }; + + LoggingType currentLoggerClass = lt; + var parentLoggerClass = typeDec.Parent as TypeDeclarationSyntax; + var parentType = methodSymbol.ContainingType.ContainingType; + + static bool IsAllowedKind(SyntaxKind kind) => + kind == SyntaxKind.ClassDeclaration || + kind == SyntaxKind.StructDeclaration || + kind == SyntaxKind.RecordDeclaration; + + while (parentLoggerClass != null && IsAllowedKind(parentLoggerClass.Kind())) + { + currentLoggerClass.Parent = new LoggingType + { + Keyword = parentLoggerClass.Keyword.ValueText, + Namespace = nspace, + Name = parentLoggerClass.Identifier.ToString() + parentLoggerClass.TypeParameterList, + Parent = null, + }; + + currentLoggerClass = currentLoggerClass.Parent; + parentLoggerClass = parentLoggerClass.Parent as TypeDeclarationSyntax; + parentType = parentType.ContainingType; + } + } + + lt.Methods.Add(lm); + } + } + + if (lt != null) + { + results.Add(lt); + } + } + } + + return results; + + (LoggingMethod lm, bool keepMethod) ProcessMethod(MethodDeclarationSyntax method, IMethodSymbol methodSymbol, Location attrLoc) + { + var attr = ParserUtilities.GetSymbolAttributeAnnotationOrDefault(symbols.LogMethodAttribute, methodSymbol)!; + + var (eventId, level, message, eventName, skipEnabledCheck) = AttributeProcessors.ExtractLogMethodAttributeValues(attr, symbols); + + var lm = new LoggingMethod + { + Name = methodSymbol.Name, + Level = level, + Message = message, + EventId = eventId, + EventName = eventName, + SkipEnabledCheck = skipEnabledCheck, + IsExtensionMethod = methodSymbol.IsExtensionMethod, + IsStatic = methodSymbol.IsStatic, + Modifiers = method.Modifiers.ToString(), + HasXmlDocumentation = HasXmlDocumentation(method), + }; + TemplateExtractor.ExtractTemplates(message, lm.TemplateMap, lm.TemplateList); + + var keepMethod = true; + if (lm.Name[0] == '_') + { + // can't have logging method names that start with _ since that can lead to conflicting symbol names + // because the generated symbols start with _ + Diag(DiagDescriptors.InvalidLoggingMethodName, method.Identifier.GetLocation()); + } + + if (!methodSymbol.ReturnsVoid) + { + // logging methods must return void + Diag(DiagDescriptors.LoggingMethodMustReturnVoid, method.ReturnType.GetLocation()); + keepMethod = false; + } + + if (method.Arity > 0) + { + // we don't currently support generic methods + Diag(DiagDescriptors.LoggingMethodIsGeneric, method.TypeParameterList!.GetLocation()); + keepMethod = false; + } + +#if ROSLYN_4_0_OR_GREATER + bool isPartial = methodSymbol.IsPartialDefinition; +#else + bool isPartial = true; // don't check for this condition on older versions of Roslyn since IsPartialDefinition doesn't exist +#endif + + if (method.Body != null) + { + Diag(DiagDescriptors.LoggingMethodHasBody, method.Body.GetLocation()); + keepMethod = false; + } + else if (!isPartial) + { + Diag(DiagDescriptors.LoggingMethodMustBePartial, method.Identifier.GetLocation()); + keepMethod = false; + } + + // ensure there are no duplicate ids. + if (lm.EventId != null) + { + if (!ids.Add(lm.EventId.Value)) + { + Diag(DiagDescriptors.ShouldntReuseEventIds, attrLoc, lm.EventId.Value, methodSymbol.ContainingType.Name); + } + } + + // ensure there are no duplicate event names. + if (lm.EventName != null) + { + if (!eventNames.Add(lm.EventName)) + { + Diag(DiagDescriptors.ShouldntReuseEventNames, attrLoc, lm.EventName, methodSymbol.ContainingType.Name); + } + } + + var msg = lm.Message; +#pragma warning disable S1067 // Expressions should not be too complex + if (msg.StartsWith("INFORMATION:", StringComparison.OrdinalIgnoreCase) + || msg.StartsWith("INFO:", StringComparison.OrdinalIgnoreCase) + || msg.StartsWith("WARNING:", StringComparison.OrdinalIgnoreCase) + || msg.StartsWith("WARN:", StringComparison.OrdinalIgnoreCase) + || msg.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase) + || msg.StartsWith("ERR:", StringComparison.OrdinalIgnoreCase)) +#pragma warning restore S1067 // Expressions should not be too complex + { + Diag(DiagDescriptors.RedundantQualifierInMessage, attrLoc, methodSymbol.Name); + } + + return (lm, keepMethod); + } + + LoggingMethodParameter? ProcessParameter( + IParameterSymbol paramSymbol, + ref bool foundLogger, + ref bool foundRedactorProvider, + ref bool foundException, + ref bool foundLogLevel, + ref bool foundDataClassificationAnnotation) + { + var paramName = paramSymbol.Name; + + var needsAtSign = false; + if (!paramSymbol.DeclaringSyntaxReferences.IsDefaultOrEmpty) + { + var paramSyntax = paramSymbol.DeclaringSyntaxReferences[0].GetSyntax(_cancellationToken) as ParameterSyntax; + if (!string.IsNullOrEmpty(paramSyntax!.Identifier.Text)) + { + needsAtSign = paramSyntax!.Identifier.Text[0] == '@'; + } + } + + if (string.IsNullOrWhiteSpace(paramName)) + { + // semantic problem, just bail quietly + return null; + } + + var paramTypeSymbol = paramSymbol.Type; + if (paramTypeSymbol is IErrorTypeSymbol) + { + // semantic problem, just bail quietly + return null; + } + + string? qualifier = null; + if (paramSymbol.RefKind == RefKind.In) + { + qualifier = "in"; + } + else if (paramSymbol.RefKind != RefKind.None) + { + // Parameter has "ref", "out" modifier, no can do + Diag(DiagDescriptors.LoggingMethodParameterRefKind, paramSymbol.GetLocation(), paramSymbol.ContainingSymbol.Name, paramName); + return null; + } + + string typeName = paramTypeSymbol.ToDisplayString( + SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions( + SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier | SymbolDisplayMiscellaneousOptions.UseSpecialTypes)); + + INamedTypeSymbol? classificationAttributeType = null; + + var paramDataClassAttributes = paramSymbol.GetDataClassificationAttributes(symbols); + if (paramDataClassAttributes.Count > 1) + { + Diag(DiagDescriptors.MultipleDataClassificationAttributes, paramSymbol.GetLocation()); + } + else + { + bool isAnnotatedWithDataClassAttr = paramDataClassAttributes.Count == 1; + if (isAnnotatedWithDataClassAttr) + { + classificationAttributeType = paramDataClassAttributes[0]; + + foundDataClassificationAnnotation = true; + } + } + + var extractedType = paramTypeSymbol; + if (paramTypeSymbol.IsNullableOfT()) + { + // extract the T from a Nullable + extractedType = ((INamedTypeSymbol)paramTypeSymbol).TypeArguments[0]; + } + + var lp = new LoggingMethodParameter + { + Name = paramName, + Type = typeName, + Qualifier = qualifier, + NeedsAtSign = needsAtSign, + ClassificationAttributeType = classificationAttributeType?.ToDisplayString(), + IsNullable = paramTypeSymbol.NullableAnnotation == NullableAnnotation.Annotated, + IsReference = paramTypeSymbol.IsReferenceType, + IsLogger = !foundLogger && ParserUtilities.IsBaseOrIdentity(paramTypeSymbol, symbols.ILoggerSymbol, _compilation), + IsException = !foundException && ParserUtilities.IsBaseOrIdentity(paramTypeSymbol, symbols.ExceptionSymbol, _compilation), + IsLogLevel = !foundLogLevel && SymbolEqualityComparer.Default.Equals(paramTypeSymbol, symbols.LogLevelSymbol), + IsEnumerable = LogParserUtilities.IsEnumerable(extractedType, symbols), + ImplementsIConvertible = LogParserUtilities.ImplementsIConvertible(paramTypeSymbol, symbols), + ImplementsIFormatable = LogParserUtilities.ImplementsIFormatable(paramTypeSymbol, symbols), + + IsRedactorProvider = !foundRedactorProvider && + symbols.RedactorProviderSymbol is not null && + ParserUtilities.IsBaseOrIdentity(paramTypeSymbol, symbols.RedactorProviderSymbol!, _compilation) + }; + + foundLogger |= lp.IsLogger; + foundRedactorProvider |= lp.IsRedactorProvider; + foundException |= lp.IsException; + foundLogLevel |= lp.IsLogLevel; + + return lp; + } + } + + private static bool HasXmlDocumentation(MethodDeclarationSyntax method) + { + var triviaList = method.GetLeadingTrivia(); + foreach (var trivia in triviaList) + { + if (trivia.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia)) + { + return true; + } + } + + return false; + } + + private Location? GetLogMethodAttribute(MethodDeclarationSyntax methodSyntax, SemanticModel sm, SymbolHolder symbols) + { + foreach (var mal in methodSyntax.AttributeLists) + { + foreach (var methodAttr in mal.Attributes) + { + var attrCtor = sm.GetSymbolInfo(methodAttr, _cancellationToken).Symbol; + if (attrCtor != null && SymbolEqualityComparer.Default.Equals(attrCtor.ContainingType, symbols.LogMethodAttribute)) + { + return methodAttr.GetLocation(); + } + } + } + + return null; + } + + private (string? field, IFieldSymbol? secondLoggerField, bool isNullable) FindField(SemanticModel sm, TypeDeclarationSyntax classDec, ITypeSymbol symbol) + { + string? field = null; + bool isNullable = false; + + foreach (var m in classDec.Members) + { + if (m is FieldDeclarationSyntax fds) + { + foreach (var v in fds.Declaration.Variables) + { + var fs = sm.GetDeclaredSymbol(v, _cancellationToken) as IFieldSymbol; + if (fs != null && ParserUtilities.IsBaseOrIdentity(fs.Type, symbol, _compilation)) + { + if (field == null) + { + field = v.Identifier.Text; + isNullable = fs.Type.NullableAnnotation == NullableAnnotation.Annotated; + } + else + { + return (null, fs, isNullable); + } + } + } + } + } + + return (field, null, isNullable); + } + + private void Diag(DiagnosticDescriptor desc, Location? location, params object?[]? messageArgs) + { + _reportDiagnostic(Diagnostic.Create(desc, location, messageArgs)); + } +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Parsing/PropertyHiddenException.cs b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/PropertyHiddenException.cs new file mode 100644 index 0000000000..97cd28f665 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/PropertyHiddenException.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Gen.Logging.Parsing; + +[SuppressMessage("Design", "CA1064:Exceptions should be public", Justification = "Internal exception")] +[SuppressMessage("Design", "CA1032:Implement standard exception constructors", Justification = "Internal exception")] +internal sealed class PropertyHiddenException : Exception +{ + public PropertyHiddenException(IPropertySymbol property) + { + Property = property; + } + + public IPropertySymbol Property { get; } +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Parsing/Resources.Designer.cs b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/Resources.Designer.cs new file mode 100644 index 0000000000..7ee8e410b9 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/Resources.Designer.cs @@ -0,0 +1,729 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Gen.Logging { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Gen.Logging.Parsing.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Parameter "{0}" is not referenced from the logging message. + /// + internal static string ArgumentHasNoCorrespondingTemplateMessage { + get { + return ResourceManager.GetString("ArgumentHasNoCorrespondingTemplateMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A parameter isn't referenced from the logging message. + /// + internal static string ArgumentHasNoCorrespondingTemplateTitle { + get { + return ResourceManager.GetString("ArgumentHasNoCorrespondingTemplateTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging method "{0}" doesn't have anything to be logged. + /// + internal static string EmptyLoggingMethodMessage { + get { + return ResourceManager.GetString("EmptyLoggingMethodMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging method doesn't log anything. + /// + internal static string EmptyLoggingMethodTitle { + get { + return ResourceManager.GetString("EmptyLoggingMethodTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging method names can't start with _. + /// + internal static string InvalidLoggingMethodNameMessage { + get { + return ResourceManager.GetString("InvalidLoggingMethodNameMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging method names can't start with an underscore. + /// + internal static string InvalidLoggingMethodNameTitle { + get { + return ResourceManager.GetString("InvalidLoggingMethodNameTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging method parameter names cannot start with _. + /// + internal static string InvalidLoggingMethodParameterNameMessage { + get { + return ResourceManager.GetString("InvalidLoggingMethodParameterNameMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging method parameter names can't start with an underscore. + /// + internal static string InvalidLoggingMethodParameterNameTitle { + get { + return ResourceManager.GetString("InvalidLoggingMethodParameterNameTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can't log properties of parameters of type "{0}". + /// + internal static string InvalidTypeToLogPropertiesMessage { + get { + return ResourceManager.GetString("InvalidTypeToLogPropertiesMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can't log properties of a logging parameter. + /// + internal static string InvalidTypeToLogPropertiesTitle { + get { + return ResourceManager.GetString("InvalidTypeToLogPropertiesTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging methods can't have a body. + /// + internal static string LoggingMethodHasBodyMessage { + get { + return ResourceManager.GetString("LoggingMethodHasBodyMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging methods can't have a body. + /// + internal static string LoggingMethodHasBodyTitle { + get { + return ResourceManager.GetString("LoggingMethodHasBodyTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging methods can't be generic. + /// + internal static string LoggingMethodIsGenericMessage { + get { + return ResourceManager.GetString("LoggingMethodIsGenericMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging methods can't be generic. + /// + internal static string LoggingMethodIsGenericTitle { + get { + return ResourceManager.GetString("LoggingMethodIsGenericTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging methods must be partial. + /// + internal static string LoggingMethodMustBePartialMessage { + get { + return ResourceManager.GetString("LoggingMethodMustBePartialMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging methods must be partial. + /// + internal static string LoggingMethodMustBePartialTitle { + get { + return ResourceManager.GetString("LoggingMethodMustBePartialTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging methods must return void. + /// + internal static string LoggingMethodMustReturnVoidMessage { + get { + return ResourceManager.GetString("LoggingMethodMustReturnVoidMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging methods must return void. + /// + internal static string LoggingMethodMustReturnVoidTitle { + get { + return ResourceManager.GetString("LoggingMethodMustReturnVoidTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging method "{0}" has parameter "{1}" with either "ref" or "out" modifier. + /// + internal static string LoggingMethodParameterRefKindMessage { + get { + return ResourceManager.GetString("LoggingMethodParameterRefKindMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging method parameters can't have "ref" or "out" modifiers. + /// + internal static string LoggingMethodParameterRefKindTitle { + get { + return ResourceManager.GetString("LoggingMethodParameterRefKindTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging methods must be static. + /// + internal static string LoggingMethodShouldBeStaticMessage { + get { + return ResourceManager.GetString("LoggingMethodShouldBeStaticMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging methods must be static. + /// + internal static string LoggingMethodShouldBeStaticTitle { + get { + return ResourceManager.GetString("LoggingMethodShouldBeStaticTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter "{0}" annotated for properties logging within logging method "{3}" has type with a cycle in its hierarchy: {1} ⇆ {2}. + /// + internal static string LogPropertiesCycleDetectedMessage { + get { + return ResourceManager.GetString("LogPropertiesCycleDetectedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging method parameter type has cycles in its type hierarchy. + /// + internal static string LogPropertiesCycleDetectedTitle { + get { + return ResourceManager.GetString("LogPropertiesCycleDetectedTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter "{0}" of logging method "{1}" has a property "{2}" within its type that hides another property from its base type. + /// + internal static string LogPropertiesHiddenPropertyDetectedMessage { + get { + return ResourceManager.GetString("LogPropertiesHiddenPropertyDetectedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging method parameter's type has a hidden property. + /// + internal static string LogPropertiesHiddenPropertyDetectedTitle { + get { + return ResourceManager.GetString("LogPropertiesHiddenPropertyDetectedTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter "{0}" is annotated for property logging but it has special semantics (ILogger, LogLevel, Exception, etc.). + /// + internal static string LogPropertiesInvalidUsageMessage { + get { + return ResourceManager.GetString("LogPropertiesInvalidUsageMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Method parameter can't be used for properties logging. + /// + internal static string LogPropertiesInvalidUsageTitle { + get { + return ResourceManager.GetString("LogPropertiesInvalidUsageTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter "{0}" causes name conflict with name "{1}" within logging method "{2}". + /// + internal static string LogPropertiesNameCollisionMessage { + get { + return ResourceManager.GetString("LogPropertiesNameCollisionMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A logging method parameter causes name conflicts. + /// + internal static string LogPropertiesNameCollisionTitle { + get { + return ResourceManager.GetString("LogPropertiesNameCollisionTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type "{0}" used with parameter "{1}" doesn't have any public properties to log. + /// + internal static string LogPropertiesParameterSkippedMessage { + get { + return ResourceManager.GetString("LogPropertiesParameterSkippedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging method parameter type has no public properties to log. + /// + internal static string LogPropertiesParameterSkippedTitle { + get { + return ResourceManager.GetString("LogPropertiesParameterSkippedTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property provider method "{0}" in type "{1}" is not accessible, increase its visibility. + /// + internal static string LogPropertiesProviderMethodInaccessibleMessage { + get { + return ResourceManager.GetString("LogPropertiesProviderMethodInaccessibleMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property provider method is inaccessible. + /// + internal static string LogPropertiesProviderMethodInaccessibleTitle { + get { + return ResourceManager.GetString("LogPropertiesProviderMethodInaccessibleTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property provider method "{0}" in type "{1}" doesn't have a signature compatible with "{2}". + /// + internal static string LogPropertiesProviderMethodInvalidSignatureMessage { + get { + return ResourceManager.GetString("LogPropertiesProviderMethodInvalidSignatureMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property provider method has an invalid signature. + /// + internal static string LogPropertiesProviderMethodInvalidSignatureTitle { + get { + return ResourceManager.GetString("LogPropertiesProviderMethodInvalidSignatureTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not find property provider method "{0}" in type "{1}". + /// + internal static string LogPropertiesProviderMethodNotFoundMessage { + get { + return ResourceManager.GetString("LogPropertiesProviderMethodNotFoundMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property provider method not found. + /// + internal static string LogPropertiesProviderMethodNotFoundTitle { + get { + return ResourceManager.GetString("LogPropertiesProviderMethodNotFoundTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter "{0}" has a custom property provider and hence will not be redacted. + /// + internal static string LogPropertiesProviderWithRedactionMessage { + get { + return ResourceManager.GetString("LogPropertiesProviderWithRedactionMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameters with a custom property provider are not subject to redaciton. + /// + internal static string LogPropertiesProviderWithRedactionTitle { + get { + return ResourceManager.GetString("LogPropertiesProviderWithRedactionTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to One or more parameters of the logging method "{0}" must be annotated with a data classification attribute. + /// + internal static string MissingDataClassificationArgumentMessage { + get { + return ResourceManager.GetString("MissingDataClassificationArgumentMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logging method parameters must be annotated with a data classification attribute. + /// + internal static string MissingDataClassificationArgumentTitle { + get { + return ResourceManager.GetString("MissingDataClassificationArgumentTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to One of the parameters to a static logging method must implement the "Microsoft.Extensions.Logging.ILogger" interface. + /// + internal static string MissingLoggerArgumentMessage { + get { + return ResourceManager.GetString("MissingLoggerArgumentMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A static logging method must have a parameter that implements the "Microsoft.Extensions.Logging.ILogger" interface. + /// + internal static string MissingLoggerArgumentTitle { + get { + return ResourceManager.GetString("MissingLoggerArgumentTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Couldn't find a field of type "Microsoft.Extensions.Logging.ILogger" in type "{0}". + /// + internal static string MissingLoggerFieldMessage { + get { + return ResourceManager.GetString("MissingLoggerFieldMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Couldn't find a field of type "Microsoft.Extensions.Logging.ILogger". + /// + internal static string MissingLoggerFieldTitle { + get { + return ResourceManager.GetString("MissingLoggerFieldTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A "LogLevel" value must be supplied in the "LoggerMethod" attribute or as a parameter to the logging method. + /// + internal static string MissingLogLevelMessage { + get { + return ResourceManager.GetString("MissingLogLevelMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A "LogLevel" value must be supplied. + /// + internal static string MissingLogLevelTitle { + get { + return ResourceManager.GetString("MissingLogLevelTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to One of the parameters to a logging method that performs redaction must implement the Microsoft.Extensions.Redaction.IRedactorProvider interface. + /// + internal static string MissingRedactorProviderArgumentMessage { + get { + return ResourceManager.GetString("MissingRedactorProviderArgumentMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A parameter to a logging method must implement the "IRedactorProvider" interface. + /// + internal static string MissingRedactorProviderArgumentTitle { + get { + return ResourceManager.GetString("MissingRedactorProviderArgumentTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Couldn't find a field of type "Microsoft.Extensions.Redaction.IRedactorProvider" in type "{0}". + /// + internal static string MissingRedactorProviderFieldMessage { + get { + return ResourceManager.GetString("MissingRedactorProviderFieldMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Couldn't find a field of type "Microsoft.Extensions.Redaction.IRedactorProvider". + /// + internal static string MissingRedactorProviderFieldTitle { + get { + return ResourceManager.GetString("MissingRedactorProviderFieldTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Couldn't find a definition for required type "{0}". + /// + internal static string MissingRequiredTypeMessage { + get { + return ResourceManager.GetString("MissingRequiredTypeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Couldn't find a required type definition. + /// + internal static string MissingRequiredTypeTitle { + get { + return ResourceManager.GetString("MissingRequiredTypeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can't have multiple data classification attributes for a single data value. + /// + internal static string MultipleDataClassificationAttributesMessage { + get { + return ResourceManager.GetString("MultipleDataClassificationAttributesMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can't have multiple data classification attributes for a single data value. + /// + internal static string MultipleDataClassificationAttributesTitle { + get { + return ResourceManager.GetString("MultipleDataClassificationAttributesTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Found multiple fields of type "Microsoft.Extensions.Logging.ILogger" in type "{0}". + /// + internal static string MultipleLoggerFieldsMessage { + get { + return ResourceManager.GetString("MultipleLoggerFieldsMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Multiple fields of type "Microsoft.Extensions.Logging.ILogger" were found. + /// + internal static string MultipleLoggerFieldsTitle { + get { + return ResourceManager.GetString("MultipleLoggerFieldsTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Found multiple fields of type "Microsoft.Extensions.Redaction.IRedactorProvider" in type "{0}". + /// + internal static string MultipleRedactorProviderFieldsMessage { + get { + return ResourceManager.GetString("MultipleRedactorProviderFieldsMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Multiple fields of type "Microsoft.Extensions.Redaction.IRedactorProvider" were found. + /// + internal static string MultipleRedactorProviderFieldsTitle { + get { + return ResourceManager.GetString("MultipleRedactorProviderFieldsTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove redundant qualifier (Info:, Warning:, Error:, etc) from the logging message since it is implicit in the specified log level.. + /// + internal static string RedundantQualifierInMessageMessage { + get { + return ResourceManager.GetString("RedundantQualifierInMessageMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Redundant qualifier in the logging message. + /// + internal static string RedundantQualifierInMessageTitle { + get { + return ResourceManager.GetString("RedundantQualifierInMessageTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Don't include a template for parameter "{0}" in the logging message, exceptions are automatically delivered without being listed in the logging message. + /// + internal static string ShouldntMentionExceptionInMessageMessage { + get { + return ResourceManager.GetString("ShouldntMentionExceptionInMessageMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Don't include exception parameters as templates in the logging message. + /// + internal static string ShouldntMentionExceptionInMessageTitle { + get { + return ResourceManager.GetString("ShouldntMentionExceptionInMessageTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Don't include a template for "{0}" in the logging message. + /// + internal static string ShouldntMentionLoggerInMessageMessage { + get { + return ResourceManager.GetString("ShouldntMentionLoggerInMessageMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Don't include logger parameters as templates. + /// + internal static string ShouldntMentionLoggerInMessageTitle { + get { + return ResourceManager.GetString("ShouldntMentionLoggerInMessageTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Don't include a template for parameter "{0}" in the logging message. + /// + internal static string ShouldntMentionLogLevelInMessageMessage { + get { + return ResourceManager.GetString("ShouldntMentionLogLevelInMessageMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Don't include log level parameters as templates. + /// + internal static string ShouldntMentionLogLevelInMessageTitle { + get { + return ResourceManager.GetString("ShouldntMentionLogLevelInMessageTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Multiple logging methods are using event id "{0}" in type "{1}". + /// + internal static string ShouldntReuseEventIdsMessage { + get { + return ResourceManager.GetString("ShouldntReuseEventIdsMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Each logging method should use a unique event id. + /// + internal static string ShouldntReuseEventIdsTitle { + get { + return ResourceManager.GetString("ShouldntReuseEventIdsTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Multiple logging methods are using event name "{0}" in type "{1}". + /// + internal static string ShouldntReuseEventNamesMessage { + get { + return ResourceManager.GetString("ShouldntReuseEventNamesMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Multiple logging methods shouldn't use the same event name. + /// + internal static string ShouldntReuseEventNamesTitle { + get { + return ResourceManager.GetString("ShouldntReuseEventNamesTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Template "{0}" is not provided as parameter to the logging method. + /// + internal static string TemplateHasNoCorrespondingArgumentMessage { + get { + return ResourceManager.GetString("TemplateHasNoCorrespondingArgumentMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The logging template has no corresponding method parameter. + /// + internal static string TemplateHasNoCorrespondingArgumentTitle { + get { + return ResourceManager.GetString("TemplateHasNoCorrespondingArgumentTitle", resourceCulture); + } + } + } +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Parsing/Resources.resx b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/Resources.resx new file mode 100644 index 0000000000..13e7dcebb7 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/Resources.resx @@ -0,0 +1,342 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Logging method names can't start with an underscore + + + Logging method names can't start with _ + + + Logging method parameter names can't start with an underscore + + + Logging method parameter names cannot start with _ + + + Couldn't find a required type definition + + + Couldn't find a definition for required type "{0}" + + + Each logging method should use a unique event id + + + Multiple logging methods are using event id "{0}" in type "{1}" + + + Logging methods must return void + + + Logging methods must return void + + + A static logging method must have a parameter that implements the "Microsoft.Extensions.Logging.ILogger" interface + + + One of the parameters to a static logging method must implement the "Microsoft.Extensions.Logging.ILogger" interface + + + Logging methods must be static + + + Logging methods must be static + + + Logging methods must be partial + + + Logging methods must be partial + + + Logging methods can't be generic + + + Logging methods can't be generic + + + Don't include a template for parameter "{0}" in the logging message, exceptions are automatically delivered without being listed in the logging message + + + Don't include exception parameters as templates in the logging message + + + Remove redundant qualifier (Info:, Warning:, Error:, etc) from the logging message since it is implicit in the specified log level. + + + Redundant qualifier in the logging message + + + Parameter "{0}" is not referenced from the logging message + + + A parameter isn't referenced from the logging message + + + Template "{0}" is not provided as parameter to the logging method + + + The logging template has no corresponding method parameter + + + Logging methods can't have a body + + + Logging methods can't have a body + + + A "LogLevel" value must be supplied + + + A "LogLevel" value must be supplied in the "LoggerMethod" attribute or as a parameter to the logging method + + + Don't include a template for parameter "{0}" in the logging message + + + Don't include log level parameters as templates + + + Don't include a template for "{0}" in the logging message + + + Don't include logger parameters as templates + + + Couldn't find a field of type "Microsoft.Extensions.Logging.ILogger" in type "{0}" + + + Couldn't find a field of type "Microsoft.Extensions.Logging.ILogger" + + + Found multiple fields of type "Microsoft.Extensions.Logging.ILogger" in type "{0}" + + + Multiple fields of type "Microsoft.Extensions.Logging.ILogger" were found + + + Can't have multiple data classification attributes for a single data value + + + Can't have multiple data classification attributes for a single data value + + + One of the parameters to a logging method that performs redaction must implement the Microsoft.R9.Extensions.Redaction.IRedactorProvider interface + + + A parameter to a logging method must implement the "IRedactorProvider" interface + + + One or more parameters of the logging method "{0}" must be annotated with a data classification attribute + + + Logging method parameters must be annotated with a data classification attribute + + + Couldn't find a field of type "Microsoft.R9.Extensions.Redaction.IRedactorProvider" in type "{0}" + + + Couldn't find a field of type "Microsoft.R9.Extensions.Redaction.IRedactorProvider" + + + Found multiple fields of type "Microsoft.R9.Extensions.Redaction.IRedactorProvider" in type "{0}" + + + Multiple fields of type "Microsoft.R9.Extensions.Redaction.IRedactorProvider" were found + + + Can't log properties of parameters of type "{0}" + + + Can't log properties of a logging parameter + + + Parameter "{0}" is annotated for property logging but it has special semantics (ILogger, LogLevel, Exception, etc.) + + + Method parameter can't be used for properties logging + + + Type "{0}" used with parameter "{1}" doesn't have any public properties to log + + + Logging method parameter type has no public properties to log + + + Parameter "{0}" annotated for properties logging within logging method "{3}" has type with a cycle in its hierarchy: {1} ⇆ {2} + + + Logging method parameter type has cycles in its type hierarchy + + + Property provider method "{0}" in type "{1}" is not accessible, increase its visibility + + + Property provider method is inaccessible + + + Could not find property provider method "{0}" in type "{1}" + + + Property provider method not found + + + Property provider method has an invalid signature + + + Logging method "{0}" has parameter "{1}" with either "ref" or "out" modifier + + + Logging method parameters can't have "ref" or "out" modifiers + + + Parameter "{0}" has a custom property provider and hence will not be redacted + + + Parameters with a custom property provider are not subject to redaciton + + + Multiple logging methods are using event name "{0}" in type "{1}" + + + Multiple logging methods shouldn't use the same event name + + + Parameter "{0}" of logging method "{1}" has a property "{2}" within its type that hides another property from its base type + + + Logging method parameter's type has a hidden property + + + Parameter "{0}" causes name conflict with name "{1}" within logging method "{2}" + + + A logging method parameter causes name conflicts + + + Logging method doesn't log anything + + + Logging method "{0}" doesn't have anything to be logged + + + Property provider method "{0}" in type "{1}" doesn't have a signature compatible with "{2}" + + diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Parsing/SymbolHolder.cs b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/SymbolHolder.cs new file mode 100644 index 0000000000..5f064b8d46 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/SymbolHolder.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Gen.Logging.Parsing; + +[ExcludeFromCodeCoverage] +internal sealed record class SymbolHolder( + Compilation Compilation, + INamedTypeSymbol LogMethodAttribute, + INamedTypeSymbol LogPropertiesAttribute, + INamedTypeSymbol? LogPropertyIgnoreAttribute, + INamedTypeSymbol ILogPropertyCollectorSymbol, + INamedTypeSymbol ILoggerSymbol, + INamedTypeSymbol? RedactorProviderSymbol, + INamedTypeSymbol LogLevelSymbol, + INamedTypeSymbol ExceptionSymbol, + HashSet IgnorePropertiesSymbols, + INamedTypeSymbol EnumerableSymbol, + INamedTypeSymbol FormatProviderSymbol, + INamedTypeSymbol? DataClassificationAttribute); diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Parsing/SymbolLoader.cs b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/SymbolLoader.cs new file mode 100644 index 0000000000..e96e790938 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/SymbolLoader.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Gen.Logging.Parsing; + +internal static class SymbolLoader +{ + internal const string LogMethodAttribute = "Microsoft.Extensions.Telemetry.Logging.LogMethodAttribute"; + internal const string LogPropertiesAttribute = "Microsoft.Extensions.Telemetry.Logging.LogPropertiesAttribute"; + internal const string LogPropertyIgnoreAttribute = "Microsoft.Extensions.Telemetry.Logging.LogPropertyIgnoreAttribute"; + internal const string ILogPropertyCollectorType = "Microsoft.Extensions.Telemetry.Logging.ILogPropertyCollector"; + internal const string ILoggerType = "Microsoft.Extensions.Logging.ILogger"; + internal const string IRedactorProviderType = "Microsoft.Extensions.Compliance.Redaction.IRedactorProvider"; + internal const string LogLevelType = "Microsoft.Extensions.Logging.LogLevel"; + internal const string ExceptionType = "System.Exception"; + internal const string DataClassificationAttribute = "Microsoft.Extensions.Compliance.Classification.DataClassificationAttribute"; + internal const string LogMethodHelper = "Microsoft.Extensions.Telemetry.Logging.LogMethodHelper"; + internal const string IEnrichmentPropertyBag = "Microsoft.Extensions.Telemetry.Enrichment.IEnrichmentPropertyBag"; + internal const string IFormatProviderType = "System.IFormatProvider"; + + private static readonly string[] _ignored = new[] + { + "System.DateTimeOffset", + "System.Guid", + "System.TimeSpan", + "System.TimeOnly", + "System.DateOnly", + "System.Version", + "System.Uri", + "System.Net.IPAddress", + "System.Net.EndPoint", + "System.Net.IPEndPoint", + "System.Net.DnsEndPoint", + "System.Numerics.BigInteger", + "System.Numerics.Complex", + "System.Numerics.Matrix3x2", + "System.Numerics.Matrix4x4", + "System.Numerics.Plane", + "System.Numerics.Quaternion", + "System.Numerics.Vector2", + "System.Numerics.Vector3", + "System.Numerics.Vector4", + }; + + internal static SymbolHolder? LoadSymbols( + Compilation compilation, + Action diagCallback) + { + var loggerSymbol = compilation.GetTypeByMetadataName(ILoggerType); + var logLevelSymbol = compilation.GetTypeByMetadataName(LogLevelType); + var logMethodAttributeSymbol = compilation.GetTypeByMetadataName(LogMethodAttribute); + var logPropertiesAttributeSymbol = compilation.GetTypeByMetadataName(LogPropertiesAttribute); + var logPropertyCollectorSymbol = compilation.GetTypeByMetadataName(ILogPropertyCollectorType); + var logPropertyIgnoreAttributeSymbol = compilation.GetTypeByMetadataName(LogPropertyIgnoreAttribute); + var dataClassificationAttribute = compilation.GetTypeByMetadataName(DataClassificationAttribute); + +#pragma warning disable S1067 // Expressions should not be too complex + if (loggerSymbol == null + || logLevelSymbol == null + || logMethodAttributeSymbol == null + || logPropertiesAttributeSymbol == null + || logPropertyCollectorSymbol == null + || logPropertyIgnoreAttributeSymbol == null) + { + // nothing to do if these types aren't available + return null; + } +#pragma warning restore S1067 // Expressions should not be too complex + + var exceptionSymbol = compilation.GetTypeByMetadataName(ExceptionType); + if (exceptionSymbol == null) + { + diagCallback(DiagDescriptors.MissingRequiredType, null, new object[] { ExceptionType }); + return null; + } + + var logMethodHelperSymbol = compilation.GetTypeByMetadataName(LogMethodHelper); + var enrichmentPropBagSymbol = compilation.GetTypeByMetadataName(IEnrichmentPropertyBag); + + if (logMethodHelperSymbol != null) + { + bool found = false; + foreach (var iface in logMethodHelperSymbol.Interfaces) + { + if (SymbolEqualityComparer.Default.Equals(iface, enrichmentPropBagSymbol)) + { + found = true; + break; + } + } + + if (!found) + { + logMethodHelperSymbol = null; + } + } + + var redactorProviderSymbol = compilation.GetTypeByMetadataName(IRedactorProviderType); + var enumerableSymbol = compilation.GetSpecialType(SpecialType.System_Collections_IEnumerable); + var formatProviderSymbol = compilation.GetTypeByMetadataName(IFormatProviderType)!; + + var ignorePropsSymbols = new HashSet(SymbolEqualityComparer.Default); + + foreach (var ign in _ignored) + { + var s = compilation.GetTypeByMetadataName(ign); + if (s != null) + { + _ = ignorePropsSymbols.Add(s); + } + } + + return new( + compilation, + logMethodAttributeSymbol, + logPropertiesAttributeSymbol, + logPropertyIgnoreAttributeSymbol, + logPropertyCollectorSymbol, + loggerSymbol, + redactorProviderSymbol, + logLevelSymbol, + exceptionSymbol, + ignorePropsSymbols, + enumerableSymbol, + formatProviderSymbol, + dataClassificationAttribute); + } +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Parsing/TemplateExtractor.cs b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/TemplateExtractor.cs new file mode 100644 index 0000000000..d9be39621f --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/TemplateExtractor.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Gen.Logging.Parsing; + +internal static class TemplateExtractor +{ + private static readonly char[] _formatDelimiters = { ',', ':' }; + + /// + /// Finds the template arguments contained in the message string. + /// + internal static void ExtractTemplates(string? message, IDictionary templateMap, ICollection templateList) + { + if (string.IsNullOrEmpty(message)) + { + return; + } + + var scanIndex = 0; + var endIndex = message!.Length; + + while (scanIndex < endIndex) + { + var openBraceIndex = FindBraceIndex(message, '{', scanIndex, endIndex); + var closeBraceIndex = FindBraceIndex(message, '}', openBraceIndex, endIndex); + + if (closeBraceIndex == endIndex) + { + scanIndex = endIndex; + } + else + { + // Format item syntax : { index[,alignment][ :formatString] }. + var formatDelimiterIndex = FindIndexOfAny(message, _formatDelimiters, openBraceIndex, closeBraceIndex); + + var templateName = message.Substring(openBraceIndex + 1, formatDelimiterIndex - openBraceIndex - 1); + if (templateName[0] == '@') + { + templateName = templateName.Substring(1); + } + + templateMap[templateName] = templateName; + templateList.Add(templateName); + scanIndex = closeBraceIndex + 1; + } + } + } + + internal static int FindIndexOfAny(string message, char[] chars, int startIndex, int endIndex) + { + var findIndex = message.IndexOfAny(chars, startIndex, endIndex - startIndex); + return findIndex == -1 ? endIndex : findIndex; + } + + private static int FindBraceIndex(string message, char brace, int startIndex, int endIndex) + { + // Example: {{prefix{{{Argument}}}suffix}}. + var braceIndex = endIndex; + var scanIndex = startIndex; + var braceOccurrenceCount = 0; + + while (scanIndex < endIndex) + { + if (braceOccurrenceCount > 0 && message[scanIndex] != brace) + { +#pragma warning disable S109 // Magic numbers should not be used + if (braceOccurrenceCount % 2 == 0) +#pragma warning restore S109 // Magic numbers should not be used + { + // Even number of '{' or '}' found. Proceed search with next occurrence of '{' or '}'. + braceOccurrenceCount = 0; + braceIndex = endIndex; + } + else + { + // An unescaped '{' or '}' found. + break; + } + } + else if (message[scanIndex] == brace) + { + if (brace == '}') + { + if (braceOccurrenceCount == 0) + { + // For '}' pick the first occurrence. + braceIndex = scanIndex; + } + } + else + { + // For '{' pick the last occurrence. + braceIndex = scanIndex; + } + + braceOccurrenceCount++; + } + + scanIndex++; + } + + return braceIndex; + } +} diff --git a/src/Generators/Microsoft.Gen.Logging/Common/Parsing/TransitiveTypeCycleException.cs b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/TransitiveTypeCycleException.cs new file mode 100644 index 0000000000..ee0753e3e9 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Common/Parsing/TransitiveTypeCycleException.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Gen.Logging.Parsing; + +[SuppressMessage("Design", "CA1064:Exceptions should be public", Justification = "Internal exception")] +[SuppressMessage("Design", "CA1032:Implement standard exception constructors", Justification = "Internal exception")] +internal sealed class TransitiveTypeCycleException : Exception +{ + public TransitiveTypeCycleException(IPropertySymbol property, INamedTypeSymbol namedType) + { + Property = property; + NamedType = namedType; + } + + public IPropertySymbol Property { get; } + + public INamedTypeSymbol NamedType { get; } +} diff --git a/src/Generators/Microsoft.Gen.Logging/Directory.Build.props b/src/Generators/Microsoft.Gen.Logging/Directory.Build.props new file mode 100644 index 0000000000..fb4ad0e833 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Directory.Build.props @@ -0,0 +1,33 @@ + + + + + Microsoft.Gen.Logging + Code generator to support Microsoft.Extensions.Telemetry's logging features. + Telemetry + + + + cs + true + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.Logging/Roslyn3.8/Microsoft.Gen.Logging.Roslyn3.8.csproj b/src/Generators/Microsoft.Gen.Logging/Roslyn3.8/Microsoft.Gen.Logging.Roslyn3.8.csproj new file mode 100644 index 0000000000..068b7948ea --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Roslyn3.8/Microsoft.Gen.Logging.Roslyn3.8.csproj @@ -0,0 +1,28 @@ + + + Microsoft.Gen.Logging + 3.8 + $(MicrosoftCodeAnalysisVersion_3_8) + + + + normal + 97 + 92 + 83 + + + + + True + True + Resources.resx + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.Logging/Roslyn4.0/Microsoft.Gen.Logging.Roslyn4.0.csproj b/src/Generators/Microsoft.Gen.Logging/Roslyn4.0/Microsoft.Gen.Logging.Roslyn4.0.csproj new file mode 100644 index 0000000000..cb1dd76099 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Roslyn4.0/Microsoft.Gen.Logging.Roslyn4.0.csproj @@ -0,0 +1,29 @@ + + + Microsoft.Gen.Logging + 4.0 + $(MicrosoftCodeAnalysisVersion_4_0) + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + + + normal + 97 + 92 + 83 + + + + + True + True + Resources.resx + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.Metering/Common/DiagDescriptors.cs b/src/Generators/Microsoft.Gen.Metering/Common/DiagDescriptors.cs new file mode 100644 index 0000000000..b44b2eb3d1 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Metering/Common/DiagDescriptors.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.Metering; + +internal sealed class DiagDescriptors : DiagDescriptorsBase +{ + private const string Category = "Metering"; + + public static DiagnosticDescriptor ErrorInvalidMethodName { get; } = Make( + id: "R9G050", + title: Resources.ErrorInvalidMethodNameTitle, + messageFormat: Resources.ErrorInvalidMethodNameMessage, + category: Category); + + public static DiagnosticDescriptor ErrorInvalidParameterName { get; } = Make( + id: "R9G051", + title: Resources.ErrorInvalidParameterNameTitle, + messageFormat: Resources.ErrorInvalidParameterNameMessage, + category: Category); + + // R9G052 is not in use, but can be referenced by the old Metric generator + + public static DiagnosticDescriptor ErrorInvalidMetricName { get; } = Make( + id: "R9G053", + title: Resources.ErrorInvalidMetricNameTitle, + messageFormat: Resources.ErrorInvalidMetricNameMessage, + category: Category); + + public static DiagnosticDescriptor ErrorMetricNameReuse { get; } = Make( + id: "R9G054", + title: Resources.ErrorMetricNameReuseTitle, + messageFormat: Resources.ErrorMetricNameReuseMessage, + category: Category); + + public static DiagnosticDescriptor ErrorInvalidMethodReturnType { get; } = Make( + id: "R9G055", + title: Resources.ErrorInvalidMethodReturnTypeTitle, + messageFormat: Resources.ErrorInvalidMethodReturnTypeMessage, + category: Category); + + public static DiagnosticDescriptor ErrorMissingMeter { get; } = Make( + id: "R9G056", + title: Resources.ErrorMissingMeterTitle, + messageFormat: Resources.ErrorMissingMeterMessage, + category: Category); + + public static DiagnosticDescriptor ErrorNotPartialMethod { get; } = Make( + id: "R9G057", + title: Resources.ErrorNotPartialMethodTitle, + messageFormat: Resources.ErrorNotPartialMethodMessage, + category: Category); + + public static DiagnosticDescriptor ErrorMethodIsGeneric { get; } = Make( + id: "R9G058", + title: Resources.ErrorMethodIsGenericTitle, + messageFormat: Resources.ErrorMethodIsGenericMessage, + category: Category); + + public static DiagnosticDescriptor ErrorMethodHasBody { get; } = Make( + id: "R9G059", + title: Resources.ErrorMethodHasBodyTitle, + messageFormat: Resources.ErrorMethodHasBodyMessage, + category: Category); + + public static DiagnosticDescriptor ErrorInvalidDimensionNames { get; } = Make( + id: "R9G060", + title: Resources.ErrorInvalidDimensionNamesMessage, + messageFormat: Resources.ErrorInvalidDimensionNamesTitle, + category: Category); + + public static DiagnosticDescriptor ErrorNotStaticMethod { get; } = Make( + id: "R9G062", + title: Resources.ErrorNotStaticMethodTitle, + messageFormat: Resources.ErrorNotStaticMethodMessage, + category: Category); + + public static DiagnosticDescriptor ErrorDuplicateDimensionName { get; } = Make( + id: "R9G063", + title: Resources.ErrorDuplicateDimensionNameTitle, + messageFormat: Resources.ErrorDuplicateDimensionNameMessage, + category: Category); + + public static DiagnosticDescriptor ErrorInvalidDimensionType { get; } = Make( + id: "R9G064", + title: Resources.ErrorInvalidDimensionTypeTitle, + messageFormat: Resources.ErrorInvalidDimensionTypeMessage, + category: Category); + + public static DiagnosticDescriptor ErrorTooManyDimensions { get; } = Make( + id: "R9G065", + title: Resources.ErrorTooManyDimensionsTitle, + messageFormat: Resources.ErrorTooManyDimensionsMessage, + category: Category); + + public static DiagnosticDescriptor ErrorInvalidAttributeGenericType { get; } = Make( + id: "R9G066", + title: Resources.ErrorInvalidAttributeGenericTypeTitle, + messageFormat: Resources.ErrorInvalidAttributeGenericTypeMessage, + category: Category); + + public static DiagnosticDescriptor ErrorInvalidMethodReturnTypeLocation { get; } = Make( + id: "R9G067", + title: Resources.ErrorInvalidMethodReturnTypeLocationTitle, + messageFormat: Resources.ErrorInvalidMethodReturnTypeLocationMessage, + category: Category); + + public static DiagnosticDescriptor ErrorInvalidMethodReturnTypeArity { get; } = Make( + id: "R9G068", + title: Resources.ErrorInvalidMethodReturnTypeArityTitle, + messageFormat: Resources.ErrorInvalidMethodReturnTypeArityMessage, + category: Category); +} diff --git a/src/Generators/Microsoft.Gen.Metering/Common/Emitter.cs b/src/Generators/Microsoft.Gen.Metering/Common/Emitter.cs new file mode 100644 index 0000000000..03593b4e1d --- /dev/null +++ b/src/Generators/Microsoft.Gen.Metering/Common/Emitter.cs @@ -0,0 +1,342 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.Gen.Metering.Model; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.Metering; + +// Stryker disable all + +internal sealed class Emitter : EmitterBase +{ + private static readonly Regex _regex = new("[:.-]+", RegexOptions.Compiled); + + public string EmitMetrics(IReadOnlyList metricTypes, CancellationToken cancellationToken) + { + Dictionary> metricClassesDict = new(); + foreach (var cl in metricTypes) + { + if (!metricClassesDict.TryGetValue(cl.Namespace, out var list)) + { + list = new List(); + metricClassesDict.Add(cl.Namespace, list); + } + + list.Add(cl); + } + + foreach (var entry in metricClassesDict.OrderBy(static x => x.Key)) + { + cancellationToken.ThrowIfCancellationRequested(); + GenTypeByNamespace(entry.Key, entry.Value, cancellationToken); + } + + return Capture(); + } + + private static string GetSanitizedParamName(string paramName) + { + return _regex.Replace(paramName, "_"); + } + + private void GenTypeByNamespace(string nspace, IEnumerable metricTypes, CancellationToken cancellationToken) + { + OutLn(); + if (!string.IsNullOrWhiteSpace(nspace)) + { + OutLn($"namespace {nspace}"); + OutOpenBrace(); + } + + var first = true; + foreach (var metricClass in metricTypes.OrderBy(static x => x.Name)) + { + if (first) + { + first = false; + } + else + { + OutLn(); + } + + cancellationToken.ThrowIfCancellationRequested(); + GenType(metricClass, nspace); + } + + if (!string.IsNullOrWhiteSpace(nspace)) + { + OutCloseBrace(); + } + } + + private void GenType(MetricType metricType, string nspace) + { + GenInstrumentCreateMethods(metricType, nspace); + OutLn(); + + var first = true; + foreach (var metricMethod in metricType.Methods) + { + if (first) + { + first = false; + } + else + { + OutLn(); + } + + GenInstrumentClass(metricMethod); + } + } + + private void GenInstrumentCreateMethods(MetricType metricType, string nspace) + { + var parent = metricType.Parent; + var parentTypes = new List(); + + // loop until you find top level nested class + while (parent != null) + { + parentTypes.Add($"{parent.Modifiers} {parent.Keyword} {parent.Name} {parent.Constraints}"); + parent = parent.Parent; + } + + // write down top level nested class first + for (int i = parentTypes.Count - 1; i >= 0; i--) + { + OutLn(parentTypes[i]); + OutOpenBrace(); + } + + OutGeneratedCodeAttribute(); + OutLn($"{metricType.Modifiers} {metricType.Keyword} {metricType.Name} {metricType.Constraints}"); + OutOpenBrace(); + var first = true; + foreach (var metricMethod in metricType.Methods) + { + if (first) + { + first = false; + } + else + { + OutLn(); + } + + GenInstrumentCreateMethod(metricMethod, nspace); + } + + OutCloseBrace(); + + parent = metricType.Parent; + while (parent != null) + { + OutCloseBrace(); + parent = parent.Parent; + } + } + + private void GenInstrumentClass(MetricMethod metricMethod) + { + const string CounterObjectName = "counter"; + const string HistogramObjectName = "histogram"; + + const string CounterRecordStatement = "Add"; + const string HistogramRecordStatement = "Record"; + + const string CounterOfTTypeDefinitionTemplate = "global::System.Diagnostics.Metrics.Counter<{0}>"; + const string HistogramOfTTypeDefinitionTemplate = "global::System.Diagnostics.Metrics.Histogram<{0}>"; + + string objectName; + string typeDefinition; + string recordStatement; + string metricValueType = metricMethod.GenericType; + + switch (metricMethod.InstrumentKind) + { + case InstrumentKind.Counter: + case InstrumentKind.CounterT: + recordStatement = CounterRecordStatement; + typeDefinition = string.Format(CultureInfo.InvariantCulture, CounterOfTTypeDefinitionTemplate, metricValueType); + objectName = CounterObjectName; + break; + case InstrumentKind.Histogram: + case InstrumentKind.HistogramT: + recordStatement = HistogramRecordStatement; + typeDefinition = string.Format(CultureInfo.InvariantCulture, HistogramOfTTypeDefinitionTemplate, metricValueType); + objectName = HistogramObjectName; + break; + default: + throw new NotSupportedException($"Instrument type '{metricMethod.InstrumentKind}' is not supported to generate metric"); + } + + var tagListInit = metricMethod.DimensionsKeys.Count != 0 || + metricMethod.StrongTypeConfigs.Count != 0; + + var accessModifier = metricMethod.MetricTypeModifiers.Contains("public") + ? "public" + : "internal"; + + OutGeneratedCodeAttribute(); + OutLn($"{accessModifier} sealed class {metricMethod.MetricTypeName}"); + OutLn($"{{"); + OutLn($" private readonly {typeDefinition} _{objectName};"); + OutLn(); + OutLn($" public {metricMethod.MetricTypeName}({typeDefinition} {objectName})"); + OutLn($" {{"); + OutLn($" _{objectName} = {objectName};"); + OutLn($" }}"); + OutLn(); + + OutIndent(); + Out($" public void {recordStatement}({metricValueType} value"); + GenDimensionsParameters(metricMethod); + Out(")"); + OutLn(); + + OutLn($" {{"); + + const int MaxDimensionsWithoutEnabledCheck = 8; + if (metricMethod.DimensionsKeys.Count > MaxDimensionsWithoutEnabledCheck || + metricMethod.StrongTypeConfigs.Count > MaxDimensionsWithoutEnabledCheck) + { + OutLn($" if (!_{objectName}.Enabled)"); + OutLn($" {{"); + OutLn($" return;"); + OutLn($" }}"); + OutLn(); + } + + if (metricMethod.IsDimensionTypeClass) + { + OutLn($" if (o == null)"); + OutLn($" {{"); + OutLn($" throw new global::System.ArgumentNullException(nameof(o));"); + OutLn($" }}"); + OutLn(); + } + + if (tagListInit) + { + Indent(); + Indent(); + OutLn("var tagList = new global::System.Diagnostics.TagList"); + OutOpenBrace(); + GenDimensionsTagList(metricMethod); + Unindent(); + OutLn("};"); + Unindent(); + Unindent(); + OutLn(); + } + + OutLn($" _{objectName}.{recordStatement}(value{(tagListInit ? ", tagList" : string.Empty)});"); + OutLn($" }}"); + OutLn("}"); + } + + private void GenInstrumentCreateMethod(MetricMethod metricMethod, string nspace) + { + var nsprefix = string.IsNullOrWhiteSpace(nspace) + ? string.Empty + : $"global::{nspace}."; + + var thisModifier = metricMethod.IsExtensionMethod + ? "this " + : string.Empty; + + var meterParam = metricMethod.AllParameters[0]; + var accessModifier = metricMethod.Modifiers.Contains("public") + ? "public" + : "internal"; + + OutGeneratedCodeAttribute(); + OutIndent(); + Out($"{accessModifier} static partial {nsprefix}{metricMethod.MetricTypeName} {metricMethod.Name}({thisModifier}"); + foreach (var p in metricMethod.AllParameters) + { + if (p != meterParam) + { + Out(", "); + } + + Out($"{p.Type} {p.Name}"); + } + + Out(")"); + OutLn(); + OutIndent(); + Out($" => {nsprefix}GeneratedInstrumentsFactory.Create{metricMethod.MetricTypeName}("); + foreach (var p in metricMethod.AllParameters) + { + if (p != meterParam) + { + Out(", "); + } + + Out(p.Name); + } + + Out(");"); + OutLn(); + } + + private void GenDimensionsTagList(MetricMethod metricMethod) + { + if (string.IsNullOrEmpty(metricMethod.StrongTypeObjectName)) + { + foreach (var dimension in metricMethod.DimensionsKeys) + { + var paramName = GetSanitizedParamName(dimension); + OutLn($"new global::System.Collections.Generic.KeyValuePair(\"{paramName}\", {paramName}),"); + } + } + else + { + foreach (var config in metricMethod.StrongTypeConfigs) + { + if (config.StrongTypeMetricObjectType != StrongTypeMetricObjectType.String && + config.StrongTypeMetricObjectType != StrongTypeMetricObjectType.Enum) + { + continue; + } + + var paramName = GetSanitizedParamName(config.Name); + var paramInvoke = config.StrongTypeMetricObjectType == StrongTypeMetricObjectType.Enum + ? $"{paramName}.ToString()" + : $"{paramName}!"; + + var access = string.IsNullOrEmpty(config.Path) + ? "o." + paramInvoke + : "o." + config.Path + "." + paramInvoke; + + OutLn($"new global::System.Collections.Generic.KeyValuePair(\"{config.DimensionName}\", {access}),"); + } + } + } + + private void GenDimensionsParameters(MetricMethod metricMethod) + { + if (string.IsNullOrEmpty(metricMethod.StrongTypeObjectName)) + { + foreach (var dimension in metricMethod.DimensionsKeys) + { + var paramName = GetSanitizedParamName(dimension); + Out($", object? {paramName}"); + } + } + else + { + Out($", global::{metricMethod.StrongTypeObjectName} o"); + } + } +} diff --git a/src/Generators/Microsoft.Gen.Metering/Common/Generator.cs b/src/Generators/Microsoft.Gen.Metering/Common/Generator.cs new file mode 100644 index 0000000000..e26af738b2 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Metering/Common/Generator.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if ROSLYN_4_0_OR_GREATER + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.Metering; + +[Generator] +[ExcludeFromCodeCoverage] +public class Generator : IIncrementalGenerator +{ + private static readonly HashSet _attributeNames = new() + { + SymbolLoader.CounterAttribute, + SymbolLoader.CounterTAttribute.Replace("`1", ""), + SymbolLoader.HistogramAttribute, + SymbolLoader.HistogramTAttribute.Replace("`1", "") + }; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + GeneratorUtilities.Initialize(context, _attributeNames, m => m.Parent as TypeDeclarationSyntax, HandleAnnotatedTypes); + } + + private static void HandleAnnotatedTypes(Compilation compilation, IEnumerable nodes, SourceProductionContext context) + { + var p = new Parser(compilation, context.ReportDiagnostic, context.CancellationToken); + + var metricClasses = p.GetMetricClasses(nodes.OfType()); + if (metricClasses.Count > 0) + { + var factoryEmitter = new MetricFactoryEmitter(); + var factory = factoryEmitter.Emit(metricClasses, context.CancellationToken); + context.AddSource("Factory.g.cs", SourceText.From(factory, Encoding.UTF8)); + + var emitter = new Emitter(); + var metrics = emitter.EmitMetrics(metricClasses, context.CancellationToken); + context.AddSource("Metering.g.cs", SourceText.From(metrics, Encoding.UTF8)); + } + } +} + +#else + +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.Metering; + +[Generator] +[ExcludeFromCodeCoverage] +public class Generator : ISourceGenerator +{ + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(TypeDeclarationSyntaxReceiver.Create); + } + + public void Execute(GeneratorExecutionContext context) + { + var receiver = context.SyntaxReceiver as TypeDeclarationSyntaxReceiver; + if (receiver == null || receiver.TypeDeclarations.Count == 0) + { + // nothing to do yet + return; + } + + var parser = new Parser(context.Compilation, context.ReportDiagnostic, context.CancellationToken); + + var metricClasses = parser.GetMetricClasses(receiver.TypeDeclarations); + if (metricClasses.Count > 0) + { + var factoryEmitter = new MetricFactoryEmitter(); + var factory = factoryEmitter.Emit(metricClasses, context.CancellationToken); + context.AddSource("Factory.g.cs", SourceText.From(factory, Encoding.UTF8)); + + var emitter = new Emitter(); + var metrics = emitter.EmitMetrics(metricClasses, context.CancellationToken); + context.AddSource("Metering.g.cs", SourceText.From(metrics, Encoding.UTF8)); + } + } +} + +#endif diff --git a/src/Generators/Microsoft.Gen.Metering/Common/MetricFactoryEmitter.cs b/src/Generators/Microsoft.Gen.Metering/Common/MetricFactoryEmitter.cs new file mode 100644 index 0000000000..d8e455601f --- /dev/null +++ b/src/Generators/Microsoft.Gen.Metering/Common/MetricFactoryEmitter.cs @@ -0,0 +1,154 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Microsoft.Gen.Metering.Model; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.Metering; + +// Stryker disable all + +internal sealed class MetricFactoryEmitter : EmitterBase +{ + public string Emit(IReadOnlyList metricClasses, CancellationToken cancellationToken) + { + Dictionary> metricClassesDict = new(); + foreach (var cl in metricClasses) + { + if (!metricClassesDict.TryGetValue(cl.Namespace, out var list)) + { + list = new List(); + metricClassesDict.Add(cl.Namespace, list); + } + + list.Add(cl); + } + + foreach (var entry in metricClassesDict.OrderBy(static x => x.Key)) + { + GenMetricFactoryByNamespace(entry.Key, entry.Value, cancellationToken); + } + + return Capture(); + } + + private static string GetStringWithFirstCharLowercase(string str) + { + if (string.IsNullOrEmpty(str) || + char.IsLower(str[0])) + { + return str; + } + + if (str.Length == 1) + { +#pragma warning disable CA1308 // Normalize strings to uppercase + return str.ToLowerInvariant(); +#pragma warning restore CA1308 // Normalize strings to uppercase + } + + return char.ToLowerInvariant(str[0]) + str.Substring(1); + } + + private static string GetMetricDictionaryName(MetricMethod metricMethod) + => $"_{GetStringWithFirstCharLowercase(metricMethod.MetricTypeName)}Instruments"; + + private void GenMetricFactoryByNamespace(string nspace, IEnumerable metricClasses, CancellationToken cancellationToken) + { + if (!string.IsNullOrWhiteSpace(nspace)) + { + OutLn($"namespace {nspace}"); + OutOpenBrace(); + } + + OutGeneratedCodeAttribute(); + OutLn("internal static partial class GeneratedInstrumentsFactory"); + OutOpenBrace(); + + var first = true; + foreach (var metricClass in metricClasses.OrderBy(static x => x.Name)) + { + if (first) + { + first = false; + } + else + { + OutLn(); + } + + cancellationToken.ThrowIfCancellationRequested(); + GenMetricFactory(metricClass, nspace); + } + + OutCloseBrace(); + + if (!string.IsNullOrWhiteSpace(nspace)) + { + OutCloseBrace(); + } + + OutLn(); + } + + private void GenMetricFactory(MetricType metricType, string nspace) + { + var nsprefix = string.IsNullOrWhiteSpace(nspace) + ? string.Empty + : $"global::{nspace}."; + + var count = metricType.Methods.Count; + foreach (var metricMethod in metricType.Methods) + { + var meterParam = metricMethod.AllParameters[0]; + OutGeneratedCodeAttribute(); + OutLn($"private static global::System.Collections.Concurrent.ConcurrentDictionary<{meterParam.Type}, {nsprefix}{metricMethod.MetricTypeName}>"); + OutLn($" {GetMetricDictionaryName(metricMethod)} = new();"); + if (--count != 0) + { + OutLn(); + } + } + + foreach (var metricMethod in metricType.Methods) + { + GenMetricFactoryMethods(metricMethod, nspace); + } + } + + private void GenMetricFactoryMethods(MetricMethod metricMethod, string nspace) + { + var meterParam = metricMethod.AllParameters[0]; + string createMethodName = metricMethod.InstrumentKind switch + { + InstrumentKind.Counter => $"CreateCounter<{metricMethod.GenericType}>", + InstrumentKind.CounterT => $"CreateCounter<{metricMethod.GenericType}>", + InstrumentKind.Histogram => $"CreateHistogram<{metricMethod.GenericType}>", + InstrumentKind.HistogramT => $"CreateHistogram<{metricMethod.GenericType}>", + _ => throw new NotSupportedException($"Metric type '{metricMethod.InstrumentKind}' is not supported to generate factory"), + }; + + var accessModifier = metricMethod.MetricTypeModifiers.Contains("public") + ? "public" + : "internal"; + + var nsprefix = string.IsNullOrWhiteSpace(nspace) + ? string.Empty + : $"global::{nspace}."; + + OutLn(); + OutGeneratedCodeAttribute(); + OutLn($"{accessModifier} static {nsprefix}{metricMethod.MetricTypeName} Create{metricMethod.MetricTypeName}({meterParam.Type} {meterParam.Name})"); + OutOpenBrace(); + OutLn($"return {GetMetricDictionaryName(metricMethod)}.GetOrAdd({meterParam.Name}, static _meter =>"); + OutLn(" {"); + OutLn($" var instrument = _meter.{createMethodName}(@\"{metricMethod.MetricName}\");"); + OutLn($" return new {nsprefix}{metricMethod.MetricTypeName}(instrument);"); + OutLn(" });"); + OutCloseBrace(); + } +} diff --git a/src/Generators/Microsoft.Gen.Metering/Common/Model/InstrumentKind.cs b/src/Generators/Microsoft.Gen.Metering/Common/Model/InstrumentKind.cs new file mode 100644 index 0000000000..0c346dd673 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Metering/Common/Model/InstrumentKind.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Gen.Metering.Model; + +internal enum InstrumentKind +{ + None = 0, + Counter = 1, + Histogram = 2, + Gauge = 3, + CounterT = 4, + HistogramT = 5, +} diff --git a/src/Generators/Microsoft.Gen.Metering/Common/Model/MetricMethod.cs b/src/Generators/Microsoft.Gen.Metering/Common/Model/MetricMethod.cs new file mode 100644 index 0000000000..a6674094eb --- /dev/null +++ b/src/Generators/Microsoft.Gen.Metering/Common/Model/MetricMethod.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Gen.Metering.Model; + +internal sealed class MetricMethod +{ + public readonly List AllParameters = new(); + public HashSet DimensionsKeys = new(); + public string? Name; + public string? MetricName; + public bool IsExtensionMethod; + public string Modifiers = string.Empty; + public string MetricTypeModifiers = string.Empty; + public string MetricTypeName = string.Empty; + public InstrumentKind InstrumentKind; + public string GenericType = string.Empty; + public List StrongTypeConfigs = new(); // Used for strong type creation only + public string? StrongTypeObjectName; // Used for strong type creation only + public bool IsDimensionTypeClass; // Used for strong type creation only +} diff --git a/src/Generators/Microsoft.Gen.Metering/Common/Model/MetricParameter.cs b/src/Generators/Microsoft.Gen.Metering/Common/Model/MetricParameter.cs new file mode 100644 index 0000000000..a49b98976a --- /dev/null +++ b/src/Generators/Microsoft.Gen.Metering/Common/Model/MetricParameter.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Gen.Metering.Model; + +internal sealed class MetricParameter +{ + public string Name = string.Empty; + public string Type = string.Empty; + public bool IsMeter; +} diff --git a/src/Generators/Microsoft.Gen.Metering/Common/Model/MetricType.cs b/src/Generators/Microsoft.Gen.Metering/Common/Model/MetricType.cs new file mode 100644 index 0000000000..9e07cd2352 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Metering/Common/Model/MetricType.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Gen.Metering.Model; + +internal sealed class MetricType +{ + public readonly List Methods = new(); + public string Namespace = string.Empty; + public string Name = string.Empty; + public string Constraints = string.Empty; + public string Modifiers = string.Empty; + public string Keyword = string.Empty; + public MetricType? Parent; +} diff --git a/src/Generators/Microsoft.Gen.Metering/Common/Model/StrongTypeConfig.cs b/src/Generators/Microsoft.Gen.Metering/Common/Model/StrongTypeConfig.cs new file mode 100644 index 0000000000..880086e9ce --- /dev/null +++ b/src/Generators/Microsoft.Gen.Metering/Common/Model/StrongTypeConfig.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Gen.Metering.Model; + +/// +/// Path are the strings that go before the property name for child objects. When creating a strong type object for dimensions, the downstream properties must be written down. +/// +/// +/// As an example: +/// +/// public class Dims +/// { +/// public string Dim1 { get; set; } +/// public ChildDims childDims { get; set; } +/// } +/// +/// public class ChildDims +/// { +/// public string Dim2 { get; set; } +/// } +/// +/// StrongTypeDimensionConfigs dim1 = new StrongTypeDimensionConfigs +/// { +/// Path = "", +/// DimensionName = "Dim1" +/// }; +/// +/// StrongTypeDimensionConfigs dim2 = new StrongTypeDimensionConfigs +/// { +/// Path = "childDims" +/// DimensionName = "Dim2" +/// } +/// +/// +internal sealed class StrongTypeConfig +{ + public string Path = string.Empty; + public string Name = string.Empty; + public string DimensionName = string.Empty; + public StrongTypeMetricObjectType StrongTypeMetricObjectType { get; set; } +} diff --git a/src/Generators/Microsoft.Gen.Metering/Common/Model/StrongTypeMetricObjectType.cs b/src/Generators/Microsoft.Gen.Metering/Common/Model/StrongTypeMetricObjectType.cs new file mode 100644 index 0000000000..f451640e0a --- /dev/null +++ b/src/Generators/Microsoft.Gen.Metering/Common/Model/StrongTypeMetricObjectType.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Gen.Metering.Model; + +internal enum StrongTypeMetricObjectType +{ + String = 0, + Enum = 1, + Class = 2, + Struct = 3 +} diff --git a/src/Generators/Microsoft.Gen.Metering/Common/Parser.cs b/src/Generators/Microsoft.Gen.Metering/Common/Parser.cs new file mode 100644 index 0000000000..61237c5005 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Metering/Common/Parser.cs @@ -0,0 +1,828 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Gen.Metering.Model; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.Metering; + +internal sealed class Parser +{ + private const int MaxDimensions = 20; + + private static readonly Regex _regex = new("^[A-Z]+[A-za-z0-9]*$", RegexOptions.Compiled); + private static readonly Regex _regexDimensionNames = new("^[A-Za-z]+[A-Za-z0-9_.:-]*$", RegexOptions.Compiled); + private static readonly SymbolDisplayFormat _typeSymbolFormat = + SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions( + SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); + + private static readonly SymbolDisplayFormat _genericTypeSymbolFormat = + SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions( + SymbolDisplayMiscellaneousOptions.UseSpecialTypes); + + private static readonly HashSet _allowedGenericAttributeTypeArgs = + new() + { + SpecialType.System_Byte, + SpecialType.System_Int16, + SpecialType.System_Int32, + SpecialType.System_Int64, + SpecialType.System_Decimal, + SpecialType.System_Single, + SpecialType.System_Double + }; + + private readonly CancellationToken _cancellationToken; + private readonly Compilation _compilation; + private readonly Action _reportDiagnostic; + private readonly StringBuilderPool _builders = new(); + + public Parser(Compilation compilation, Action reportDiagnostic, CancellationToken cancellationToken) + { + _compilation = compilation; + _cancellationToken = cancellationToken; + _reportDiagnostic = reportDiagnostic; + } + + public IReadOnlyList GetMetricClasses(IEnumerable types) + { + var symbols = SymbolLoader.LoadSymbols(_compilation); + if (symbols == null) + { + return Array.Empty(); + } + + var results = new List(); + var metricNames = new HashSet(); + + foreach (var typeDeclarationGroup in types.GroupBy(x => x.SyntaxTree)) + { + SemanticModel? semanticModel = null; + foreach (var typeDeclaration in typeDeclarationGroup) + { + // stop if we're asked to + _cancellationToken.ThrowIfCancellationRequested(); + + MetricType? metricType = null; + string nspace = string.Empty; + + metricNames.Clear(); + + foreach (var memberSyntax in typeDeclaration.Members.Where(x => x.IsKind(SyntaxKind.MethodDeclaration))) + { + var methodSyntax = (MethodDeclarationSyntax)memberSyntax; + semanticModel ??= _compilation.GetSemanticModel(typeDeclaration.SyntaxTree); + IMethodSymbol? methodSymbol = semanticModel.GetDeclaredSymbol(methodSyntax, _cancellationToken); + if (methodSymbol == null) + { + continue; + } + + foreach (var methodAttribute in methodSymbol.GetAttributes()) + { + if (methodAttribute == null) + { + continue; + } + + var (metricMethod, keepMethod) = ProcessMethodAttribute(typeDeclaration, methodSyntax, methodSymbol, methodAttribute, symbols, metricNames); + if (metricMethod == null) + { + continue; + } + + if (metricType == null) + { + // determine the namespace the class is declared in, if any + SyntaxNode? potentialNamespaceParent = typeDeclaration.Parent; + while (potentialNamespaceParent != null && +#if ROSLYN_4_0_OR_GREATER + potentialNamespaceParent is not NamespaceDeclarationSyntax && + potentialNamespaceParent is not FileScopedNamespaceDeclarationSyntax) +#else + potentialNamespaceParent is not NamespaceDeclarationSyntax) +#endif + { + potentialNamespaceParent = potentialNamespaceParent.Parent; + } + +#if ROSLYN_4_0_OR_GREATER + var ns = potentialNamespaceParent as BaseNamespaceDeclarationSyntax; +#else + var ns = potentialNamespaceParent as NamespaceDeclarationSyntax; +#endif + + if (ns != null) + { + nspace = ns.Name.ToString(); + while (true) + { + ns = ns.Parent as NamespaceDeclarationSyntax; + if (ns == null) + { + break; + } + + nspace = $"{ns.Name}.{nspace}"; + } + } + } + + if (keepMethod) + { + metricType ??= new MetricType + { + Namespace = nspace, + Name = typeDeclaration.Identifier.ToString() + typeDeclaration.TypeParameterList, + Constraints = typeDeclaration.ConstraintClauses.ToString(), + Keyword = typeDeclaration.Keyword.ValueText, + Parent = null, + }; + + UpdateMetricKeywordIfRequired(typeDeclaration, metricType); + + MetricType currentMetricClass = metricType; + var parentMetricClass = typeDeclaration.Parent as TypeDeclarationSyntax; + var parentType = methodSymbol.ContainingType.ContainingType; + + static bool IsAllowedKind(SyntaxKind kind) => + kind is SyntaxKind.ClassDeclaration or + SyntaxKind.StructDeclaration or + SyntaxKind.RecordDeclaration; + + while (parentMetricClass != null && IsAllowedKind(parentMetricClass.Kind())) + { + currentMetricClass.Parent = new MetricType + { + Namespace = nspace, + Name = parentMetricClass.Identifier.ToString() + parentMetricClass.TypeParameterList, + Constraints = parentMetricClass.ConstraintClauses.ToString(), + Keyword = parentMetricClass.Keyword.ValueText, + Modifiers = parentMetricClass.Modifiers.ToString(), + Parent = null, + }; + + UpdateMetricKeywordIfRequired(parentMetricClass, currentMetricClass); + + currentMetricClass = currentMetricClass.Parent; + parentMetricClass = parentMetricClass.Parent as TypeDeclarationSyntax; + parentType = parentType.ContainingType; + } + + metricType.Methods.Add(metricMethod); + } + } + } + + if (metricType != null) + { + metricType.Modifiers = typeDeclaration.Modifiers.ToString(); + + results.Add(metricType); + } + } + } + + return results; + } + + private static void UpdateMetricKeywordIfRequired(TypeDeclarationSyntax? typeDeclaration, MetricType metricType) + { +#if ROSLYN_4_0_OR_GREATER + if (typeDeclaration.IsKind(SyntaxKind.RecordStructDeclaration) && + !metricType.Keyword.Contains("struct")) + { + metricType.Keyword += " struct"; + } +#endif + } + + private static bool AreDimensionKeyNamesValid(MetricMethod metricMethod) + { + foreach (string? dynDim in metricMethod.DimensionsKeys) + { + if (!_regexDimensionNames.IsMatch(dynDim)) + { + return false; + } + } + + return true; + } + + private static ITypeSymbol? GetGenericType(INamedTypeSymbol symbol) + => symbol.TypeArguments.IsDefaultOrEmpty + ? null + : symbol.TypeArguments[0]; + + private static (InstrumentKind instrumentKind, ITypeSymbol? genericType) GetInstrumentType( + INamedTypeSymbol? methodAttributeSymbol, + SymbolHolder symbols) + { + if (methodAttributeSymbol == null) + { + return (InstrumentKind.None, null); + } + + if (methodAttributeSymbol.Equals(symbols.CounterAttribute, SymbolEqualityComparer.Default)) + { + return (InstrumentKind.Counter, symbols.LongTypeSymbol); + } + + if (methodAttributeSymbol.Equals(symbols.HistogramAttribute, SymbolEqualityComparer.Default)) + { + return (InstrumentKind.Histogram, symbols.LongTypeSymbol); + } + + // Gauge is not supported yet + + if (methodAttributeSymbol.OriginalDefinition.Equals(symbols.CounterOfTAttribute, SymbolEqualityComparer.Default)) + { + return (InstrumentKind.CounterT, GetGenericType(methodAttributeSymbol)); + } + + if (methodAttributeSymbol.OriginalDefinition.Equals(symbols.HistogramOfTAttribute, SymbolEqualityComparer.Default)) + { + return (InstrumentKind.HistogramT, GetGenericType(methodAttributeSymbol)); + } + + return (InstrumentKind.None, null); + } + + private static bool TryGetDimensionNameFromAttribute(ISymbol symbol, SymbolHolder symbols, out string dimensionName) + { + var attributeData = ParserUtilities.GetSymbolAttributeAnnotationOrDefault(symbols.DimensionAttribute, symbol); + + if (attributeData is not null + && !attributeData.ConstructorArguments.IsDefaultOrEmpty + && attributeData.ConstructorArguments[0].Kind == TypedConstantKind.Primitive) + { + var ctorArg0 = attributeData.ConstructorArguments[0].Value as string; + + if (!string.IsNullOrWhiteSpace(ctorArg0)) + { + dimensionName = ctorArg0!; + return true; + } + } + + dimensionName = string.Empty; + return false; + } + + private static (string metricName, HashSet dimensions) ExtractAttributeParameters(AttributeData attribute) + { + var dimensionHashSet = new HashSet(); + string metricNameFromAttribute = string.Empty; + if (!attribute.NamedArguments.IsDefaultOrEmpty) + { + foreach (var arg in attribute.NamedArguments) + { + if (arg.Value.Kind == TypedConstantKind.Primitive && + arg.Key is "MetricName" or "Name") + { + metricNameFromAttribute = (arg.Value.Value ?? string.Empty).ToString().Replace("\\\\", "\\"); + break; + } + } + } + + if (!attribute.ConstructorArguments.IsDefaultOrEmpty) + { + foreach (var arg in attribute.ConstructorArguments) + { + if (arg.Kind != TypedConstantKind.Array) + { + continue; + } + + foreach (var item in arg.Values) + { + if (item.Kind != TypedConstantKind.Primitive) + { + continue; + } + + var value = item.Value?.ToString(); + if (value == null) + { + continue; + } + + _ = dimensionHashSet.Add(value); + } + } + } + + return (metricNameFromAttribute, dimensionHashSet); + } + + private (MetricMethod? metricMethod, bool keepMethod) ProcessMethodAttribute( + TypeDeclarationSyntax typeDeclaration, + MethodDeclarationSyntax methodSyntax, + IMethodSymbol methodSymbol, + AttributeData methodAttribute, + SymbolHolder symbols, + HashSet metricNames) + { + var (instrumentKind, genericType) = GetInstrumentType(methodAttribute.AttributeClass, symbols); + if (instrumentKind == InstrumentKind.None || + genericType == null) + { + return (null, false); + } + + bool keepMethod = CheckMethodReturnType(methodSymbol); + if (!_allowedGenericAttributeTypeArgs.Contains(genericType.SpecialType)) + { + Diag(DiagDescriptors.ErrorInvalidAttributeGenericType, methodSymbol.GetLocation(), genericType.ToString()); + keepMethod = false; + } + + string metricNameFromAttribute; + HashSet dimensions; + var strongTypeDimensionConfigs = new List(); + var strongTypeObjectName = string.Empty; + var isClass = false; + + if (!methodAttribute.ConstructorArguments.IsDefaultOrEmpty + && methodAttribute.ConstructorArguments[0].Kind == TypedConstantKind.Type) + { + KeyValuePair namedArg; + var ctorArg = methodAttribute.ConstructorArguments[0]; + + if (!methodAttribute.NamedArguments.IsDefaultOrEmpty) + { + namedArg = methodAttribute.NamedArguments[0]; + } + + (metricNameFromAttribute, dimensions, strongTypeDimensionConfigs, strongTypeObjectName, isClass) = ExtractStrongTypeAttributeParameters( + ctorArg, + namedArg, + symbols); + } + else + { + (metricNameFromAttribute, dimensions) = ExtractAttributeParameters(methodAttribute); + } + + string metricNameFromMethod = methodSymbol.ReturnType.Name; + + var metricMethod = new MetricMethod + { + Name = methodSymbol.Name, + MetricName = string.IsNullOrWhiteSpace(metricNameFromAttribute) ? metricNameFromMethod : metricNameFromAttribute, + InstrumentKind = instrumentKind, + GenericType = genericType.ToDisplayString(_genericTypeSymbolFormat), + DimensionsKeys = dimensions, + IsExtensionMethod = methodSymbol.IsExtensionMethod, + Modifiers = methodSyntax.Modifiers.ToString(), + MetricTypeName = methodSymbol.ReturnType.ToDisplayString(), // Roslyn doesn't know this type yet, no need to use a format here + StrongTypeConfigs = strongTypeDimensionConfigs, + StrongTypeObjectName = strongTypeObjectName, + IsDimensionTypeClass = isClass, + MetricTypeModifiers = typeDeclaration.Modifiers.ToString() + }; + + if (metricMethod.Name[0] == '_') + { + // can't have logging method names that start with _ since that can lead to conflicting symbol names + // because the generated symbols start with _ + Diag(DiagDescriptors.ErrorInvalidMethodName, methodSymbol.GetLocation()); + keepMethod = false; + } + + if (methodSymbol.Arity > 0) + { + // we don't currently support generic methods + Diag(DiagDescriptors.ErrorMethodIsGeneric, methodSymbol.GetLocation()); + keepMethod = false; + } + + bool isStatic = methodSymbol.IsStatic; +#if ROSLYN_4_0_OR_GREATER + bool isPartial = methodSymbol.IsPartialDefinition; +#else + bool isPartial = true; // don't check for this condition on older versions of Roslyn since IsPartialDefinition doesn't exist +#endif + + if (!isStatic) + { + Diag(DiagDescriptors.ErrorNotStaticMethod, methodSymbol.GetLocation()); + keepMethod = false; + } + + if (methodSyntax.Body != null) + { + Diag(DiagDescriptors.ErrorMethodHasBody, methodSymbol.GetLocation()); + keepMethod = false; + } +#pragma warning disable S2583 // Conditionally executed code should be reachable + else if (!isPartial) + { + Diag(DiagDescriptors.ErrorNotPartialMethod, methodSymbol.GetLocation()); + keepMethod = false; + } +#pragma warning restore S2583 // Conditionally executed code should be reachable + + // ensure Metric name is not empty and starts with a Capital letter. + // ensure there are no duplicate ids. + if (!_regex.IsMatch(metricNameFromMethod)) + { + Diag(DiagDescriptors.ErrorInvalidMetricName, methodSymbol.GetLocation(), metricNameFromMethod); + keepMethod = false; + } + else if (!metricNames.Add(metricNameFromMethod)) + { + Diag(DiagDescriptors.ErrorMetricNameReuse, methodSymbol.GetLocation(), metricNameFromMethod); + keepMethod = false; + } + + if (!AreDimensionKeyNamesValid(metricMethod)) + { + Diag(DiagDescriptors.ErrorInvalidDimensionNames, methodSymbol.GetLocation()); + keepMethod = false; + } + + bool isFirstParam = true; + foreach (var paramSymbol in methodSymbol.Parameters) + { + var paramName = paramSymbol.Name; + if (string.IsNullOrWhiteSpace(paramName)) + { + // semantic problem, just bail quietly + keepMethod = false; + break; + } + + var paramTypeSymbol = paramSymbol.Type; + if (paramTypeSymbol is IErrorTypeSymbol) + { + // semantic problem, just bail quietly + keepMethod = false; + break; + } + + if (isFirstParam && + symbols.IMeterInterface != null && + ParserUtilities.IsBaseOrIdentity(paramTypeSymbol, symbols.IMeterInterface, _compilation)) + { + // The method uses old IMeter, no need to parse it + return (null, false); + } + + var meterParameter = new MetricParameter + { + Name = paramName, + Type = paramTypeSymbol.ToDisplayString(_typeSymbolFormat), + IsMeter = isFirstParam && ParserUtilities.IsBaseOrIdentity(paramTypeSymbol, symbols.MeterSymbol, _compilation) + }; + + if (meterParameter.Name[0] == '_') + { + // can't have method parameter names that start with _ since that can lead to conflicting symbol names + // because all generated symbols start with _ + Diag(DiagDescriptors.ErrorInvalidParameterName, paramSymbol.Locations[0]); + } + + metricMethod.AllParameters.Add(meterParameter); + isFirstParam = false; + } + + if (keepMethod) + { + if (metricMethod.AllParameters.Count < 1 || + !metricMethod.AllParameters[0].IsMeter) + { + Diag(DiagDescriptors.ErrorMissingMeter, methodSymbol.GetLocation()); + keepMethod = false; + } + } + + return (metricMethod, keepMethod); + } + + private bool CheckMethodReturnType(IMethodSymbol methodSymbol) + { + var returnType = methodSymbol.ReturnType; + if (returnType.SpecialType != SpecialType.None || + returnType.TypeKind != TypeKind.Error) + { + // Make sure return type is not from existing known type + Diag(DiagDescriptors.ErrorInvalidMethodReturnType, methodSymbol.ReturnType.GetLocation(), methodSymbol.Name); + return false; + } + + if (returnType is INamedTypeSymbol { Arity: > 0 }) + { + Diag(DiagDescriptors.ErrorInvalidMethodReturnTypeArity, methodSymbol.GetLocation(), methodSymbol.Name, returnType.Name); + return false; + } + + if (!string.Equals(returnType.Name, returnType.ToString(), StringComparison.Ordinal)) + { + Diag(DiagDescriptors.ErrorInvalidMethodReturnTypeLocation, methodSymbol.GetLocation(), methodSymbol.Name, returnType.Name); + return false; + } + + return true; + } + + private void Diag(DiagnosticDescriptor desc, Location? location) + { + _reportDiagnostic(Diagnostic.Create(desc, location, Array.Empty())); + } + + private void Diag(DiagnosticDescriptor desc, Location? location, params object?[]? messageArgs) + { + _reportDiagnostic(Diagnostic.Create(desc, location, messageArgs)); + } + + private (string metricName, HashSet dimensions, List strongTypeConfigs, string strongTypeObjectName, bool isClass) + ExtractStrongTypeAttributeParameters( + TypedConstant constructorArg, + KeyValuePair namedArgument, + SymbolHolder symbols) + { + var dimensionHashSet = new HashSet(); + string metricNameFromAttribute = string.Empty; + var strongTypeConfigs = new List(); + bool isClass = false; + + if (namedArgument is { Key: "Name", Value.Value: { } }) + { + metricNameFromAttribute = namedArgument.Value.Value.ToString(); + } + + if (constructorArg.IsNull || + constructorArg.Value is not INamedTypeSymbol strongTypeSymbol) + { + return (metricNameFromAttribute, dimensionHashSet, strongTypeConfigs, string.Empty, isClass); + } + + // Need to check if the strongType is a class or struct, classes need a null check whereas structs do not. + if (strongTypeSymbol.TypeKind == TypeKind.Class) + { + isClass = true; + } + + // Loop through all of the members of the object level and below + foreach (var member in strongTypeSymbol.GetMembers()) + { + strongTypeConfigs.AddRange(BuildDimensionConfigs(member, dimensionHashSet, symbols, _builders.GetStringBuilder())); + } + + // Now that all of the current level and below dimensions are extracted, let's get any parent ones + strongTypeConfigs.AddRange(GetParentDimensionConfigs(strongTypeSymbol, dimensionHashSet, symbols)); + + if (strongTypeConfigs.Count > MaxDimensions) + { + Diag(DiagDescriptors.ErrorTooManyDimensions, strongTypeSymbol.Locations[0]); + } + + return (metricNameFromAttribute, dimensionHashSet, strongTypeConfigs, constructorArg.Value.ToString(), isClass); + } + + /// + /// Called recursively to build all required StrongTypeDimensionConfigs. + /// + /// The Symbol being extracted. + /// HashSet of all dimensions seen so far. + /// Shared symbols. + /// List of all property names when walking down the object model. See StrongTypeDimensionConfigs for an example. + /// List of all StrongTypeDimensionConfigs seen so far. + private List BuildDimensionConfigs( + ISymbol symbol, + HashSet dimensionHashSet, + SymbolHolder symbols, + StringBuilder stringBuilder) + { + var dimensionConfigs = new List(); + + TypeKind kind; + SpecialType specialType; + ITypeSymbol typeSymbol; + + if (symbol.IsImplicitlyDeclared) + { + return dimensionConfigs; + } + + switch (symbol.Kind) + { + case SymbolKind.Property: + var propertySymbol = symbol as IPropertySymbol; + + kind = propertySymbol!.Type.TypeKind; + specialType = propertySymbol.Type.SpecialType; + typeSymbol = propertySymbol.Type; + break; + + case SymbolKind.Field: + var fieldSymbol = symbol as IFieldSymbol; + + kind = fieldSymbol!.Type.TypeKind; + specialType = fieldSymbol.Type.SpecialType; + typeSymbol = fieldSymbol.Type; + break; + + default: + _builders.ReturnStringBuilder(stringBuilder); + return dimensionConfigs; + } + + // This one is to properly cover "Nullable" cases: + if (specialType == SpecialType.None) + { + specialType = typeSymbol.OriginalDefinition.SpecialType; + } + + try + { + if (kind == TypeKind.Enum) + { + var name = TryGetDimensionNameFromAttribute(symbol, symbols, out var dimensionName) + ? dimensionName + : symbol.Name; + + if (!dimensionHashSet.Add(name)) + { + Diag(DiagDescriptors.ErrorDuplicateDimensionName, symbol.Locations[0], symbol.Name); + } + else + { + dimensionConfigs.Add(new StrongTypeConfig + { + Name = symbol.Name, + Path = stringBuilder.ToString(), + DimensionName = name, + StrongTypeMetricObjectType = StrongTypeMetricObjectType.Enum + }); + } + + return dimensionConfigs; + } + + if (kind == TypeKind.Class) + { + if (specialType == SpecialType.System_String) + { + var name = TryGetDimensionNameFromAttribute(symbol, symbols, out var dimensionName) + ? dimensionName + : symbol.Name; + + if (!dimensionHashSet.Add(name)) + { + Diag(DiagDescriptors.ErrorDuplicateDimensionName, symbol.Locations[0], symbol.Name); + } + else + { + dimensionConfigs.Add(new StrongTypeConfig + { + Name = symbol.Name, + Path = stringBuilder.ToString(), + DimensionName = name, + StrongTypeMetricObjectType = StrongTypeMetricObjectType.String + }); + } + + return dimensionConfigs; + } + else if (specialType == SpecialType.None) + { + if (typeSymbol is INamedTypeSymbol namedTypeSymbol) + { + // User defined class, first add into dimensionConfigs, then walk the object model + + dimensionConfigs.Add(new StrongTypeConfig + { + Name = symbol.Name, + Path = stringBuilder.ToString(), + StrongTypeMetricObjectType = StrongTypeMetricObjectType.Class + }); + + dimensionConfigs.AddRange( + WalkObjectModel( + symbol, + namedTypeSymbol, + stringBuilder, + dimensionHashSet, + symbols, + true)); + + return dimensionConfigs; + } + } + else + { + Diag(DiagDescriptors.ErrorInvalidDimensionType, symbol.Locations[0]); + return dimensionConfigs; + } + } + + if (kind == TypeKind.Struct && specialType == SpecialType.None) + { + if (typeSymbol is not INamedTypeSymbol namedTypeSymbol) + { + Diag(DiagDescriptors.ErrorInvalidDimensionType, symbol.Locations[0]); + } + else + { + // User defined struct. First add into dimensionConfigs, then walk down the rest of the struct. + dimensionConfigs.Add(new StrongTypeConfig + { + Name = symbol.Name, + Path = stringBuilder.ToString(), + StrongTypeMetricObjectType = StrongTypeMetricObjectType.Struct + }); + + dimensionConfigs.AddRange( + WalkObjectModel( + symbol, + namedTypeSymbol, + stringBuilder, + dimensionHashSet, + symbols, + false)); + } + + return dimensionConfigs; + } + else + { + Diag(DiagDescriptors.ErrorInvalidDimensionType, symbol.Locations[0]); + return dimensionConfigs; + } + } + finally + { + _builders.ReturnStringBuilder(stringBuilder); + } + } + + private List WalkObjectModel( + ISymbol parentSymbol, + INamedTypeSymbol namedTypeSymbol, + StringBuilder stringBuilder, + HashSet dimensionHashSet, + SymbolHolder symbols, + bool isClass) + { + var dimensionConfigs = new List(); + + if (stringBuilder.Length != 0) + { + _ = stringBuilder.Append('.'); + } + + _ = stringBuilder.Append(parentSymbol.Name); + + if (isClass) + { + _ = stringBuilder.Append('?'); + } + + foreach (var member in namedTypeSymbol.GetMembers()) + { + dimensionConfigs.AddRange(BuildDimensionConfigs(member, dimensionHashSet, symbols, stringBuilder)); + } + + return dimensionConfigs; + } + + private List GetParentDimensionConfigs( + ITypeSymbol symbol, + HashSet dimensionHashSet, + SymbolHolder symbols) + { + var dimensionConfigs = new List(); + INamedTypeSymbol? parentObjectBase = symbol.BaseType; + + do + { + if (parentObjectBase == null) + { + continue; + } + + foreach (var member in parentObjectBase.GetMembers()) + { + dimensionConfigs.AddRange(BuildDimensionConfigs(member, dimensionHashSet, symbols, _builders.GetStringBuilder())); + } + + parentObjectBase = parentObjectBase.BaseType; + } + while (parentObjectBase?.BaseType != null); + + return dimensionConfigs; + } +} diff --git a/src/Generators/Microsoft.Gen.Metering/Common/Resources.Designer.cs b/src/Generators/Microsoft.Gen.Metering/Common/Resources.Designer.cs new file mode 100644 index 0000000000..d6a82342ef --- /dev/null +++ b/src/Generators/Microsoft.Gen.Metering/Common/Resources.Designer.cs @@ -0,0 +1,369 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Gen.Metering { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Gen.Metering.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Dimension {0} is defined more than once. Metric dimension names must be unique. + /// + internal static string ErrorDuplicateDimensionNameMessage { + get { + return ResourceManager.GetString("ErrorDuplicateDimensionNameMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A strong type object contains duplicate dimension names. + /// + internal static string ErrorDuplicateDimensionNameTitle { + get { + return ResourceManager.GetString("ErrorDuplicateDimensionNameTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A type '{0}' cannot be used as a metering attribute type argument. + /// + internal static string ErrorInvalidAttributeGenericTypeMessage { + get { + return ResourceManager.GetString("ErrorInvalidAttributeGenericTypeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A metering attribute type argument is invalid. + /// + internal static string ErrorInvalidAttributeGenericTypeTitle { + get { + return ResourceManager.GetString("ErrorInvalidAttributeGenericTypeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Dimension names should contain alphanumeric characters and only allowed symbols. + /// + internal static string ErrorInvalidDimensionNamesMessage { + get { + return ResourceManager.GetString("ErrorInvalidDimensionNamesMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Dimension names should only contain alphanumeric characters and allowed symbols. + /// + internal static string ErrorInvalidDimensionNamesTitle { + get { + return ResourceManager.GetString("ErrorInvalidDimensionNamesTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid dimension type. Valid types are string and enum. + /// + internal static string ErrorInvalidDimensionTypeMessage { + get { + return ResourceManager.GetString("ErrorInvalidDimensionTypeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A metric class contains an invalid dimension type. + /// + internal static string ErrorInvalidDimensionTypeTitle { + get { + return ResourceManager.GetString("ErrorInvalidDimensionTypeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric method names cannot start with _. + /// + internal static string ErrorInvalidMethodNameMessage { + get { + return ResourceManager.GetString("ErrorInvalidMethodNameMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric method names can't start with an underscore. + /// + internal static string ErrorInvalidMethodNameTitle { + get { + return ResourceManager.GetString("ErrorInvalidMethodNameTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric method '{0}' has return type '{1}' that is generic. + /// + internal static string ErrorInvalidMethodReturnTypeArityMessage { + get { + return ResourceManager.GetString("ErrorInvalidMethodReturnTypeArityMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric methods mustn't use any generic type as the return type. + /// + internal static string ErrorInvalidMethodReturnTypeArityTitle { + get { + return ResourceManager.GetString("ErrorInvalidMethodReturnTypeArityTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric method '{0}' has return type '{1}' that is defined in another namespace/class. + /// + internal static string ErrorInvalidMethodReturnTypeLocationMessage { + get { + return ResourceManager.GetString("ErrorInvalidMethodReturnTypeLocationMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric methods mustn't use any external type as the return type. + /// + internal static string ErrorInvalidMethodReturnTypeLocationTitle { + get { + return ResourceManager.GetString("ErrorInvalidMethodReturnTypeLocationTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric methods must not return '{0}' type. + /// + internal static string ErrorInvalidMethodReturnTypeMessage { + get { + return ResourceManager.GetString("ErrorInvalidMethodReturnTypeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric methods mustn't use any existing type as the return type. + /// + internal static string ErrorInvalidMethodReturnTypeTitle { + get { + return ResourceManager.GetString("ErrorInvalidMethodReturnTypeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric name {0} is invalid. It must be non empty and start with an alphabetic character. + /// + internal static string ErrorInvalidMetricNameMessage { + get { + return ResourceManager.GetString("ErrorInvalidMetricNameMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric names must start with an uppercase alphabetic character. + /// + internal static string ErrorInvalidMetricNameTitle { + get { + return ResourceManager.GetString("ErrorInvalidMetricNameTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric method parameter names cannot start with _. + /// + internal static string ErrorInvalidParameterNameMessage { + get { + return ResourceManager.GetString("ErrorInvalidParameterNameMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric method parameter names can't start with an underscore. + /// + internal static string ErrorInvalidParameterNameTitle { + get { + return ResourceManager.GetString("ErrorInvalidParameterNameTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric methods cannot have a body. + /// + internal static string ErrorMethodHasBodyMessage { + get { + return ResourceManager.GetString("ErrorMethodHasBodyMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric methods can't have a body. + /// + internal static string ErrorMethodHasBodyTitle { + get { + return ResourceManager.GetString("ErrorMethodHasBodyTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric methods cannot be generic. + /// + internal static string ErrorMethodIsGenericMessage { + get { + return ResourceManager.GetString("ErrorMethodIsGenericMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric methods can't be generic. + /// + internal static string ErrorMethodIsGenericTitle { + get { + return ResourceManager.GetString("ErrorMethodIsGenericTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Multiple metric methods are using metric name {0}. + /// + internal static string ErrorMetricNameReuseMessage { + get { + return ResourceManager.GetString("ErrorMetricNameReuseMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Multiple metric methods can't use the same metric name. + /// + internal static string ErrorMetricNameReuseTitle { + get { + return ResourceManager.GetString("ErrorMetricNameReuseTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to First parameter should be of System.Diagnostics.Metrics.Meter type. + /// + internal static string ErrorMissingMeterMessage { + get { + return ResourceManager.GetString("ErrorMissingMeterMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The first parameter should be of type `System.Diagnostics.Metrics.Meter`. + /// + internal static string ErrorMissingMeterTitle { + get { + return ResourceManager.GetString("ErrorMissingMeterTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric methods must be partial. + /// + internal static string ErrorNotPartialMethodMessage { + get { + return ResourceManager.GetString("ErrorNotPartialMethodMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric methods must be partial. + /// + internal static string ErrorNotPartialMethodTitle { + get { + return ResourceManager.GetString("ErrorNotPartialMethodTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric methods must be static. + /// + internal static string ErrorNotStaticMethodMessage { + get { + return ResourceManager.GetString("ErrorNotStaticMethodMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Metric methods must be static. + /// + internal static string ErrorNotStaticMethodTitle { + get { + return ResourceManager.GetString("ErrorNotStaticMethodTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The type {0} has too many dimensions. + /// + internal static string ErrorTooManyDimensionsMessage { + get { + return ResourceManager.GetString("ErrorTooManyDimensionsMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A metric class contains too many dimensions. + /// + internal static string ErrorTooManyDimensionsTitle { + get { + return ResourceManager.GetString("ErrorTooManyDimensionsTitle", resourceCulture); + } + } + } +} diff --git a/src/Generators/Microsoft.Gen.Metering/Common/Resources.resx b/src/Generators/Microsoft.Gen.Metering/Common/Resources.resx new file mode 100644 index 0000000000..b36410945b --- /dev/null +++ b/src/Generators/Microsoft.Gen.Metering/Common/Resources.resx @@ -0,0 +1,222 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Dimension {0} is defined more than once. Metric dimension names must be unique + + + A strong type object contains duplicate dimension names + + + A type '{0}' cannot be used as a metering attribute type argument + + + A metering attribute type argument is invalid + + + Dimension names should contain alphanumeric characters and only allowed symbols + + + Dimension names should only contain alphanumeric characters and allowed symbols + + + Invalid dimension type. Valid types are string and enum + + + A metric class contains an invalid dimension type + + + Metric method names cannot start with _ + + + Metric method names can't start with an underscore + + + Metric method '{0}' has return type '{1}' that is generic + + + Metric methods mustn't use any generic type as the return type + + + Metric method '{0}' has return type '{1}' that is defined in another namespace/class + + + Metric methods mustn't use any external type as the return type + + + Metric methods must not return '{0}' type + + + Metric methods mustn't use any existing type as the return type + + + Metric name {0} is invalid. It must be non empty and start with an alphabetic character + + + Metric names must start with an uppercase alphabetic character + + + Metric method parameter names cannot start with _ + + + Metric method parameter names can't start with an underscore + + + Metric methods cannot have a body + + + Metric methods can't have a body + + + Metric methods cannot be generic + + + Metric methods can't be generic + + + Multiple metric methods are using metric name {0} + + + Multiple metric methods can't use the same metric name + + + First parameter should be of System.Diagnostics.Metrics.Meter type + + + The first parameter should be of type `System.Diagnostics.Metrics.Meter` + + + Metric methods must be partial + + + Metric methods must be partial + + + Metric methods must be static + + + Metric methods must be static + + + The type {0} has too many dimensions + + + A metric class contains too many dimensions + + diff --git a/src/Generators/Microsoft.Gen.Metering/Common/SymbolHolder.cs b/src/Generators/Microsoft.Gen.Metering/Common/SymbolHolder.cs new file mode 100644 index 0000000000..49c1d73c2d --- /dev/null +++ b/src/Generators/Microsoft.Gen.Metering/Common/SymbolHolder.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Gen.Metering; + +internal sealed record class SymbolHolder( + INamedTypeSymbol MeterSymbol, + INamedTypeSymbol CounterAttribute, + INamedTypeSymbol? CounterOfTAttribute, + INamedTypeSymbol HistogramAttribute, + INamedTypeSymbol? HistogramOfTAttribute, + INamedTypeSymbol LongTypeSymbol, + INamedTypeSymbol? DimensionAttribute, + INamedTypeSymbol? IMeterInterface); diff --git a/src/Generators/Microsoft.Gen.Metering/Common/SymbolLoader.cs b/src/Generators/Microsoft.Gen.Metering/Common/SymbolLoader.cs new file mode 100644 index 0000000000..b131b3a5aa --- /dev/null +++ b/src/Generators/Microsoft.Gen.Metering/Common/SymbolLoader.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Gen.Metering; + +internal static class SymbolLoader +{ + internal const string CounterTAttribute = "Microsoft.Extensions.Telemetry.Metering.CounterAttribute`1"; + internal const string HistogramTAttribute = "Microsoft.Extensions.Telemetry.Metering.HistogramAttribute`1"; + internal const string CounterAttribute = "Microsoft.Extensions.Telemetry.Metering.CounterAttribute"; + internal const string HistogramAttribute = "Microsoft.Extensions.Telemetry.Metering.HistogramAttribute"; + internal const string DimensionAttribute = "Microsoft.Extensions.Telemetry.Metering.DimensionAttribute"; + internal const string MeterClass = "System.Diagnostics.Metrics.Meter"; + private const string MeterInterface = "Microsoft.Extensions.Telemetry.Metering.IMeter"; + + internal static SymbolHolder? LoadSymbols(Compilation compilation) + { + var meterClassSymbol = compilation.GetTypeByMetadataName(MeterClass); + var counterAttribute = compilation.GetTypeByMetadataName(CounterAttribute); + var histogramAttribute = compilation.GetTypeByMetadataName(HistogramAttribute); + if (meterClassSymbol == null || + counterAttribute == null || + histogramAttribute == null) + { + // nothing to do if these types aren't available + return null; + } + + var counterTAttribute = compilation.GetTypeByMetadataName(CounterTAttribute); + var histogramTAttribute = compilation.GetTypeByMetadataName(HistogramTAttribute); + var dimensionAttribute = compilation.GetTypeByMetadataName(DimensionAttribute); + var longType = compilation.GetSpecialType(SpecialType.System_Int64); + var meterInterface = compilation.GetTypeByMetadataName(MeterInterface); + + return new( + meterClassSymbol, + counterAttribute, + counterTAttribute, + histogramAttribute, + histogramTAttribute, + longType, + dimensionAttribute, + meterInterface); + } +} diff --git a/src/Generators/Microsoft.Gen.Metering/Directory.Build.props b/src/Generators/Microsoft.Gen.Metering/Directory.Build.props new file mode 100644 index 0000000000..120f36d4be --- /dev/null +++ b/src/Generators/Microsoft.Gen.Metering/Directory.Build.props @@ -0,0 +1,33 @@ + + + + + Microsoft.Gen.Metering + Code generator to support Microsoft.Extensions.Telemetry's metering features. + Telemetry + + + + cs + true + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.Metering/Roslyn3.8/Microsoft.Gen.Metering.Roslyn3.8.csproj b/src/Generators/Microsoft.Gen.Metering/Roslyn3.8/Microsoft.Gen.Metering.Roslyn3.8.csproj new file mode 100644 index 0000000000..260d679769 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Metering/Roslyn3.8/Microsoft.Gen.Metering.Roslyn3.8.csproj @@ -0,0 +1,27 @@ + + + Microsoft.Gen.Metering + 3.8 + $(MicrosoftCodeAnalysisVersion_3_8) + + + + dev + 92 + 85 + + + + + True + True + Resources.resx + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.Metering/Roslyn4.0/Microsoft.Gen.Metering.Roslyn4.0.csproj b/src/Generators/Microsoft.Gen.Metering/Roslyn4.0/Microsoft.Gen.Metering.Roslyn4.0.csproj new file mode 100644 index 0000000000..cb5ffbccb5 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Metering/Roslyn4.0/Microsoft.Gen.Metering.Roslyn4.0.csproj @@ -0,0 +1,28 @@ + + + Microsoft.Gen.Metering + 4.0 + $(MicrosoftCodeAnalysisVersion_4_0) + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + + + dev + 92 + 50 + + + + + True + True + Resources.resx + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.MeteringReports/Common/MetricDefinitionEmitter.cs b/src/Generators/Microsoft.Gen.MeteringReports/Common/MetricDefinitionEmitter.cs new file mode 100644 index 0000000000..4436617132 --- /dev/null +++ b/src/Generators/Microsoft.Gen.MeteringReports/Common/MetricDefinitionEmitter.cs @@ -0,0 +1,185 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using Microsoft.Gen.Metering.Model; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.MeteringReports; + +// Stryker disable all + +internal static class MetricDefinitionEmitter +{ + private static readonly StringBuilderPool _builders = new(); + + public static string GenerateReport(IReadOnlyList metricClasses, CancellationToken cancellationToken) + { + if (metricClasses == null || metricClasses.Count == 0) + { + return string.Empty; + } + + var sb = _builders.GetStringBuilder(); + try + { + _ = sb.Append('[') + .Append('\n'); + + for (int i = 0; i < metricClasses.Count; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + var metricClass = metricClasses[i]; + _ = sb.Append(GenMetricClassDefinition(metricClass, cancellationToken)); + + if (i < metricClasses.Count - 1) + { + _ = sb.Append(','); + } + + _ = sb.Append('\n'); + } + + _ = sb.Append(']'); + + return sb.ToString(); + } + finally + { + _builders.ReturnStringBuilder(sb); + } + } + + private static string GenMetricClassDefinition(ReportedMetricClass metricClass, CancellationToken cancellationToken) + { + var sb = _builders.GetStringBuilder(); + try + { + cancellationToken.ThrowIfCancellationRequested(); + _ = sb.Append(" {") + .Append('\n'); + + _ = sb.Append($" \"{metricClass.RootNamespace}\":"); + _ = sb.Append('\n'); + + if (metricClass.Methods.Length > 0) + { + _ = sb.Append(" [") + .Append('\n'); + + for (int j = 0; j < metricClass.Methods.Length; j++) + { + var metricMethod = metricClass.Methods[j]; + + _ = sb.Append(GenMetricMethodDefinition(metricMethod, cancellationToken)); + + if (j < metricClass.Methods.Length - 1) + { + _ = sb.Append(','); + } + + _ = sb.Append('\n'); + } + + _ = sb.Append(" ]") + .Append('\n'); + } + + _ = sb.Append(" }"); + + return sb.ToString(); + } + finally + { + _builders.ReturnStringBuilder(sb); + } + } + + private static string GenMetricMethodDefinition(ReportedMetricMethod metricMethod, CancellationToken cancellationToken) + { + switch (metricMethod.Kind) + { + case InstrumentKind.Counter: + case InstrumentKind.Histogram: + case InstrumentKind.Gauge: + var sb = _builders.GetStringBuilder(); + try + { + cancellationToken.ThrowIfCancellationRequested(); + + _ = sb.Append(" {") + .Append('\n'); + + _ = sb.Append($" \"MetricName\": \"{metricMethod.MetricName.Replace("\\", "\\\\").Replace("\"", "\\\"")}\",") + .Append('\n'); + + if (!string.IsNullOrEmpty(metricMethod.Summary)) + { + _ = sb.Append($" \"MetricDescription\": \"{metricMethod.Summary.Replace("\\", "\\\\").Replace("\"", "\\\"")}\","); + _ = sb.Append('\n'); + } + + _ = sb.Append($" \"InstrumentName\": \"{metricMethod.Kind}\""); + + if (metricMethod.Dimensions.Count > 0) + { + _ = sb.Append(','); + _ = sb.Append('\n'); + _ = sb.Append(" \"Dimensions\": {"); + + int k = 0; + + foreach (var dimension in metricMethod.Dimensions) + { + _ = sb.Append('\n'); + if (metricMethod.DimensionsDescriptions.TryGetValue(dimension, out var description)) + { + _ = sb.Append($" \"{dimension}\": \"{description.Replace("\\", "\\\\").Replace("\"", "\\\"")}\""); + } + else + { + _ = sb.Append($" \"{dimension}\": \"\""); + } + + if (k < metricMethod.Dimensions.Count - 1) + { + _ = sb.Append(','); + } + + k++; + } + + _ = sb.Append('\n'); + _ = sb.Append(" }"); + _ = sb.Append('\n'); + } + else + { + _ = sb.Append('\n'); + } + + _ = sb.Append(" }"); + + return sb.ToString(); + } + catch (Exception e) + { + // This should report diagnostic. + throw new InvalidOperationException($"An exception occurred during metric report generation {e.GetType()}:{e.Message}."); + } + finally + { + _builders.ReturnStringBuilder(sb); + } + + case InstrumentKind.None: + case InstrumentKind.CounterT: + case InstrumentKind.HistogramT: + default: + // This should report diagnostic. + throw new NotSupportedException($"Report for metric kind: '{metricMethod.Kind}' is not supported."); + } + } +} diff --git a/src/Generators/Microsoft.Gen.MeteringReports/Common/MetricDefinitionGenerator.cs b/src/Generators/Microsoft.Gen.MeteringReports/Common/MetricDefinitionGenerator.cs new file mode 100644 index 0000000000..408948655e --- /dev/null +++ b/src/Generators/Microsoft.Gen.MeteringReports/Common/MetricDefinitionGenerator.cs @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Gen.Metering; +using Microsoft.Gen.Metering.Model; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.MeteringReports; + +[Generator] +[ExcludeFromCodeCoverage] +public class MetricDefinitionGenerator : ISourceGenerator +{ + private const string GenerateMetricDefinitionReport = "build_property.GenerateMeteringReport"; + private const string RootNamespace = "build_property.rootnamespace"; + private const string ReportOutputPath = "build_property.MeteringReportOutputPath"; + private const string CompilationOutputPath = "build_property.outputpath"; + private const string CurrentProjectPath = "build_property.projectdir"; + private const string FileName = "MeteringReport.json"; + + private static readonly Dictionary _empty = new(); + + private string? _compilationOutputPath; + private string? _currentProjectPath; + private string? _reportOutputPath; + + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(ClassDeclarationSyntaxReceiver.Create); + } + + public void Execute(GeneratorExecutionContext context) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + var receiver = context.SyntaxReceiver as ClassDeclarationSyntaxReceiver; + if (receiver == null || receiver.ClassDeclarations.Count == 0 || !GeneratorUtilities.ShouldGenerateReport(context, GenerateMetricDefinitionReport)) + { + return; + } + + var meteringParser = new Metering.Parser(context.Compilation, context.ReportDiagnostic, context.CancellationToken); + + var meteringClasses = meteringParser.GetMetricClasses(receiver.ClassDeclarations); + + if (meteringClasses.Count == 0) + { + return; + } + + var options = context.AnalyzerConfigOptions.GlobalOptions; + + var path = (_reportOutputPath != null || options.TryGetValue(ReportOutputPath, out _reportOutputPath)) + ? _reportOutputPath + : GetDefaultReportOutputPath(options); + + if (string.IsNullOrWhiteSpace(path)) + { + // Report diagnostic. Tell that it is either missing or visibility to compiler. + return; + } + + _ = options.TryGetValue(RootNamespace, out var rootNamespace); + + var reportedMetrics = MapToCommonModel(meteringClasses, rootNamespace); + var report = MetricDefinitionEmitter.GenerateReport(reportedMetrics, context.CancellationToken); + +#pragma warning disable R9A017 // Switch to an asynchronous metricMethod for increased performance; Cannot because it is void metricMethod, and generators dont support tasks. + File.WriteAllText(Path.Combine(path, FileName), report, Encoding.UTF8); +#pragma warning restore R9A017 // Switch to an asynchronous metricMethod for increased performance; Cannot because it is void metricMethod, and generators dont support tasks. + } + + private static ReportedMetricClass[] MapToCommonModel(IReadOnlyList meteringClasses, string? rootNamespace) + { + var reportedMetrics = meteringClasses + .Select(meteringClass => new ReportedMetricClass( + Name: meteringClass.Name, + RootNamespace: rootNamespace ?? meteringClass.Namespace, + Constraints: meteringClass.Constraints, + Modifiers: meteringClass.Modifiers, + Methods: meteringClass.Methods.Select(meteringMethod => new ReportedMetricMethod( + MetricName: meteringMethod.MetricName ?? "(Missing Name)", + Summary: "(Missing Summary)" /* Missing feature in .NET metering generator. */, + Kind: meteringMethod.InstrumentKind, + Dimensions: meteringMethod.DimensionsKeys, + DimensionsDescriptions: _empty /* Missing feature in .NET metering generator. */)) + .ToArray())); + + return reportedMetrics.ToArray(); + } + + private string GetDefaultReportOutputPath(AnalyzerConfigOptions options) + { + if (_currentProjectPath != null && _compilationOutputPath != null) + { + return _currentProjectPath + _compilationOutputPath; + } + + _ = options.TryGetValue(CompilationOutputPath, out _compilationOutputPath); + _ = options.TryGetValue(CurrentProjectPath, out _currentProjectPath); + + return string.IsNullOrWhiteSpace(_currentProjectPath) || string.IsNullOrWhiteSpace(_compilationOutputPath) + ? string.Empty + : _currentProjectPath + _compilationOutputPath; + } +} diff --git a/src/Generators/Microsoft.Gen.MeteringReports/Common/ReportedMetricClass.cs b/src/Generators/Microsoft.Gen.MeteringReports/Common/ReportedMetricClass.cs new file mode 100644 index 0000000000..9ed9f8bfa1 --- /dev/null +++ b/src/Generators/Microsoft.Gen.MeteringReports/Common/ReportedMetricClass.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Gen.Metering.Model; + +namespace Microsoft.Gen.MeteringReports; + +internal readonly record struct ReportedMetricClass(string Name, string RootNamespace, string Constraints, string Modifiers, ReportedMetricMethod[] Methods); + +internal readonly record struct ReportedMetricMethod(string MetricName, string Summary, InstrumentKind Kind, HashSet Dimensions, Dictionary DimensionsDescriptions); diff --git a/src/Generators/Microsoft.Gen.MeteringReports/Directory.Build.props b/src/Generators/Microsoft.Gen.MeteringReports/Directory.Build.props new file mode 100644 index 0000000000..42e800fc30 --- /dev/null +++ b/src/Generators/Microsoft.Gen.MeteringReports/Directory.Build.props @@ -0,0 +1,42 @@ + + + + + Microsoft.Gen.MeteringReports + Generates reports about metric usage in the project being compiled. + Telemetry + + + + cs + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.MeteringReports/Roslyn3.8/Microsoft.Gen.MeteringReports.Roslyn3.8.csproj b/src/Generators/Microsoft.Gen.MeteringReports/Roslyn3.8/Microsoft.Gen.MeteringReports.Roslyn3.8.csproj new file mode 100644 index 0000000000..e70e0f7190 --- /dev/null +++ b/src/Generators/Microsoft.Gen.MeteringReports/Roslyn3.8/Microsoft.Gen.MeteringReports.Roslyn3.8.csproj @@ -0,0 +1,27 @@ + + + Microsoft.Gen.MeteringReports + 3.8 + $(MicrosoftCodeAnalysisVersion_3_8) + + + + dev + 9 + 85 + + + + + True + True + Resources.resx + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.MeteringReports/Roslyn4.0/Microsoft.Gen.MeteringReports.Roslyn4.0.csproj b/src/Generators/Microsoft.Gen.MeteringReports/Roslyn4.0/Microsoft.Gen.MeteringReports.Roslyn4.0.csproj new file mode 100644 index 0000000000..1b1a26dcb0 --- /dev/null +++ b/src/Generators/Microsoft.Gen.MeteringReports/Roslyn4.0/Microsoft.Gen.MeteringReports.Roslyn4.0.csproj @@ -0,0 +1,28 @@ + + + Microsoft.Gen.MeteringReports + 4.0 + $(MicrosoftCodeAnalysisVersion_4_0) + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + + + dev + 9 + 0 + + + + + True + True + Resources.resx + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.OptionsValidation/Common/DiagDescriptors.cs b/src/Generators/Microsoft.Gen.OptionsValidation/Common/DiagDescriptors.cs new file mode 100644 index 0000000000..25a4bbe69c --- /dev/null +++ b/src/Generators/Microsoft.Gen.OptionsValidation/Common/DiagDescriptors.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.OptionsValidation; + +internal sealed class DiagDescriptors : DiagDescriptorsBase +{ + private const string Category = "OptionsValidation"; + + // Skipping R9G100 + + public static DiagnosticDescriptor CantUseWithGenericTypes { get; } = Make( + id: "R9G101", + title: Resources.CantUseWithGenericTypesTitle, + messageFormat: Resources.CantUseWithGenericTypesMessage, + category: Category); + + public static DiagnosticDescriptor NoEligibleMember { get; } = Make( + id: "R9G102", + title: Resources.NoEligibleMemberTitle, + messageFormat: Resources.NoEligibleMemberMessage, + category: Category, + defaultSeverity: DiagnosticSeverity.Warning); + + public static DiagnosticDescriptor NoEligibleMembersFromValidator { get; } = Make( + id: "R9G103", + title: Resources.NoEligibleMembersFromValidatorTitle, + messageFormat: Resources.NoEligibleMembersFromValidatorMessage, + category: Category, + defaultSeverity: DiagnosticSeverity.Warning); + + public static DiagnosticDescriptor DoesntImplementIValidateOptions { get; } = Make( + id: "R9G104", + title: Resources.DoesntImplementIValidateOptionsTitle, + messageFormat: Resources.DoesntImplementIValidateOptionsMessage, + category: Category); + + public static DiagnosticDescriptor AlreadyImplementsValidateMethod { get; } = Make( + id: "R9G105", + title: Resources.AlreadyImplementsValidateMethodTitle, + messageFormat: Resources.AlreadyImplementsValidateMethodMessage, + category: Category); + + public static DiagnosticDescriptor MemberIsInaccessible { get; } = Make( + id: "R9G106", + title: Resources.MemberIsInaccessibleTitle, + messageFormat: Resources.MemberIsInaccessibleMessage, + category: Category); + + public static DiagnosticDescriptor NotEnumerableType { get; } = Make( + id: "R9G107", + title: Resources.NotEnumerableTypeTitle, + messageFormat: Resources.NotEnumerableTypeMessage, + category: Category); + + public static DiagnosticDescriptor ValidatorsNeedSimpleConstructor { get; } = Make( + id: "R9G108", + title: Resources.ValidatorsNeedSimpleConstructorTitle, + messageFormat: Resources.ValidatorsNeedSimpleConstructorMessage, + category: Category); + + public static DiagnosticDescriptor CantBeStaticClass { get; } = Make( + id: "R9G109", + title: Resources.CantBeStaticClassTitle, + messageFormat: Resources.CantBeStaticClassMessage, + category: Category); + + public static DiagnosticDescriptor NullValidatorType { get; } = Make( + id: "R9G110", + title: Resources.NullValidatorTypeTitle, + messageFormat: Resources.NullValidatorTypeMessage, + category: Category); + + public static DiagnosticDescriptor CircularTypeReferences { get; } = Make( + id: "R9G111", + title: Resources.CircularTypeReferencesTitle, + messageFormat: Resources.CircularTypeReferencesMessage, + category: Category); + + // 112 is available for reuse + + public static DiagnosticDescriptor PotentiallyMissingTransitiveValidation { get; } = Make( + id: "R9G113", + title: Resources.PotentiallyMissingTransitiveValidationTitle, + messageFormat: Resources.PotentiallyMissingTransitiveValidationMessage, + category: Category, + defaultSeverity: DiagnosticSeverity.Warning); + + public static DiagnosticDescriptor PotentiallyMissingEnumerableValidation { get; } = Make( + id: "R9G114", + title: Resources.PotentiallyMissingEnumerableValidationTitle, + messageFormat: Resources.PotentiallyMissingEnumerableValidationMessage, + category: Category, + defaultSeverity: DiagnosticSeverity.Warning); +} diff --git a/src/Generators/Microsoft.Gen.OptionsValidation/Common/Emitter.cs b/src/Generators/Microsoft.Gen.OptionsValidation/Common/Emitter.cs new file mode 100644 index 0000000000..c7103aec95 --- /dev/null +++ b/src/Generators/Microsoft.Gen.OptionsValidation/Common/Emitter.cs @@ -0,0 +1,368 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Microsoft.Gen.OptionsValidation.Model; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.OptionsValidation; + +// Stryker disable all + +/// +/// Emits option validation. +/// +internal sealed class Emitter : EmitterBase +{ + private const string StaticValidationAttributeHolderClassName = "__Attributes"; + private const string StaticValidatorHolderClassName = "__Validators"; + private const string StaticFieldHolderClassesNamespace = "__OptionValidationStaticInstances"; + private const string StaticValidationAttributeHolderClassFQN = $"global::{StaticFieldHolderClassesNamespace}.{StaticValidationAttributeHolderClassName}"; + private const string StaticValidatorHolderClassFQN = $"global::{StaticFieldHolderClassesNamespace}.{StaticValidatorHolderClassName}"; + private sealed record StaticFieldInfo(string FieldTypeFQN, int FieldOrder, string FieldName, IList InstantiationLines); + + public string Emit( + IEnumerable validatorTypes, + CancellationToken cancellationToken) + { + var staticValidationAttributesDict = new Dictionary(); + var staticValidatorsDict = new Dictionary(); + + foreach (var vt in validatorTypes.OrderBy(static lt => lt.Namespace + "." + lt.Name)) + { + cancellationToken.ThrowIfCancellationRequested(); + GenValidatorType(vt, ref staticValidationAttributesDict, ref staticValidatorsDict); + } + + GenStaticClassWithStaticReadonlyFields(staticValidationAttributesDict.Values, StaticFieldHolderClassesNamespace, StaticValidationAttributeHolderClassName); + GenStaticClassWithStaticReadonlyFields(staticValidatorsDict.Values, StaticFieldHolderClassesNamespace, StaticValidatorHolderClassName); + + return Capture(); + } + + private void GenValidatorType(ValidatorType vt, ref Dictionary staticValidationAttributesDict, ref Dictionary staticValidatorsDict) + { + OutLn("#pragma warning disable CS0618 // Type or member is obsolete"); + + if (vt.Namespace.Length > 0) + { + OutLn($"namespace {vt.Namespace}"); + OutOpenBrace(); + } + + foreach (var p in vt.ParentTypes) + { + OutLn(p); + OutOpenBrace(); + } + + if (vt.IsSynthetic) + { + OutGeneratedCodeAttribute(); + OutLn($"internal sealed partial {vt.DeclarationKeyword} {vt.Name}"); + } + else + { + OutLn($"partial {vt.DeclarationKeyword} {vt.Name}"); + } + + OutOpenBrace(); + + for (var i = 0; i < vt.ModelsToValidate.Count; i++) + { + var modelToValidate = vt.ModelsToValidate[i]; + + GenModelValidationMethod(modelToValidate, vt.IsSynthetic, ref staticValidationAttributesDict, ref staticValidatorsDict); + } + + OutCloseBrace(); + + foreach (var _ in vt.ParentTypes) + { + OutCloseBrace(); + } + + if (vt.Namespace.Length > 0) + { + OutCloseBrace(); + } + } + + private void GenStaticClassWithStaticReadonlyFields(IEnumerable staticFields, string classNamespace, string className) + { + OutLn($"namespace {classNamespace}"); + OutOpenBrace(); + + OutGeneratedCodeAttribute(); + OutLn($"file static class {className}"); + OutOpenBrace(); + + var staticValidationAttributes = staticFields + .OrderBy(x => x.FieldOrder) + .ToArray(); + + for (var i = 0; i < staticValidationAttributes.Length; i++) + { + var attributeInstance = staticValidationAttributes[i]; + OutIndent(); + Out($"internal static readonly {attributeInstance.FieldTypeFQN} {attributeInstance.FieldName} = "); + for (var j = 0; j < attributeInstance.InstantiationLines.Count; j++) + { + var line = attributeInstance.InstantiationLines[j]; + Out(line); + if (j != attributeInstance.InstantiationLines.Count - 1) + { + OutLn(); + OutIndent(); + } + else + { + Out(';'); + } + } + + OutLn(); + + if (i != staticValidationAttributes.Length - 1) + { + OutLn(); + } + } + + OutCloseBrace(); + + OutCloseBrace(); + } + + private void GenModelSelfValidationIfNecessary(ValidatedModel modelToValidate) + { + if (modelToValidate.SelfValidates) + { + OutLn($"builder.AddResults(((global::System.ComponentModel.DataAnnotations.IValidatableObject)options).Validate(context));"); + OutLn(); + } + } + + private void GenModelValidationMethod( + ValidatedModel modelToValidate, + bool makeStatic, + ref Dictionary staticValidationAttributesDict, + ref Dictionary staticValidatorsDict) + { + OutLn($"/// "); + OutLn($"/// Validates a specific named options instance (or all when is )."); + OutLn($"/// "); + OutLn($"/// The name of the options instance being validated."); + OutLn($"/// The options instance."); + OutLn($"/// Validation result."); + OutGeneratedCodeAttribute(); + + OutLn($"public {(makeStatic ? "static " : string.Empty)}global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, {modelToValidate.Name} options)"); + OutOpenBrace(); + OutLn($"var baseName = (string.IsNullOrEmpty(name) ? \"{modelToValidate.SimpleName}\" : name) + \".\";"); + OutLn($"var builder = new global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder();"); + OutLn($"var context = new global::System.ComponentModel.DataAnnotations.ValidationContext(options);"); + OutLn(); + + foreach (var vm in modelToValidate.MembersToValidate) + { + if (vm.ValidationAttributes.Count > 0) + { + GenMemberValidation(vm, ref staticValidationAttributesDict); + OutLn(); + } + + if (vm.TransValidatorType != null) + { + GenTransitiveValidation(vm, ref staticValidatorsDict); + OutLn(); + } + + if (vm.EnumerationValidatorType != null) + { + GenEnumerationValidation(vm, ref staticValidatorsDict); + OutLn(); + } + } + + GenModelSelfValidationIfNecessary(modelToValidate); + OutLn($"return builder.Build();"); + OutCloseBrace(); + } + + private void GenMemberValidation(ValidatedMember vm, + ref Dictionary staticValidationAttributesDict) + { + OutLn($"context.MemberName = \"{vm.Name}\";"); + OutLn($"context.DisplayName = baseName + \"{vm.Name}\";"); + + foreach (var attr in vm.ValidationAttributes) + { + var staticValidationAttributeInstance = GetOrAddStaticValidationAttribute(ref staticValidationAttributesDict, attr); + + OutLn($"builder.AddResult({StaticValidationAttributeHolderClassFQN}.{staticValidationAttributeInstance.FieldName}.GetValidationResult(options.{vm.Name}, context));"); + } + } + + private StaticFieldInfo GetOrAddStaticValidationAttribute(ref Dictionary staticValidationAttributesDict, ValidationAttributeInfo attr) + { + var attrInstantiationStatementLines = new List(); + + if (attr.ConstructorArguments.Count > 0) + { + attrInstantiationStatementLines.Add($"new {attr.AttributeName}("); + + for (var i = 0; i < attr.ConstructorArguments.Count; i++) + { + if (i != attr.ConstructorArguments.Count - 1) + { + attrInstantiationStatementLines.Add($"{GetPaddingString(1)}{attr.ConstructorArguments[i]},"); + } + else + { + attrInstantiationStatementLines.Add($"{GetPaddingString(1)}{attr.ConstructorArguments[i]})"); + } + } + } + else + { + attrInstantiationStatementLines.Add($"new {attr.AttributeName}()"); + } + + if (attr.Properties.Count > 0) + { + attrInstantiationStatementLines.Add("{"); + + var propertiesOrderedByKey = attr.Properties + .OrderBy(p => p.Key) + .ToArray(); + + for (var i = 0; i < propertiesOrderedByKey.Length; i++) + { + var prop = propertiesOrderedByKey[i]; + var notLast = i != propertiesOrderedByKey.Length - 1; + attrInstantiationStatementLines.Add($"{GetPaddingString(1)}{prop.Key} = {prop.Value}{(notLast ? "," : string.Empty)}"); + } + + attrInstantiationStatementLines.Add("}"); + } + + var instantiationStatement = string.Join(Environment.NewLine, attrInstantiationStatementLines); + + if (!staticValidationAttributesDict.TryGetValue(instantiationStatement, out var staticValidationAttributeInstance)) + { + var fieldNumber = staticValidationAttributesDict.Count + 1; + staticValidationAttributeInstance = new StaticFieldInfo( + FieldTypeFQN: attr.AttributeName, + FieldOrder: fieldNumber, + FieldName: $"A{fieldNumber}", + InstantiationLines: attrInstantiationStatementLines); + + staticValidationAttributesDict.Add(instantiationStatement, staticValidationAttributeInstance); + } + + return staticValidationAttributeInstance; + } + + private void GenTransitiveValidation(ValidatedMember vm, ref Dictionary staticValidatorsDict) + { + string callSequence; + if (vm.TransValidateTypeIsSynthetic) + { + callSequence = vm.TransValidatorType!; + } + else + { + var staticValidatorInstance = GetOrAddStaticValidator(ref staticValidatorsDict, vm.TransValidatorType!); + + callSequence = $"{StaticValidatorHolderClassFQN}.{staticValidatorInstance.FieldName}"; + } + + var valueAccess = (vm.IsNullable && vm.IsValueType) ? ".Value" : string.Empty; + + if (vm.IsNullable) + { + OutLn($"if (options.{vm.Name} != null)"); + OutLn($"{{"); + OutLn($" builder.AddResult({callSequence}.Validate(baseName + \"{vm.Name}\", options.{vm.Name}{valueAccess}));"); + OutLn($"}}"); + } + else + { + OutLn($"builder.AddResult({callSequence}.Validate(baseName + \"{vm.Name}\", options.{vm.Name}{valueAccess}));"); + } + } + + private void GenEnumerationValidation(ValidatedMember vm, ref Dictionary staticValidatorsDict) + { + var valueAccess = (vm.IsValueType && vm.IsNullable) ? ".Value" : string.Empty; + var enumeratedValueAccess = (vm.EnumeratedIsNullable && vm.EnumeratedIsValueType) ? ".Value" : string.Empty; + string callSequence; + if (vm.EnumerationValidatorTypeIsSynthetic) + { + callSequence = vm.EnumerationValidatorType!; + } + else + { + var staticValidatorInstance = GetOrAddStaticValidator(ref staticValidatorsDict, vm.EnumerationValidatorType!); + + callSequence = $"{StaticValidatorHolderClassFQN}.{staticValidatorInstance.FieldName}"; + } + + if (vm.IsNullable) + { + OutLn($"if (options.{vm.Name} != null)"); + } + + OutOpenBrace(); + + OutLn($"var count = 0;"); + OutLn($"foreach (var o in options.{vm.Name}{valueAccess})"); + OutOpenBrace(); + + if (vm.EnumeratedIsNullable) + { + OutLn($"if (o is not null)"); + OutLn($"{{"); + OutLn($" builder.AddResult({callSequence}.Validate(baseName + $\"{vm.Name}[{{count++}}]\", o{enumeratedValueAccess}));"); + OutLn($"}}"); + + if (!vm.EnumeratedMayBeNull) + { + OutLn($"else"); + OutLn($"{{"); + OutLn($" builder.AddError(baseName + $\"{vm.Name}[{{count++}}] is null\");"); + OutLn($"}}"); + } + } + else + { + OutLn($"builder.AddResult({callSequence}.Validate(baseName + $\"{vm.Name}[{{count++}}]\", o{enumeratedValueAccess}));"); + } + + OutCloseBrace(); + OutCloseBrace(); + } + +#pragma warning disable CA1822 // Mark members as static: static should come before non-static, but we want the method to be here + private StaticFieldInfo GetOrAddStaticValidator(ref Dictionary staticValidatorsDict, string validatorTypeFQN) +#pragma warning restore CA1822 + { + if (!staticValidatorsDict.TryGetValue(validatorTypeFQN, out var staticValidatorInstance)) + { + var fieldNumber = staticValidatorsDict.Count + 1; + staticValidatorInstance = new StaticFieldInfo( + FieldTypeFQN: validatorTypeFQN, + FieldOrder: fieldNumber, + FieldName: $"V{fieldNumber}", + InstantiationLines: new[] { $"new {validatorTypeFQN}()" }); + + staticValidatorsDict.Add(validatorTypeFQN, staticValidatorInstance); + } + + return staticValidatorInstance; + } +} diff --git a/src/Generators/Microsoft.Gen.OptionsValidation/Common/Generator.cs b/src/Generators/Microsoft.Gen.OptionsValidation/Common/Generator.cs new file mode 100644 index 0000000000..3f0a49c5a3 --- /dev/null +++ b/src/Generators/Microsoft.Gen.OptionsValidation/Common/Generator.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if ROSLYN_4_0_OR_GREATER + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.OptionsValidation; + +[Generator] +[ExcludeFromCodeCoverage] +public class Generator : IIncrementalGenerator +{ + private static readonly HashSet _attributeNames = new() + { + SymbolLoader.OptionsValidatorAttribute, + }; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + GeneratorUtilities.Initialize(context, _attributeNames, HandleAnnotatedTypes); + } + + private static void HandleAnnotatedTypes(Compilation compilation, IEnumerable nodes, SourceProductionContext context) + { + if (!SymbolLoader.TryLoad(compilation, out var symbolHolder)) + { + // Not eligible compilation + return; + } + + var parser = new Parser(compilation, context.ReportDiagnostic, symbolHolder!, context.CancellationToken); + + var validatorTypes = parser.GetValidatorTypes(nodes.OfType()); + if (validatorTypes.Count > 0) + { + var emitter = new Emitter(); + var result = emitter.Emit(validatorTypes, context.CancellationToken); + + context.AddSource("Validators.g.cs", SourceText.From(result, Encoding.UTF8)); + } + } +} + +#else + +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.OptionsValidation; + +/// +/// Generates for classes that are marked with . +/// +[Generator] +[ExcludeFromCodeCoverage] +public class Generator : ISourceGenerator +{ + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(TypeDeclarationSyntaxReceiver.Create); + } + + public void Execute(GeneratorExecutionContext context) + { + var receiver = context.SyntaxReceiver as TypeDeclarationSyntaxReceiver; + if (receiver == null || receiver.TypeDeclarations.Count == 0) + { + // nothing to do yet + return; + } + + if (!SymbolLoader.TryLoad(context.Compilation, out var symbolHolder)) + { + // Not eligible compilation + return; + } + + var parser = new Parser(context.Compilation, context.ReportDiagnostic, symbolHolder!, context.CancellationToken); + var validatorTypes = parser.GetValidatorTypes(receiver.TypeDeclarations); + if (validatorTypes.Count > 0) + { + var emitter = new Emitter(); + var result = emitter.Emit(validatorTypes, context.CancellationToken); + context.AddSource("Validators.g.cs", SourceText.From(result, Encoding.UTF8)); + } + } +} + +#endif diff --git a/src/Generators/Microsoft.Gen.OptionsValidation/Common/Model/ValidatedMember.cs b/src/Generators/Microsoft.Gen.OptionsValidation/Common/Model/ValidatedMember.cs new file mode 100644 index 0000000000..0ffe9385e8 --- /dev/null +++ b/src/Generators/Microsoft.Gen.OptionsValidation/Common/Model/ValidatedMember.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Gen.OptionsValidation.Model; + +internal sealed record class ValidatedMember( + string Name, + List ValidationAttributes, + string? TransValidatorType, + bool TransValidateTypeIsSynthetic, + string? EnumerationValidatorType, + bool EnumerationValidatorTypeIsSynthetic, + bool IsNullable, + bool IsValueType, + bool EnumeratedIsNullable, + bool EnumeratedIsValueType, + bool EnumeratedMayBeNull); diff --git a/src/Generators/Microsoft.Gen.OptionsValidation/Common/Model/ValidatedModel.cs b/src/Generators/Microsoft.Gen.OptionsValidation/Common/Model/ValidatedModel.cs new file mode 100644 index 0000000000..8b354f5d23 --- /dev/null +++ b/src/Generators/Microsoft.Gen.OptionsValidation/Common/Model/ValidatedModel.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Gen.OptionsValidation.Model; + +internal sealed record class ValidatedModel( + string Name, + string SimpleName, + bool SelfValidates, + List MembersToValidate); diff --git a/src/Generators/Microsoft.Gen.OptionsValidation/Common/Model/ValidationAttributeInfo.cs b/src/Generators/Microsoft.Gen.OptionsValidation/Common/Model/ValidationAttributeInfo.cs new file mode 100644 index 0000000000..111073d1aa --- /dev/null +++ b/src/Generators/Microsoft.Gen.OptionsValidation/Common/Model/ValidationAttributeInfo.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Gen.OptionsValidation.Model; + +internal sealed record class ValidationAttributeInfo(string AttributeName) +{ + public List ConstructorArguments { get; } = new(); + public Dictionary Properties { get; } = new(); +} diff --git a/src/Generators/Microsoft.Gen.OptionsValidation/Common/Model/ValidatorType.cs b/src/Generators/Microsoft.Gen.OptionsValidation/Common/Model/ValidatorType.cs new file mode 100644 index 0000000000..27c5caaa63 --- /dev/null +++ b/src/Generators/Microsoft.Gen.OptionsValidation/Common/Model/ValidatorType.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Gen.OptionsValidation.Model; + +internal sealed record class ValidatorType( + string Namespace, + string Name, + string NameWithoutGenerics, + string DeclarationKeyword, + List ParentTypes, + bool IsSynthetic, + IList ModelsToValidate); diff --git a/src/Generators/Microsoft.Gen.OptionsValidation/Common/Parser.cs b/src/Generators/Microsoft.Gen.OptionsValidation/Common/Parser.cs new file mode 100644 index 0000000000..4b27c09597 --- /dev/null +++ b/src/Generators/Microsoft.Gen.OptionsValidation/Common/Parser.cs @@ -0,0 +1,696 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Gen.OptionsValidation.Model; +using Microsoft.Gen.Shared; + +namespace Microsoft.Gen.OptionsValidation; + +/// +/// Holds an internal parser class that extracts necessary information for generating IValidateOptions. +/// +internal sealed class Parser +{ + private const int NumValidationMethodArgs = 2; + + private readonly CancellationToken _cancellationToken; + private readonly Compilation _compilation; + private readonly Action _reportDiagnostic; + private readonly SymbolHolder _symbolHolder; + private readonly Dictionary _synthesizedValidators = new(SymbolEqualityComparer.Default); + private readonly HashSet _visitedModelTypes = new(SymbolEqualityComparer.Default); + + public Parser( + Compilation compilation, + Action reportDiagnostic, + SymbolHolder symbolHolder, + CancellationToken cancellationToken) + { + _compilation = compilation; + _cancellationToken = cancellationToken; + _reportDiagnostic = reportDiagnostic; + _symbolHolder = symbolHolder; + } + + public IReadOnlyList GetValidatorTypes(IEnumerable classes) + { + var results = new List(); + + foreach (var group in classes.GroupBy(x => x.SyntaxTree)) + { + SemanticModel? sm = null; + foreach (var typeDec in group) + { + _cancellationToken.ThrowIfCancellationRequested(); + sm ??= _compilation.GetSemanticModel(typeDec.SyntaxTree); + + var validatorType = sm.GetDeclaredSymbol(typeDec) as ITypeSymbol; + if (validatorType != null) + { +#if !ROSLYN_4_0_OR_GREATER + if (!IsAnnotated(validatorType)) + { + continue; + } +#endif + if (validatorType.IsStatic) + { + Diag(DiagDescriptors.CantBeStaticClass, typeDec.GetLocation()); + continue; + } + + _visitedModelTypes.Clear(); + + var modelTypes = GetModelTypes(validatorType); + if (modelTypes.Count == 0) + { + // validator doesn't implement IValidateOptions + Diag(DiagDescriptors.DoesntImplementIValidateOptions, typeDec.GetLocation(), validatorType.Name); + continue; + } + + var modelsValidatorTypeValidates = new List(modelTypes.Count); + + foreach (var modelType in modelTypes) + { + if (modelType.Kind == SymbolKind.ErrorType) + { + // the compiler will report this error for us + continue; + } + else + { + // keep track of the models we look at, to detect loops + _ = _visitedModelTypes.Add(modelType.WithNullableAnnotation(NullableAnnotation.None)); + } + + if (AlreadyImplementsValidateMethod(validatorType, modelType)) + { + // this type already implements a validation function, we can't auto-generate a new one + Diag(DiagDescriptors.AlreadyImplementsValidateMethod, typeDec.GetLocation(), validatorType.Name); + continue; + } + + var membersToValidate = GetMembersToValidate(modelType, true); + if (membersToValidate.Count == 0) + { + // this type lacks any eligible members + Diag(DiagDescriptors.NoEligibleMembersFromValidator, typeDec.GetLocation(), modelType.ToString(), validatorType.ToString()); + continue; + } + + modelsValidatorTypeValidates.Add(new ValidatedModel( + GetFQN(modelType), + modelType.Name, + ModelSelfValidates(modelType), + membersToValidate)); + } + + string keyword = GetTypeKeyword(validatorType); + + // following code establishes the containment hierarchy for the generated type in terms of nested types + + var parents = new List(); + var parent = typeDec.Parent as TypeDeclarationSyntax; + + while (parent != null && IsAllowedKind(parent.Kind())) + { + parents.Add($"partial {GetTypeKeyword(parent)} {parent.Identifier}{parent.TypeParameterList} {parent.ConstraintClauses}"); + parent = parent.Parent as TypeDeclarationSyntax; + } + + parents.Reverse(); + + results.Add(new ValidatorType( + validatorType.ContainingNamespace.IsGlobalNamespace ? string.Empty : validatorType.ContainingNamespace.ToString(), + GetMinimalFQN(validatorType), + GetMinimalFQNWithoutGenerics(validatorType), + keyword, + parents, + false, + modelsValidatorTypeValidates)); + } + } + } + + results.AddRange(_synthesizedValidators.Values); + _synthesizedValidators.Clear(); + + return results; + } + + private static bool IsAllowedKind(SyntaxKind kind) => + kind == SyntaxKind.ClassDeclaration || + kind == SyntaxKind.StructDeclaration || +#if ROSLYN_4_0_OR_GREATER + kind == SyntaxKind.RecordStructDeclaration || +#endif + kind == SyntaxKind.RecordDeclaration; + + private static string GetTypeKeyword(ITypeSymbol type) + { +#if ROSLYN_4_0_OR_GREATER + if (type.IsReferenceType) + { + return type.IsRecord ? "record class" : "class"; + } + + return type.IsRecord ? "record struct" : "struct"; +#else + return type.IsReferenceType ? "class" : "struct"; +#endif + } + + private static string GetTypeKeyword(TypeDeclarationSyntax type) => + type.Kind() switch + { + SyntaxKind.ClassDeclaration => "class", + SyntaxKind.RecordDeclaration => "record class", +#if ROSLYN_4_0_OR_GREATER + SyntaxKind.RecordStructDeclaration => "record struct", +#endif + _ => type.Keyword.ValueText, + }; + + private static string GetFQN(ISymbol type) + => type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier)); + + private static string GetMinimalFQN(ISymbol type) + => type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat.AddGenericsOptions(SymbolDisplayGenericsOptions.IncludeTypeParameters)); + + private static string GetMinimalFQNWithoutGenerics(ISymbol type) + => type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat.WithGenericsOptions(SymbolDisplayGenericsOptions.None)); + + /// + /// Checks whether the given validator already implement the IValidationOptions>T< interface. + /// + private static bool AlreadyImplementsValidateMethod(INamespaceOrTypeSymbol validatorType, ISymbol modelType) + => validatorType + .GetMembers("Validate") + .Where(m => m.Kind == SymbolKind.Method) + .Select(m => (IMethodSymbol)m) + .Any(m => m.Parameters.Length == NumValidationMethodArgs + && m.Parameters[0].Type.SpecialType == SpecialType.System_String + && SymbolEqualityComparer.Default.Equals(m.Parameters[1].Type, modelType)); + + /// + /// Checks whether the given type contain any unbound generic type arguments. + /// + private static bool HasOpenGenerics(ITypeSymbol type, out string genericType) + { + if (type is INamedTypeSymbol mt) + { + if (mt.IsGenericType) + { + foreach (var ta in mt.TypeArguments) + { + if (ta.TypeKind == TypeKind.TypeParameter) + { + genericType = ta.Name; + return true; + } + } + } + } + else if (type is ITypeParameterSymbol) + { + genericType = type.Name; + return true; + } + else if (type is IArrayTypeSymbol ats) + { + return HasOpenGenerics(ats.ElementType, out genericType); + } + + genericType = string.Empty; + return false; + } + +#if !ROSLYN_4_0_OR_GREATER + private bool IsAnnotated(ISymbol type) + { + foreach (var attribute in type.GetAttributes().Where(a => a.AttributeClass != null)) + { + var attributeType = attribute.AttributeClass!; + + if (SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.OptionsValidatorSymbol)) + { + return true; + } + } + + return false; + } +#endif + + private ITypeSymbol? GetEnumeratedType(ITypeSymbol type) + { + if (type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + // extract the T from a Nullable + type = ((INamedTypeSymbol)type).TypeArguments[0]; + } + + foreach (var implementingInterface in type.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(implementingInterface.OriginalDefinition, _compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T))) + { + return implementingInterface.TypeArguments.First(); + } + } + + return null; + } + + private List GetMembersToValidate(ITypeSymbol modelType, bool speculate) + { + // make a list of the most derived members in the model type + + if (modelType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + // extract the T from a Nullable + modelType = ((INamedTypeSymbol)modelType).TypeArguments[0]; + } + + var members = modelType.GetMembers().ToList(); + var addedMembers = new HashSet(members.Select(m => m.Name)); + var baseType = modelType.BaseType; + while (baseType is not null && baseType.SpecialType != SpecialType.System_Object) + { + var baseMembers = baseType.GetMembers().Where(m => !addedMembers.Contains(m.Name)); + members.AddRange(baseMembers); + addedMembers.UnionWith(baseMembers.Select(m => m.Name)); + baseType = baseType.BaseType; + } + + var membersToValidate = new List(); + foreach (var member in members) + { + var memberInfo = GetMemberInfo(member, speculate); + if (memberInfo != null) + { + if (member.DeclaredAccessibility != Accessibility.Public && member.DeclaredAccessibility != Accessibility.Internal) + { + Diag(DiagDescriptors.MemberIsInaccessible, member.Locations.First(), member.Name); + continue; + } + + membersToValidate.Add(memberInfo); + } + } + + return membersToValidate; + } + + private ValidatedMember? GetMemberInfo(ISymbol member, bool speculate) + { + ITypeSymbol memberType; + switch (member) + { + case IPropertySymbol prop: + memberType = prop.Type; + break; + case IFieldSymbol field: + if (field.AssociatedSymbol != null) + { + // a backing field for a property, don't need those + return null; + } + + memberType = field.Type; + break; + default: + // we only care about properties and fields + return null; + } + + var validationAttrs = new List(); + string? transValidatorTypeName = null; + string? enumerationValidatorTypeName = null; + var enumeratedIsNullable = false; + var enumeratedIsValueType = false; + var enumeratedMayBeNull = false; + var transValidatorIsSynthetic = false; + var enumerationValidatorIsSynthetic = false; + + foreach (var attribute in member.GetAttributes().Where(a => a.AttributeClass != null)) + { + var attributeType = attribute.AttributeClass!; + var attrLoc = attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation(); + + if (SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.ValidateObjectMembersAttributeSymbol) + || SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.LegacyValidateTransitivelyAttributeSymbol)) + { + if (HasOpenGenerics(memberType, out var genericType)) + { + Diag(DiagDescriptors.CantUseWithGenericTypes, attrLoc, genericType); +#pragma warning disable S1226 // Method parameters, caught exceptions and foreach variables' initial values should not be ignored + speculate = false; +#pragma warning restore S1226 // Method parameters, caught exceptions and foreach variables' initial values should not be ignored + continue; + } + + if (attribute.ConstructorArguments.Length == 1) + { + var transValidatorType = attribute.ConstructorArguments[0].Value as INamedTypeSymbol; + if (transValidatorType != null) + { + if (CanValidate(transValidatorType, memberType)) + { + if (transValidatorType.Constructors.Where(c => !c.Parameters.Any()).Any()) + { + transValidatorTypeName = transValidatorType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + else + { + Diag(DiagDescriptors.ValidatorsNeedSimpleConstructor, attrLoc, transValidatorType.Name); + } + } + else + { + Diag(DiagDescriptors.DoesntImplementIValidateOptions, attrLoc, transValidatorType.Name, memberType.Name); + } + } + else + { + Diag(DiagDescriptors.NullValidatorType, attrLoc); + } + } + else if (!_visitedModelTypes.Add(memberType.WithNullableAnnotation(NullableAnnotation.None))) + { + Diag(DiagDescriptors.CircularTypeReferences, attrLoc, memberType.ToString()); + speculate = false; + continue; + } + + if (transValidatorTypeName == null) + { + transValidatorIsSynthetic = true; + transValidatorTypeName = AddSynthesizedValidator(memberType, member); + } + + // pop the stack + _ = _visitedModelTypes.Remove(memberType.WithNullableAnnotation(NullableAnnotation.None)); + } + else if (SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.ValidateEnumeratedItemsAttributeSymbol)) + { + var enumeratedType = GetEnumeratedType(memberType); + if (enumeratedType == null) + { + Diag(DiagDescriptors.NotEnumerableType, attrLoc, memberType); + speculate = false; + continue; + } + + enumeratedIsNullable = enumeratedType.IsReferenceType || enumeratedType.NullableAnnotation == NullableAnnotation.Annotated; + enumeratedIsValueType = enumeratedType.IsValueType; + enumeratedMayBeNull = enumeratedType.NullableAnnotation == NullableAnnotation.Annotated; + + if (HasOpenGenerics(enumeratedType, out var genericType)) + { + Diag(DiagDescriptors.CantUseWithGenericTypes, attrLoc, genericType); + speculate = false; + continue; + } + + if (attribute.ConstructorArguments.Length == 1) + { + var enumerationValidatorType = attribute.ConstructorArguments[0].Value as INamedTypeSymbol; + if (enumerationValidatorType != null) + { + if (CanValidate(enumerationValidatorType, enumeratedType)) + { + if (enumerationValidatorType.Constructors.Where(c => c.Parameters.Length == 0).Any()) + { + enumerationValidatorTypeName = enumerationValidatorType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + else + { + Diag(DiagDescriptors.ValidatorsNeedSimpleConstructor, attrLoc, enumerationValidatorType.Name); + } + } + else + { + Diag(DiagDescriptors.DoesntImplementIValidateOptions, attrLoc, enumerationValidatorType.Name, enumeratedType.Name); + } + } + else + { + Diag(DiagDescriptors.NullValidatorType, attrLoc); + } + } + else if (!_visitedModelTypes.Add(enumeratedType.WithNullableAnnotation(NullableAnnotation.None))) + { + Diag(DiagDescriptors.CircularTypeReferences, attrLoc, enumeratedType.ToString()); + speculate = false; + continue; + } + + if (enumerationValidatorTypeName == null) + { + enumerationValidatorIsSynthetic = true; + enumerationValidatorTypeName = AddSynthesizedValidator(enumeratedType, member); + } + + // pop the stack + _ = _visitedModelTypes.Remove(enumeratedType.WithNullableAnnotation(NullableAnnotation.None)); + } + else if (DerivesFrom(attributeType, _symbolHolder.ValidationAttributeSymbol)) + { + var validationAttr = new ValidationAttributeInfo(attributeType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + validationAttrs.Add(validationAttr); + + foreach (var constructorArgument in attribute.ConstructorArguments) + { + validationAttr.ConstructorArguments.Add(GetArgumentExpression(constructorArgument.Type!, constructorArgument.Value)); + } + + foreach (var namedArgument in attribute.NamedArguments) + { + validationAttr.Properties.Add(namedArgument.Key, GetArgumentExpression(namedArgument.Value.Type!, namedArgument.Value.Value)); + } + } + } + + // generate a warning if the field/property seems like it should be transitively validated + if (transValidatorTypeName == null && speculate && memberType.SpecialType == SpecialType.None) + { + if (!HasOpenGenerics(memberType, out var genericType)) + { + var membersToValidate = GetMembersToValidate(memberType, false); + if (membersToValidate.Count > 0) + { + Diag(DiagDescriptors.PotentiallyMissingTransitiveValidation, member.GetLocation(), memberType.Name, member.Name); + } + } + } + + // generate a warning if the field/property seems like it should be enumerated + if (enumerationValidatorTypeName == null && speculate) + { + var enumeratedType = GetEnumeratedType(memberType); + if (enumeratedType != null) + { + if (!HasOpenGenerics(enumeratedType, out var genericType)) + { + var membersToValidate = GetMembersToValidate(enumeratedType, false); + if (membersToValidate.Count > 0) + { + Diag(DiagDescriptors.PotentiallyMissingEnumerableValidation, member.GetLocation(), enumeratedType.Name, member.Name); + } + } + } + } + + if (validationAttrs.Count > 0 || transValidatorTypeName != null || enumerationValidatorTypeName != null) + { + return new( + member.Name, + validationAttrs, + transValidatorTypeName, + transValidatorIsSynthetic, + enumerationValidatorTypeName, + enumerationValidatorIsSynthetic, + memberType.IsReferenceType || memberType.NullableAnnotation == NullableAnnotation.Annotated, + memberType.IsValueType, + enumeratedIsNullable, + enumeratedIsValueType, + enumeratedMayBeNull); + } + + return null; + } + + private string? AddSynthesizedValidator(ITypeSymbol modelType, ISymbol member) + { + var mt = modelType.WithNullableAnnotation(NullableAnnotation.None); + if (mt.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + // extract the T from a Nullable + mt = ((INamedTypeSymbol)mt).TypeArguments[0]; + } + + if (_synthesizedValidators.TryGetValue(mt, out var validator)) + { + return "global::" + validator.Namespace + "." + validator.Name; + } + + var membersToValidate = GetMembersToValidate(mt, true); + if (membersToValidate.Count == 0) + { + // this type lacks any eligible members + Diag(DiagDescriptors.NoEligibleMember, member.GetLocation(), mt.ToString(), member.ToString()); + return null; + } + + var model = new ValidatedModel( + GetFQN(mt), + mt.Name, + false, + membersToValidate); + + var validatorTypeName = "__" + mt.Name + "Validator__"; + + var result = new ValidatorType( + mt.ContainingNamespace.IsGlobalNamespace ? string.Empty : mt.ContainingNamespace.ToString(), + validatorTypeName, + validatorTypeName, + "class", + new List(), + true, + new[] { model }); + + _synthesizedValidators[mt] = result; + return "global::" + (result.Namespace.Length > 0 ? result.Namespace + "." + result.Name : result.Name); + } + + private bool DerivesFrom(ITypeSymbol source, ITypeSymbol dest) + { + var conversion = _compilation.ClassifyConversion(source, dest); + return conversion.IsReference && conversion.IsImplicit; + } + + private bool ModelSelfValidates(ITypeSymbol modelType) + { + foreach (var implementingInterface in modelType.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(implementingInterface.OriginalDefinition, _symbolHolder.IValidatableObjectSymbol)) + { + return true; + } + } + + return false; + } + + private List GetModelTypes(ITypeSymbol validatorType) + { + var result = new List(); + foreach (var implementingInterface in validatorType.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(implementingInterface.OriginalDefinition, _symbolHolder.ValidateOptionsSymbol)) + { + result.Add(implementingInterface.TypeArguments.First()); + } + } + + return result; + } + + private bool CanValidate(ITypeSymbol validatorType, ISymbol modelType) + { + foreach (var implementingInterface in validatorType.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(implementingInterface.OriginalDefinition, _symbolHolder.ValidateOptionsSymbol)) + { + var t = implementingInterface.TypeArguments.First(); + if (SymbolEqualityComparer.Default.Equals(modelType, t)) + { + return true; + } + } + } + + return false; + } + + private string GetArgumentExpression(ITypeSymbol type, object? value) + { + if (value == null) + { + return "null"; + } + + if (type.SpecialType == SpecialType.System_Boolean) + { + return (bool)value ? "true" : "false"; + } + + if (SymbolEqualityComparer.Default.Equals(type, _symbolHolder.TypeSymbol) && + value is INamedTypeSymbol sym) + { + return $"typeof({sym.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)})"; + } + + if (type.SpecialType == SpecialType.System_String) + { + return $@"""{EscapeString(value.ToString())}"""; + } + + if (type.SpecialType == SpecialType.System_Char) + { + return $@"'{EscapeString(value.ToString())}'"; + } + + return $"({type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}){Convert.ToString(value, CultureInfo.InvariantCulture)}"; + } + + private static readonly char[] _specialChars = { '\n', '\r', '"', '\\' }; + + private static string EscapeString(string s) + { + int index = s.IndexOfAny(_specialChars); + if (index < 0) + { + return s; + } + + var sb = new StringBuilder(s.Length); + _ = sb.Append(s, 0, index); + + while (index < s.Length) + { + _ = s[index] switch + { + '\n' => sb.Append("\\n"), + '\r' => sb.Append("\\r"), + '"' => sb.Append("\\\""), + '\\' => sb.Append("\\\\"), + var other => sb.Append(other), + }; + + index++; + } + + return sb.ToString(); + } + + private void Diag(DiagnosticDescriptor desc, Location? location) + { + _reportDiagnostic(Diagnostic.Create(desc, location, Array.Empty())); + } + + private void Diag(DiagnosticDescriptor desc, Location? location, params object?[]? messageArgs) + { + _reportDiagnostic(Diagnostic.Create(desc, location, messageArgs)); + } +} diff --git a/src/Generators/Microsoft.Gen.OptionsValidation/Common/Resources.Designer.cs b/src/Generators/Microsoft.Gen.OptionsValidation/Common/Resources.Designer.cs new file mode 100644 index 0000000000..0ec5247cbb --- /dev/null +++ b/src/Generators/Microsoft.Gen.OptionsValidation/Common/Resources.Designer.cs @@ -0,0 +1,297 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Gen.OptionsValidation { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Gen.OptionsValidation.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Type {0} already implements the Validate method. + /// + internal static string AlreadyImplementsValidateMethodMessage { + get { + return ResourceManager.GetString("AlreadyImplementsValidateMethodMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A type already includes an implementation of the `Validate` method. + /// + internal static string AlreadyImplementsValidateMethodTitle { + get { + return ResourceManager.GetString("AlreadyImplementsValidateMethodTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [OptionsValidator] cannot be applied to static class {0}. + /// + internal static string CantBeStaticClassMessage { + get { + return ResourceManager.GetString("CantBeStaticClassMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `OptionsValidatorAttribute` can't be applied to a static class. + /// + internal static string CantBeStaticClassTitle { + get { + return ResourceManager.GetString("CantBeStaticClassTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can't use [ValidateObjectMembers] or [ValidateEnumeratedItems] on fields or properties with open generic type {0}. + /// + internal static string CantUseWithGenericTypesMessage { + get { + return ResourceManager.GetString("CantUseWithGenericTypesMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can't use `ValidateObjectMembersAttribute` or `ValidateEnumeratedItemsAttribute` on fields or properties with open generic types. + /// + internal static string CantUseWithGenericTypesTitle { + get { + return ResourceManager.GetString("CantUseWithGenericTypesTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to There is a circular type reference involving type {0} preventing it from being used for static validation. + /// + internal static string CircularTypeReferencesMessage { + get { + return ResourceManager.GetString("CircularTypeReferencesMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unsupported circular references in model types. + /// + internal static string CircularTypeReferencesTitle { + get { + return ResourceManager.GetString("CircularTypeReferencesTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type {0} does not implement the required IValidateOptions<{1}> interface. + /// + internal static string DoesntImplementIValidateOptionsMessage { + get { + return ResourceManager.GetString("DoesntImplementIValidateOptionsMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A type annotated with `OptionsValidatorAttribute` doesn't implement the necessary interface. + /// + internal static string DoesntImplementIValidateOptionsTitle { + get { + return ResourceManager.GetString("DoesntImplementIValidateOptionsTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can't apply validation attributes to private field or property {0}. + /// + internal static string MemberIsInaccessibleMessage { + get { + return ResourceManager.GetString("MemberIsInaccessibleMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can't validate private fields or properties. + /// + internal static string MemberIsInaccessibleTitle { + get { + return ResourceManager.GetString("MemberIsInaccessibleTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type {0} has no fields or properties to validate, referenced from member {1}. + /// + internal static string NoEligibleMemberMessage { + get { + return ResourceManager.GetString("NoEligibleMemberMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type {0} has no fields or properties to validate, referenced by type {1}. + /// + internal static string NoEligibleMembersFromValidatorMessage { + get { + return ResourceManager.GetString("NoEligibleMembersFromValidatorMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A type has no fields or properties to validate. + /// + internal static string NoEligibleMembersFromValidatorTitle { + get { + return ResourceManager.GetString("NoEligibleMembersFromValidatorTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A member type has no fields or properties to validate. + /// + internal static string NoEligibleMemberTitle { + get { + return ResourceManager.GetString("NoEligibleMemberTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [ValidateEnumeratedItems] cannot be used on members of type {0} as it doesn't implement IEnumerable<T>. + /// + internal static string NotEnumerableTypeMessage { + get { + return ResourceManager.GetString("NotEnumerableTypeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Member type is not enumerable. + /// + internal static string NotEnumerableTypeTitle { + get { + return ResourceManager.GetString("NotEnumerableTypeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Null validator type specified in [ValidateObjectMembers] or [ValidateEnumeratedItems] attributes. + /// + internal static string NullValidatorTypeMessage { + get { + return ResourceManager.GetString("NullValidatorTypeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Null validator type specified for the `ValidateObjectMembersAttribute` or `ValidateEnumeratedItemsAttribute` attributes. + /// + internal static string NullValidatorTypeTitle { + get { + return ResourceManager.GetString("NullValidatorTypeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type {0} has validation annotations, but member {1} doesn't specify [ValidateEnumeratedItems] which could be an oversight. + /// + internal static string PotentiallyMissingEnumerableValidationMessage { + get { + return ResourceManager.GetString("PotentiallyMissingEnumerableValidationMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Member potentially missing enumerable validation. + /// + internal static string PotentiallyMissingEnumerableValidationTitle { + get { + return ResourceManager.GetString("PotentiallyMissingEnumerableValidationTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type {0} has validation annotations, but member {1} doesn't specify [ValidateObjectMembers] which could be an oversight. + /// + internal static string PotentiallyMissingTransitiveValidationMessage { + get { + return ResourceManager.GetString("PotentiallyMissingTransitiveValidationMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Member potentially missing transitive validation. + /// + internal static string PotentiallyMissingTransitiveValidationTitle { + get { + return ResourceManager.GetString("PotentiallyMissingTransitiveValidationTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Validator type {0} doesn't have a parameterless constructor. + /// + internal static string ValidatorsNeedSimpleConstructorMessage { + get { + return ResourceManager.GetString("ValidatorsNeedSimpleConstructorMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Validators used for transitive or enumerable validation must have a constructor with no parameters. + /// + internal static string ValidatorsNeedSimpleConstructorTitle { + get { + return ResourceManager.GetString("ValidatorsNeedSimpleConstructorTitle", resourceCulture); + } + } + } +} diff --git a/src/Generators/Microsoft.Gen.OptionsValidation/Common/Resources.resx b/src/Generators/Microsoft.Gen.OptionsValidation/Common/Resources.resx new file mode 100644 index 0000000000..d9bae1523a --- /dev/null +++ b/src/Generators/Microsoft.Gen.OptionsValidation/Common/Resources.resx @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Type {0} already implements the Validate method + + + A type already includes an implementation of the `Validate` method + + + [OptionsValidator] cannot be applied to static class {0} + + + `OptionsValidatorAttribute` can't be applied to a static class + + + Can't use [ValidateObjectMembers] or [ValidateEnumeratedItems] on fields or properties with open generic type {0} + + + Can't use `ValidateObjectMembersAttribute` or `ValidateEnumeratedItemsAttribute` on fields or properties with open generic types + + + There is a circular type reference involving type {0} preventing it from being used for static validation + + + Unsupported circular references in model types + + + Type {0} does not implement the required IValidateOptions<{1}> interface + + + A type annotated with `OptionsValidatorAttribute` doesn't implement the necessary interface + + + Can't apply validation attributes to private field or property {0} + + + Can't validate private fields or properties + + + Type {0} has no fields or properties to validate, referenced from member {1} + + + Type {0} has no fields or properties to validate, referenced by type {1} + + + A type has no fields or properties to validate + + + A member type has no fields or properties to validate + + + [ValidateEnumeratedItems] cannot be used on members of type {0} as it doesn't implement IEnumerable<T> + + + Member type is not enumerable + + + Null validator type specified in [ValidateObjectMembers] or [ValidateEnumeratedItems] attributes + + + Null validator type specified for the `ValidateObjectMembersAttribute` or `ValidateEnumeratedItemsAttribute` attributes + + + Type {0} has validation annotations, but member {1} doesn't specify [ValidateEnumeratedItems] which could be an oversight + + + Member potentially missing enumerable validation + + + Type {0} has validation annotations, but member {1} doesn't specify [ValidateObjectMembers] which could be an oversight + + + Member potentially missing transitive validation + + + Validator type {0} doesn't have a parameterless constructor + + + Validators used for transitive or enumerable validation must have a constructor with no parameters + + diff --git a/src/Generators/Microsoft.Gen.OptionsValidation/Common/SymbolHolder.cs b/src/Generators/Microsoft.Gen.OptionsValidation/Common/SymbolHolder.cs new file mode 100644 index 0000000000..02d8deb9cd --- /dev/null +++ b/src/Generators/Microsoft.Gen.OptionsValidation/Common/SymbolHolder.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Gen.OptionsValidation; + +/// +/// Holds required symbols for the . +/// +internal sealed record class SymbolHolder( + INamedTypeSymbol OptionsValidatorSymbol, + INamedTypeSymbol ValidationAttributeSymbol, + INamedTypeSymbol DataTypeAttributeSymbol, + INamedTypeSymbol ValidateOptionsSymbol, + INamedTypeSymbol IValidatableObjectSymbol, + INamedTypeSymbol TypeSymbol, + INamedTypeSymbol? LegacyValidateTransitivelyAttributeSymbol, + INamedTypeSymbol? ValidateObjectMembersAttributeSymbol, + INamedTypeSymbol? ValidateEnumeratedItemsAttributeSymbol); diff --git a/src/Generators/Microsoft.Gen.OptionsValidation/Common/SymbolLoader.cs b/src/Generators/Microsoft.Gen.OptionsValidation/Common/SymbolLoader.cs new file mode 100644 index 0000000000..8b365c7f68 --- /dev/null +++ b/src/Generators/Microsoft.Gen.OptionsValidation/Common/SymbolLoader.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Gen.OptionsValidation; + +internal static class SymbolLoader +{ + public const string OptionsValidatorAttribute = "Microsoft.Extensions.Options.Validation.OptionsValidatorAttribute"; + internal const string ValidationAttribute = "System.ComponentModel.DataAnnotations.ValidationAttribute"; + internal const string DataTypeAttribute = "System.ComponentModel.DataAnnotations.DataTypeAttribute"; + internal const string IValidatableObjectType = "System.ComponentModel.DataAnnotations.IValidatableObject"; + internal const string IValidateOptionsType = "Microsoft.Extensions.Options.IValidateOptions`1"; + internal const string TypeOfType = "System.Type"; + internal const string LegacyValidateTransitivelyAttribute = "Microsoft.Extensions.Data.Validation.ValidateTransitivelyObjectMembersAttribute"; + internal const string ValidateObjectMembersAttribute = "Microsoft.Extensions.Options.Validation.ValidateObjectMembersAttribute"; + internal const string ValidateEnumeratedItemsAttribute = "Microsoft.Extensions.Options.Validation.ValidateEnumeratedItemsAttribute"; + + public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHolder) + { + INamedTypeSymbol? GetSymbol(string metadataName, bool optional = false) + { + var symbol = compilation.GetTypeByMetadataName(metadataName); + if (symbol == null && !optional) + { + return null; + } + + return symbol; + } + + // required + var optionsValidatorSymbol = GetSymbol(OptionsValidatorAttribute); + var validationAttributeSymbol = GetSymbol(ValidationAttribute); + var dataTypeAttributeSymbol = GetSymbol(DataTypeAttribute); + var ivalidatableObjectSymbol = GetSymbol(IValidatableObjectType); + var validateOptionsSymbol = GetSymbol(IValidateOptionsType); + var typeSymbol = GetSymbol(TypeOfType); + +#pragma warning disable S1067 // Expressions should not be too complex + if (optionsValidatorSymbol == null || + validationAttributeSymbol == null || + dataTypeAttributeSymbol == null || + ivalidatableObjectSymbol == null || + validateOptionsSymbol == null || + typeSymbol == null) + { + symbolHolder = default; + return false; + } +#pragma warning restore S1067 // Expressions should not be too complex + + symbolHolder = new( + optionsValidatorSymbol, + validationAttributeSymbol, + dataTypeAttributeSymbol, + validateOptionsSymbol, + ivalidatableObjectSymbol, + typeSymbol, + + // optional + GetSymbol(LegacyValidateTransitivelyAttribute, optional: true), + GetSymbol(ValidateObjectMembersAttribute, optional: true), + GetSymbol(ValidateEnumeratedItemsAttribute, optional: true)); + + return true; + } +} diff --git a/src/Generators/Microsoft.Gen.OptionsValidation/Directory.Build.props b/src/Generators/Microsoft.Gen.OptionsValidation/Directory.Build.props new file mode 100644 index 0000000000..882655d516 --- /dev/null +++ b/src/Generators/Microsoft.Gen.OptionsValidation/Directory.Build.props @@ -0,0 +1,33 @@ + + + + + Microsoft.Gen.OptionsValidation + Code generator to support Microsoft.Extensions.Options.Validation. + Fundamentals + + + + cs + true + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.OptionsValidation/Roslyn3.8/Microsoft.Gen.OptionsValidation.Roslyn3.8.csproj b/src/Generators/Microsoft.Gen.OptionsValidation/Roslyn3.8/Microsoft.Gen.OptionsValidation.Roslyn3.8.csproj new file mode 100644 index 0000000000..e1ef356a7d --- /dev/null +++ b/src/Generators/Microsoft.Gen.OptionsValidation/Roslyn3.8/Microsoft.Gen.OptionsValidation.Roslyn3.8.csproj @@ -0,0 +1,28 @@ + + + Microsoft.Gen.OptionsValidation + 3.8 + $(MicrosoftCodeAnalysisVersion_3_8) + + + + normal + 94 + 85 + 85 + + + + + True + True + Resources.resx + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.OptionsValidation/Roslyn4.0/Microsoft.Gen.OptionsValidation.Roslyn4.0.csproj b/src/Generators/Microsoft.Gen.OptionsValidation/Roslyn4.0/Microsoft.Gen.OptionsValidation.Roslyn4.0.csproj new file mode 100644 index 0000000000..69d0da6a10 --- /dev/null +++ b/src/Generators/Microsoft.Gen.OptionsValidation/Roslyn4.0/Microsoft.Gen.OptionsValidation.Roslyn4.0.csproj @@ -0,0 +1,29 @@ + + + Microsoft.Gen.OptionsValidation + 4.0 + $(MicrosoftCodeAnalysisVersion_4_0) + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + + + normal + 94 + 85 + 50 + + + + + True + True + Resources.resx + + + + + + + + + diff --git a/src/Generators/Shared/ClassDeclarationSyntaxReceiver.cs b/src/Generators/Shared/ClassDeclarationSyntaxReceiver.cs new file mode 100644 index 0000000000..04d5da3b9c --- /dev/null +++ b/src/Generators/Shared/ClassDeclarationSyntaxReceiver.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +#pragma warning disable CA1716 +namespace Microsoft.Gen.Shared; +#pragma warning restore CA1716 + +/// +/// Class declaration syntax receiver for generators. +/// +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif +internal sealed class ClassDeclarationSyntaxReceiver : ISyntaxReceiver +{ + internal static ISyntaxReceiver Create() => new ClassDeclarationSyntaxReceiver(); + + /// + /// Gets class declaration syntax holders after visiting nodes. + /// + public ICollection ClassDeclarations { get; } = new List(); + + /// + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is ClassDeclarationSyntax classSyntax) + { + ClassDeclarations.Add(classSyntax); + } + } +} diff --git a/src/Generators/Shared/DiagDescriptorsBase.cs b/src/Generators/Shared/DiagDescriptorsBase.cs new file mode 100644 index 0000000000..7a71c871e5 --- /dev/null +++ b/src/Generators/Shared/DiagDescriptorsBase.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.CodeAnalysis; + +#pragma warning disable CA1716 +namespace Microsoft.Gen.Shared; +#pragma warning restore CA1716 + +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif +internal class DiagDescriptorsBase +{ +#pragma warning disable S1075 // URIs should not be hardcoded + public const string HelpLinkBase = "https://eng.ms/docs/experiences-devices/r9-sdk/docs/code-generation/generators/"; +#pragma warning restore S1075 // URIs should not be hardcoded + + protected static DiagnosticDescriptor Make( + string id, + string title, + string messageFormat, + string category, + DiagnosticSeverity defaultSeverity = DiagnosticSeverity.Error, + bool isEnabledByDefault = true) + { + return new( + id, + title, + messageFormat, + category, + defaultSeverity, + isEnabledByDefault, + null, +#pragma warning disable CA1308 // Normalize strings to uppercase + HelpLinkBase + id.ToLowerInvariant(), +#pragma warning restore CA1308 // Normalize strings to uppercase + Array.Empty()); + } +} diff --git a/src/Generators/Shared/EmitterBase.cs b/src/Generators/Shared/EmitterBase.cs new file mode 100644 index 0000000000..c220026ad6 --- /dev/null +++ b/src/Generators/Shared/EmitterBase.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text; + +#pragma warning disable CA1716 +namespace Microsoft.Gen.Shared; +#pragma warning restore CA1716 + +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif +internal class EmitterBase +{ + private const int DefaultStringBuilderCapacity = 1024; + private const int IndentChars = 4; + + private readonly StringBuilder _sb = new(DefaultStringBuilderCapacity); + private readonly string[] _padding = new string[16]; + private int _indent; + + public EmitterBase(bool emitPreamble = true) + { + var padding = _padding; + for (int i = 0; i < padding.Length; i++) + { + padding[i] = new string(' ', i * IndentChars); + } + + if (emitPreamble) + { + Out(GeneratorUtilities.FilePreamble); + } + } + + protected void OutOpenBrace() + { + OutLn("{"); + Indent(); + } + + protected void OutCloseBrace() + { + Unindent(); + OutLn("}"); + } + + protected void OutCloseBraceWithExtra(string extra) + { + Unindent(); + OutIndent(); + Out("}"); + Out(extra); + OutLn(); + } + + protected void OutIndent() + { + _ = _sb.Append(_padding[_indent]); + } + + protected string GetPaddingString(byte indent) + { + return _padding[indent]; + } + + protected void OutLn() + { + _ = _sb.AppendLine(); + } + + protected void OutLn(string line) + { + OutIndent(); + _ = _sb.AppendLine(line); + } + + protected void OutPP(string line) + { + _ = _sb.AppendLine(line); + } + + protected void OutEnumeration(IEnumerable e) + { + bool first = true; + foreach (var item in e) + { + if (!first) + { + Out(", "); + } + + Out(item); + first = false; + } + } + + protected void Out(string text) => _ = _sb.Append(text); + protected void Out(char ch) => _ = _sb.Append(ch); + protected void Indent() => _indent++; + protected void Unindent() => _indent--; + protected void OutGeneratedCodeAttribute() => OutLn($"[{GeneratorUtilities.GeneratedCodeAttribute}]"); + protected string Capture() => _sb.ToString(); +} diff --git a/src/Generators/Shared/GeneratorUtilities.cs b/src/Generators/Shared/GeneratorUtilities.cs new file mode 100644 index 0000000000..56c865d055 --- /dev/null +++ b/src/Generators/Shared/GeneratorUtilities.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.CodeAnalysis; +#if ROSLYN_4_0_OR_GREATER +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +#endif + +[assembly: System.Resources.NeutralResourcesLanguage("en-us")] + +#pragma warning disable CA1716 +namespace Microsoft.Gen.Shared; +#pragma warning restore CA1716 + +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif +internal static class GeneratorUtilities +{ + public static string GeneratedCodeAttribute { get; } = $"global::System.CodeDom.Compiler.GeneratedCodeAttribute(" + + $"\"{typeof(GeneratorUtilities).Assembly.GetName().Name}\", " + + $"\"{typeof(GeneratorUtilities).Assembly.GetName().Version}\")"; + + public static string FilePreamble { get; } = @$" +// +#nullable enable +#pragma warning disable CS1591 // Compensate for https://github.com/dotnet/roslyn/issues/54103 +"; + +#if ROSLYN_4_0_OR_GREATER + + [ExcludeFromCodeCoverage] + public static void Initialize( + IncrementalGeneratorInitializationContext context, + HashSet fullyQualifiedAttributeNames, + Action, SourceProductionContext> process) => Initialize(context, fullyQualifiedAttributeNames, x => x, process); + + [ExcludeFromCodeCoverage] + public static void Initialize( + IncrementalGeneratorInitializationContext context, + HashSet fullyQualifiedAttributeNames, + Func transform, + Action, SourceProductionContext> process) + { + // strip the namespace prefix and the Attribute suffix + var shortAttributeNames = new HashSet(); + foreach (var n in fullyQualifiedAttributeNames) + { + var index = n.LastIndexOf('.') + 1; + _ = shortAttributeNames.Add(n.Substring(index, n.Length - index - "Attribute".Length)); + } + + var declarations = context.SyntaxProvider + .CreateSyntaxProvider( + (node, _) => Predicate(node, shortAttributeNames), + (gsc, ct) => Filter(gsc, fullyQualifiedAttributeNames, transform, ct)) + .Where(t => t is not null) + .Select((t, _) => t!); + + var compilationAndTypes = context.CompilationProvider.Combine(declarations.Collect()); + + context.RegisterSourceOutput(compilationAndTypes, (spc, source) => + { + var compilation = source.Left; + var nodes = source.Right; + + if (nodes.IsDefaultOrEmpty) + { + // nothing to do yet + return; + } + + process(compilation, nodes.Distinct(), spc); + }); + + static bool Predicate(SyntaxNode node, HashSet shortAttributeNames) + { + if (node.IsKind(SyntaxKind.Attribute)) + { + var attr = (AttributeSyntax)node; + + // see if we can trivially reject this node and avoid further work + if (attr.Name is IdentifierNameSyntax id) + { + return shortAttributeNames.Contains(id.Identifier.Text); + } + + // too complicated to check further, the filter will have to decide + return true; + } + + return false; + } + + static SyntaxNode? Filter(GeneratorSyntaxContext context, HashSet fullyQualifiedAttributeNames, Func transform, CancellationToken cancellationToken) + { + var attributeSyntax = (AttributeSyntax)context.Node; + + var ctor = context.SemanticModel.GetSymbolInfo(attributeSyntax, cancellationToken).Symbol as IMethodSymbol; + var attributeType = ctor?.ContainingType; + if (attributeType != null && fullyQualifiedAttributeNames.Contains(GetAttributeDisplayName(attributeType))) + { + var node = attributeSyntax.Parent?.Parent; + if (node != null) + { + return transform(node); + } + } + + return null; + } + + static string GetAttributeDisplayName(INamedTypeSymbol attributeType) + => attributeType.IsGenericType ? + attributeType.OriginalDefinition.ToDisplayString() : + attributeType.ToDisplayString(); + } +#endif + + /// + /// Reports will not be generated during design time to prevent file being written on every keystroke in VS. + /// Refererences: + /// 1. + /// Design-time build. + /// 2. + /// Reading MSBuild Properties in Source Generators. + /// + /// . + /// The name of the MSBuild property that determines whether to produce a report. + /// bool value to indicate if reports should be generated. + public static bool ShouldGenerateReport(GeneratorExecutionContext context, string msBuildProperty) + { + _ = context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(msBuildProperty, out var generateFiles); + + return string.Equals(generateFiles, bool.TrueString, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/src/Generators/Shared/ParserUtilities.cs b/src/Generators/Shared/ParserUtilities.cs new file mode 100644 index 0000000000..28c72ddaf1 --- /dev/null +++ b/src/Generators/Shared/ParserUtilities.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +#pragma warning disable CA1716 +namespace Microsoft.Gen.Shared; +#pragma warning restore CA1716 + +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif +internal static class ParserUtilities +{ + internal static AttributeData? GetSymbolAttributeAnnotationOrDefault(ISymbol? attribute, ISymbol symbol) + { + if (attribute is null) + { + return null; + } + + var attrs = symbol.GetAttributes(); + foreach (var item in attrs) + { + if (SymbolEqualityComparer.Default.Equals(attribute, item.AttributeClass) && item.AttributeConstructor != null) + { + return item; + } + } + + return null; + } + + internal static bool PropertyHasModifier(ISymbol property, SyntaxKind modifierToSearch, CancellationToken token) + => property + .DeclaringSyntaxReferences + .Any(x => + x.GetSyntax(token) is PropertyDeclarationSyntax syntax && + syntax.Modifiers.Any(m => m.IsKind(modifierToSearch))); + + internal static Location? GetLocation(this ISymbol symbol) + { + if (symbol is null) + { + return null; + } + + return symbol.Locations.IsDefaultOrEmpty + ? null + : symbol.Locations[0]; + } + + internal static bool IsBaseOrIdentity(ITypeSymbol source, ITypeSymbol dest, Compilation comp) + { + var conversion = comp.ClassifyConversion(source, dest); + return conversion.IsIdentity || (conversion.IsReference && conversion.IsImplicit); + } + + internal static bool ImplementsInterface(this ITypeSymbol type, ITypeSymbol interfaceType) + { + foreach (var iface in type.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(interfaceType, iface)) + { + return true; + } + } + + return false; + } + + // Check if parameter has either simplified (i.e. "int?") or explicit (Nullable) nullable type declaration: + internal static bool IsNullableOfT(this ITypeSymbol type) + => type.SpecialType == SpecialType.System_Nullable_T || type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T; +} diff --git a/src/Generators/Shared/StringBuilderPool.cs b/src/Generators/Shared/StringBuilderPool.cs new file mode 100644 index 0000000000..ce3f62b6a3 --- /dev/null +++ b/src/Generators/Shared/StringBuilderPool.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text; + +#pragma warning disable CA1716 +namespace Microsoft.Gen.Shared; +#pragma warning restore CA1716 + +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif +internal sealed class StringBuilderPool +{ + private readonly Stack _builders = new(); + + public StringBuilder GetStringBuilder() + { + const int DefaultStringBuilderCapacity = 1024; + + if (_builders.Count == 0) + { + return new StringBuilder(DefaultStringBuilderCapacity); + } + + var sb = _builders.Pop(); + _ = sb.Clear(); + return sb; + } + + public void ReturnStringBuilder(StringBuilder sb) + { + _builders.Push(sb); + } +} diff --git a/src/Generators/Shared/SymbolHelpers.cs b/src/Generators/Shared/SymbolHelpers.cs new file mode 100644 index 0000000000..d32bbd7c1f --- /dev/null +++ b/src/Generators/Shared/SymbolHelpers.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +#pragma warning disable CA1716 +namespace Microsoft.Gen.Shared; +#pragma warning restore CA1716 + +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif +internal static class SymbolHelpers +{ + public static string GetFullNamespace(ISymbol symbol) + { + return symbol.ContainingNamespace.IsGlobalNamespace ? string.Empty : symbol.ContainingNamespace.ToString(); + } +} diff --git a/src/Generators/Shared/TypeDeclarationSyntaxReceiver.cs b/src/Generators/Shared/TypeDeclarationSyntaxReceiver.cs new file mode 100644 index 0000000000..c3aadba6d4 --- /dev/null +++ b/src/Generators/Shared/TypeDeclarationSyntaxReceiver.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +#pragma warning disable CA1716 +namespace Microsoft.Gen.Shared; +#pragma warning restore CA1716 + +/// +/// Class/struct/record declaration syntax receiver for generators. +/// +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif +internal sealed class TypeDeclarationSyntaxReceiver : ISyntaxReceiver +{ + internal static ISyntaxReceiver Create() => new TypeDeclarationSyntaxReceiver(); + + /// + /// Gets class/struct/record declaration syntax holders after visiting nodes. + /// + public ICollection TypeDeclarations { get; } = new List(); + + /// + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is ClassDeclarationSyntax classSyntax) + { + TypeDeclarations.Add(classSyntax); + } + else if (syntaxNode is StructDeclarationSyntax structSyntax) + { + TypeDeclarations.Add(structSyntax); + } + else if (syntaxNode is RecordDeclarationSyntax recordSyntax) + { + TypeDeclarations.Add(recordSyntax); + } + else if (syntaxNode is InterfaceDeclarationSyntax interfaceSyntax) + { + TypeDeclarations.Add(interfaceSyntax); + } + } +} diff --git a/src/LegacySupport/BitOperations/BitOperations.cs b/src/LegacySupport/BitOperations/BitOperations.cs new file mode 100644 index 0000000000..aa3ba07fa0 --- /dev/null +++ b/src/LegacySupport/BitOperations/BitOperations.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable S109 // Magic numbers should not be used + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System.Numerics; + +[ExcludeFromCodeCoverage] +internal static class BitOperations +{ + // Summary: + // Rotates the specified value left by the specified number of bits. + // + // Parameters: + // value: + // The value to rotate. + // + // offset: + // The number of bits to rotate by. Any value outside the range [0..31] is treated + // as congruent mod 32. + // + // Returns: + // The rotated value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint RotateLeft(uint value, int offset) + => (value << offset) | (value >> (32 - offset)); + + // Summary: + // Rotates the specified value left by the specified number of bits. + // + // Parameters: + // value: + // The value to rotate. + // + // offset: + // The number of bits to rotate by. Any value outside the range [0..63] is treated + // as congruent mod 64. + // + // Returns: + // The rotated value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong RotateLeft(ulong value, int offset) + => (value << offset) | (value >> (64 - offset)); +} diff --git a/src/LegacySupport/BitOperations/README.md b/src/LegacySupport/BitOperations/README.md new file mode 100644 index 0000000000..469443e009 --- /dev/null +++ b/src/LegacySupport/BitOperations/README.md @@ -0,0 +1,7 @@ +To use this source in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/LegacySupport/CallerAttributes/CallerArgumentExpressionAttribute.cs b/src/LegacySupport/CallerAttributes/CallerArgumentExpressionAttribute.cs new file mode 100644 index 0000000000..364f9ef0c7 --- /dev/null +++ b/src/LegacySupport/CallerAttributes/CallerArgumentExpressionAttribute.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0079 +#pragma warning disable SA1101 +#pragma warning disable SA1512 + +using System.Diagnostics.CodeAnalysis; + +namespace System.Runtime.CompilerServices; + +/// +/// Tags parameter that should be filled with specific caller name. +/// +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class CallerArgumentExpressionAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// Function parameter to take the name from. + public CallerArgumentExpressionAttribute(string parameterName) + { + ParameterName = parameterName; + } + + /// + /// Gets name of the function parameter that name should be taken from. + /// + public string ParameterName { get; } +} diff --git a/src/LegacySupport/CallerAttributes/CallerFilePathAttribute.cs b/src/LegacySupport/CallerAttributes/CallerFilePathAttribute.cs new file mode 100644 index 0000000000..68377ab9ea --- /dev/null +++ b/src/LegacySupport/CallerAttributes/CallerFilePathAttribute.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0079 +#pragma warning disable SA1512 + +using System.Diagnostics.CodeAnalysis; + +namespace System.Runtime.CompilerServices; + +/// +/// Tags parameter that should be filled with specific caller source file path. +/// +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class CallerFilePathAttribute : Attribute +{ +} diff --git a/src/LegacySupport/CallerAttributes/CallerLineNumberAttribute.cs b/src/LegacySupport/CallerAttributes/CallerLineNumberAttribute.cs new file mode 100644 index 0000000000..21d85c1118 --- /dev/null +++ b/src/LegacySupport/CallerAttributes/CallerLineNumberAttribute.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0079 +#pragma warning disable SA1512 + +using System.Diagnostics.CodeAnalysis; + +namespace System.Runtime.CompilerServices; + +/// +/// Tags parameter that should be filled with specific caller line number. +/// +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class CallerLineNumberAttribute : Attribute +{ +} diff --git a/src/LegacySupport/CallerAttributes/CallerMemberNameAttribute.cs b/src/LegacySupport/CallerAttributes/CallerMemberNameAttribute.cs new file mode 100644 index 0000000000..96133be2c0 --- /dev/null +++ b/src/LegacySupport/CallerAttributes/CallerMemberNameAttribute.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0079 +#pragma warning disable SA1512 + +using System.Diagnostics.CodeAnalysis; + +namespace System.Runtime.CompilerServices; + +/// +/// Tags parameter that should be filled with specific caller member name. +/// +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class CallerMemberNameAttribute : Attribute +{ +} diff --git a/src/LegacySupport/CallerAttributes/README.md b/src/LegacySupport/CallerAttributes/README.md new file mode 100644 index 0000000000..2d4a94eda1 --- /dev/null +++ b/src/LegacySupport/CallerAttributes/README.md @@ -0,0 +1,7 @@ +To use this source in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/LegacySupport/DiagnosticAttributes/NullableAttributes.cs b/src/LegacySupport/DiagnosticAttributes/NullableAttributes.cs new file mode 100644 index 0000000000..bc2a5b684a --- /dev/null +++ b/src/LegacySupport/DiagnosticAttributes/NullableAttributes.cs @@ -0,0 +1,177 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0079 +#pragma warning disable IDE0079 +#pragma warning disable SA1101 +#pragma warning disable SA1116 +#pragma warning disable SA1117 +#pragma warning disable SA1402 +#pragma warning disable SA1512 +#pragma warning disable SA1623 +#pragma warning disable SA1642 +#pragma warning disable SA1623 +#pragma warning disable SA1642 +#pragma warning disable SA1649 +#pragma warning disable S3903 +#pragma warning disable IDE0021 // Use block body for constructors +#pragma warning disable CA1019 + +namespace System.Diagnostics.CodeAnalysis; + +#if !NETCOREAPP3_1_OR_GREATER +/// Specifies that null is allowed as an input even if the corresponding type disallows it. +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class AllowNullAttribute : Attribute +{ +} + +/// Specifies that null is disallowed as an input even if the corresponding type allows it. +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class DisallowNullAttribute : Attribute +{ +} + +/// Specifies that an output may be null even if the corresponding type disallows it. +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class MaybeNullAttribute : Attribute +{ +} + +/// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class NotNullAttribute : Attribute +{ +} + +/// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class MaybeNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } +} + +/// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class NotNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } +} + +/// Specifies that the output will be non-null if the named parameter is non-null. +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class NotNullIfNotNullAttribute : Attribute +{ + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } +} + +/// Applied to a method that will never return under any circumstance. +[AttributeUsage(AttributeTargets.Method, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class DoesNotReturnAttribute : Attribute +{ +} + +/// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class DoesNotReturnIfAttribute : Attribute +{ + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } +} +#endif + +/// Specifies that the method or property will ensure that the listed field and property members have not-null values. +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +[ExcludeFromCodeCoverage] +internal sealed class MemberNotNullAttribute : Attribute +{ + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute(string member) => Members = new[] { member }; + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) => Members = members; + + /// Gets field or property member names. + public string[] Members { get; } +} + +/// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +[ExcludeFromCodeCoverage] +internal sealed class MemberNotNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = new[] { member }; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } +} diff --git a/src/LegacySupport/DiagnosticAttributes/README.md b/src/LegacySupport/DiagnosticAttributes/README.md new file mode 100644 index 0000000000..b34b86160e --- /dev/null +++ b/src/LegacySupport/DiagnosticAttributes/README.md @@ -0,0 +1,7 @@ +To use this source in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/LegacySupport/DictionaryExtensions/DictionaryExtensions.cs b/src/LegacySupport/DictionaryExtensions/DictionaryExtensions.cs new file mode 100644 index 0000000000..6b2cf5cfae --- /dev/null +++ b/src/LegacySupport/DictionaryExtensions/DictionaryExtensions.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace System.Collections.Generic; + +/// +/// Extensions for . +/// +internal static class DictionaryExtensions +{ + /// + /// Tries to remove the value with the specified key from the dictionary. + /// + /// The type of the keys in the dictionary. + /// The type of the values in the dictionary. + /// The dictionary to operate on. + /// The key of the value to query or add. + /// When this method returns true, the removed value; when this method returns false, the default value for TValue. + /// true when a value is found in the dictionary with the specified key; false when the dictionary cannot find a value associated with the specified key. + /// If the dictionary or key are . + [ExcludeFromCodeCoverage] + public static bool Remove(this IDictionary dictionary, TKey key, [MaybeNullWhen(false)] out TValue value) + { + if (dictionary.TryGetValue(key, out value)) + { + _ = dictionary.Remove(key); + return true; + } + + value = default; + return false; + } +} diff --git a/src/LegacySupport/DictionaryExtensions/README.md b/src/LegacySupport/DictionaryExtensions/README.md new file mode 100644 index 0000000000..250b45346c --- /dev/null +++ b/src/LegacySupport/DictionaryExtensions/README.md @@ -0,0 +1,7 @@ +To use this source in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/LegacySupport/ExperimentalAttribute/ExperimentalAttribute.cs b/src/LegacySupport/ExperimentalAttribute/ExperimentalAttribute.cs new file mode 100644 index 0000000000..6d7d44a95f --- /dev/null +++ b/src/LegacySupport/ExperimentalAttribute/ExperimentalAttribute.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Indicates that an API element is experimental and subject to change without notice. +/// +[ExcludeFromCodeCoverage] +[AttributeUsage( + AttributeTargets.Class | + AttributeTargets.Struct | + AttributeTargets.Enum | + AttributeTargets.Interface | + AttributeTargets.Delegate | + AttributeTargets.Method | + AttributeTargets.Constructor | + AttributeTargets.Property | + AttributeTargets.Field | + AttributeTargets.Event | + AttributeTargets.Assembly)] +internal sealed class ExperimentalAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + public ExperimentalAttribute() + { + // Intentionally left empty. + } + + /// + /// Initializes a new instance of the class. + /// + /// Human readable explanation for marking experimental API. + public ExperimentalAttribute(string message) + { +#pragma warning disable R9A014 // Use the 'Microsoft.Extensions.Diagnostics.Throws' class instead of explicitly throwing exception for improved performance +#pragma warning disable R9A039 // Remove superfluous null check when compiling in a nullable context +#pragma warning disable R9A060 // Consider removing unnecessary null coalescing (??) since the left-hand value is statically known not to be null +#pragma warning disable SA1101 // Prefix local calls with this + Message = message ?? throw new ArgumentNullException(nameof(message)); +#pragma warning restore SA1101 // Prefix local calls with this +#pragma warning restore R9A060 // Consider removing unnecessary null coalescing (??) since the left-hand value is statically known not to be null +#pragma warning restore R9A039 // Remove superfluous null check when compiling in a nullable context +#pragma warning restore R9A014 // Use the 'Microsoft.Extensions.Diagnostics.Throws' class instead of explicitly throwing exception for improved performance + } + + /// + /// Gets a human readable explanation for marking API as experimental. + /// + public string? Message { get; } +} diff --git a/src/LegacySupport/GetOrAdd/GetOrAddExtensions.cs b/src/LegacySupport/GetOrAdd/GetOrAddExtensions.cs new file mode 100644 index 0000000000..b495983f02 --- /dev/null +++ b/src/LegacySupport/GetOrAdd/GetOrAddExtensions.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace System.Collections.Concurrent; + +/// +/// Extensions for . +/// +internal static class GetOrAddExtensions +{ + /// + /// Adds a key/value pair to a concurrent dictionary by using the specified function and an argument if the key does not already exist, or returns the existing value if the key exists. + /// + /// The type of the keys in the dictionary. + /// The type of the values in the dictionary. + /// The type of state to pass to the value factory. + /// The dictionary to operate on. + /// The key of the value to query or add. + /// A function that returns a value to insert into the dictionary if it is not already present. + /// The state to pass to the value factory. + /// The value for the key. This will be either the existing value for the key if the key is already in the dictionary, or the new value if the key was not in the dictionary. + /// If the dictionary, key, or value factory are . + [ExcludeFromCodeCoverage] + public static TValue GetOrAdd(this ConcurrentDictionary dictionary, TKey key, Func valueFactory, TArg factoryArgument) + { + if (dictionary.TryGetValue(key, out TValue value)) + { + return value; + } + + return dictionary.GetOrAdd(key, valueFactory(key, factoryArgument)); + } +} diff --git a/src/LegacySupport/GetOrAdd/README.md b/src/LegacySupport/GetOrAdd/README.md new file mode 100644 index 0000000000..a0a09261ad --- /dev/null +++ b/src/LegacySupport/GetOrAdd/README.md @@ -0,0 +1,7 @@ +To use this source in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/LegacySupport/IsExternalInit/IsExternalInit.cs b/src/LegacySupport/IsExternalInit/IsExternalInit.cs new file mode 100644 index 0000000000..4e1b8ba04c --- /dev/null +++ b/src/LegacySupport/IsExternalInit/IsExternalInit.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0079 +#pragma warning disable S3903 + +/* This enables support for C# 9/10 records on older frameworks */ + +namespace System.Runtime.CompilerServices; + +internal static class IsExternalInit +{ +} diff --git a/src/LegacySupport/IsExternalInit/README.md b/src/LegacySupport/IsExternalInit/README.md new file mode 100644 index 0000000000..97d61e0c7f --- /dev/null +++ b/src/LegacySupport/IsExternalInit/README.md @@ -0,0 +1,9 @@ +Enables use of C# record types on older frameworks. + +To use this source in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/LegacySupport/README.md b/src/LegacySupport/README.md new file mode 100644 index 0000000000..dc06a9c733 --- /dev/null +++ b/src/LegacySupport/README.md @@ -0,0 +1,8 @@ +# About this Folder + +This folder contains a bunch of sources copied from newer versions of .NET which we pull in to +R9 sources as necessary. This enables us to compile source code that depends on these newer +features from .NET even when targeting older frameworks. + +Please see the `eng/MSBuild/LegacySupport.props` file for the set of project properties that control importing +these source files into your project. diff --git a/src/LegacySupport/SkipLocalsInitAttribute/README.md b/src/LegacySupport/SkipLocalsInitAttribute/README.md new file mode 100644 index 0000000000..131cbb1053 --- /dev/null +++ b/src/LegacySupport/SkipLocalsInitAttribute/README.md @@ -0,0 +1,7 @@ +To use this source in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/LegacySupport/SkipLocalsInitAttribute/SkipLocalsInitAttribute.cs b/src/LegacySupport/SkipLocalsInitAttribute/SkipLocalsInitAttribute.cs new file mode 100644 index 0000000000..af85d7f71c --- /dev/null +++ b/src/LegacySupport/SkipLocalsInitAttribute/SkipLocalsInitAttribute.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0079 +#pragma warning disable SA1116 +#pragma warning disable SA1117 +#pragma warning disable SA1512 + +using System.Diagnostics.CodeAnalysis; + +namespace System.Runtime.CompilerServices; + +/// +/// Used to indicate to the compiler that the .locals init +/// flag should not be set in method headers. +/// +/// +/// This attribute is unsafe because it may reveal uninitialized memory to +/// the application in certain instances (e.g., reading from uninitialized +/// stackalloc'd memory). If applied to a method directly, the attribute +/// applies to that method and all nested functions (lambdas, local +/// functions) below it. If applied to a type or module, it applies to all +/// methods nested inside. This attribute is intentionally not permitted on +/// assemblies. Use at the module level instead to apply to multiple type +/// declarations. +/// +[AttributeUsage(AttributeTargets.Module + | AttributeTargets.Class + | AttributeTargets.Struct + | AttributeTargets.Interface + | AttributeTargets.Constructor + | AttributeTargets.Method + | AttributeTargets.Property + | AttributeTargets.Event, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class SkipLocalsInitAttribute : Attribute +{ +} diff --git a/src/LegacySupport/StringBuilderExtensions/README.md b/src/LegacySupport/StringBuilderExtensions/README.md new file mode 100644 index 0000000000..9837280bb0 --- /dev/null +++ b/src/LegacySupport/StringBuilderExtensions/README.md @@ -0,0 +1,7 @@ +To use this source in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/LegacySupport/StringBuilderExtensions/StringBuilderExtensions.cs b/src/LegacySupport/StringBuilderExtensions/StringBuilderExtensions.cs new file mode 100644 index 0000000000..c98554d6dc --- /dev/null +++ b/src/LegacySupport/StringBuilderExtensions/StringBuilderExtensions.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace System.Text; + +[ExcludeFromCodeCoverage] +internal static class StringBuilderExtensions +{ + public static StringBuilder Append(this StringBuilder sb, ReadOnlySpan value) + { + if (value.Length > 0) + { + unsafe + { + fixed (char* valueChars = &MemoryMarshal.GetReference(value)) + { + _ = sb.Append(valueChars, value.Length); + } + } + } + + return sb; + } +} diff --git a/src/LegacySupport/StringHash/README.md b/src/LegacySupport/StringHash/README.md new file mode 100644 index 0000000000..73043d6a22 --- /dev/null +++ b/src/LegacySupport/StringHash/README.md @@ -0,0 +1,7 @@ +To use this source in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/LegacySupport/StringHash/StringHash.cs b/src/LegacySupport/StringHash/StringHash.cs new file mode 100644 index 0000000000..c150427bc0 --- /dev/null +++ b/src/LegacySupport/StringHash/StringHash.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0079 +#pragma warning disable CPR103 +#pragma warning disable S3903 + +using System.Diagnostics.CodeAnalysis; + +namespace System; + +[ExcludeFromCodeCoverage] +internal static class StringHash +{ + public static int GetHashCode(this string s, StringComparison comparisonType) + { + var comparer = comparisonType switch + { + StringComparison.CurrentCulture => StringComparer.CurrentCulture, + StringComparison.CurrentCultureIgnoreCase => StringComparer.CurrentCultureIgnoreCase, + StringComparison.InvariantCulture => StringComparer.InvariantCulture, + StringComparison.InvariantCultureIgnoreCase => StringComparer.InvariantCultureIgnoreCase, + StringComparison.Ordinal => StringComparer.Ordinal, + StringComparison.OrdinalIgnoreCase => StringComparer.OrdinalIgnoreCase, + _ => throw new ArgumentOutOfRangeException(nameof(comparisonType)), + }; + + return comparer.GetHashCode(s); + } +} diff --git a/src/LegacySupport/StringSyntaxAttribute/README.md b/src/LegacySupport/StringSyntaxAttribute/README.md new file mode 100644 index 0000000000..7fca1b9b25 --- /dev/null +++ b/src/LegacySupport/StringSyntaxAttribute/README.md @@ -0,0 +1,7 @@ +To use this source in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/LegacySupport/StringSyntaxAttribute/StringSyntaxAttribute.cs b/src/LegacySupport/StringSyntaxAttribute/StringSyntaxAttribute.cs new file mode 100644 index 0000000000..847fddb325 --- /dev/null +++ b/src/LegacySupport/StringSyntaxAttribute/StringSyntaxAttribute.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable SA1623 +#pragma warning disable SA1642 +#pragma warning disable SA1201 +#pragma warning disable SA1101 + +namespace System.Diagnostics.CodeAnalysis; + +/// Specifies the syntax used in a string. +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class StringSyntaxAttribute : Attribute +{ + /// Initializes the with the identifier of the syntax used. + /// The syntax identifier. + public StringSyntaxAttribute(string syntax) + { + Syntax = syntax; + Arguments = Array.Empty(); + } + + /// Initializes the with the identifier of the syntax used. + /// The syntax identifier. + /// Optional arguments associated with the specific syntax employed. + public StringSyntaxAttribute(string syntax, params object?[] arguments) + { + Syntax = syntax; + Arguments = arguments; + } + + /// Gets the identifier of the syntax used. + public string Syntax { get; } + + /// Optional arguments associated with the specific syntax employed. + public object?[] Arguments { get; } + + /// The syntax identifier for strings containing composite formats for string formatting. + public const string CompositeFormat = nameof(CompositeFormat); + + /// The syntax identifier for strings containing date format specifiers. + public const string DateOnlyFormat = nameof(DateOnlyFormat); + + /// The syntax identifier for strings containing date and time format specifiers. + public const string DateTimeFormat = nameof(DateTimeFormat); + + /// The syntax identifier for strings containing format specifiers. + public const string EnumFormat = nameof(EnumFormat); + + /// The syntax identifier for strings containing format specifiers. + public const string GuidFormat = nameof(GuidFormat); + + /// The syntax identifier for strings containing JavaScript Object Notation (JSON). + public const string Json = nameof(Json); + + /// The syntax identifier for strings containing numeric format specifiers. + public const string NumericFormat = nameof(NumericFormat); + + /// The syntax identifier for strings containing regular expressions. + public const string Regex = nameof(Regex); + + /// The syntax identifier for strings containing time format specifiers. + public const string TimeOnlyFormat = nameof(TimeOnlyFormat); + + /// The syntax identifier for strings containing format specifiers. + public const string TimeSpanFormat = nameof(TimeSpanFormat); + + /// The syntax identifier for strings containing URIs. + public const string Uri = nameof(Uri); + + /// The syntax identifier for strings containing XML. + public const string Xml = nameof(Xml); +} diff --git a/src/LegacySupport/TaskWaitAsync/README.md b/src/LegacySupport/TaskWaitAsync/README.md new file mode 100644 index 0000000000..f5e530d5b9 --- /dev/null +++ b/src/LegacySupport/TaskWaitAsync/README.md @@ -0,0 +1,7 @@ +To use this source in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/LegacySupport/TaskWaitAsync/TaskExtensions.cs b/src/LegacySupport/TaskWaitAsync/TaskExtensions.cs new file mode 100644 index 0000000000..271bf7da6b --- /dev/null +++ b/src/LegacySupport/TaskWaitAsync/TaskExtensions.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace System.Threading.Tasks; + +/// +/// Provides a set of static methods for . +/// +[ExcludeFromCodeCoverage] +internal static class TaskExtensions +{ +#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks + /// + /// Gets a that will complete when the completes or when the specified has cancellation requested. + /// + /// The type of the task result. + /// The task to wait on for completion. + /// The to monitor for a cancellation request. + /// The representing the asynchronous wait. + public static Task WaitAsync(this Task task, CancellationToken cancellationToken) + { + if (task.IsCompleted || (!cancellationToken.CanBeCanceled)) + { + return task; + } + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return WaitTaskAsync(task, cancellationToken); + } + + private static async Task WaitTaskAsync(Task task, CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using (cancellationToken.Register( + static o => ((TaskCompletionSource)o!).SetCanceled(), tcs, false)) + { + var t = await Task.WhenAny(task, tcs.Task).ConfigureAwait(false); +#pragma warning disable VSTHRD103 // Call async methods when in an async method + return t.GetAwaiter().GetResult(); +#pragma warning restore VSTHRD103 // Call async methods when in an async method + } + } +} diff --git a/src/LegacySupport/TrimAttributes/DynamicDependencyAttribute.cs b/src/LegacySupport/TrimAttributes/DynamicDependencyAttribute.cs new file mode 100644 index 0000000000..9b6615259a --- /dev/null +++ b/src/LegacySupport/TrimAttributes/DynamicDependencyAttribute.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0079 +#pragma warning disable SA1101 +#pragma warning disable SA1116 +#pragma warning disable SA1117 +#pragma warning disable SA1512 +#pragma warning disable SA1623 +#pragma warning disable SA1642 +#pragma warning disable S3903 + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// States a dependency that one member has on another. +/// +/// +/// This can be used to inform tooling of a dependency that is otherwise not evident purely from +/// metadata and IL, for example a member relied on via reflection. +/// +[AttributeUsage( + AttributeTargets.Constructor | AttributeTargets.Field | AttributeTargets.Method, + AllowMultiple = true, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class DynamicDependencyAttribute : Attribute +{ + /// + /// Initializes a new instance of the class + /// with the specified signature of a member on the same type as the consumer. + /// + /// The signature of the member depended on. + public DynamicDependencyAttribute(string memberSignature) + { + MemberSignature = memberSignature; + } + + /// + /// Initializes a new instance of the class + /// with the specified signature of a member on a . + /// + /// The signature of the member depended on. + /// The containing . + public DynamicDependencyAttribute(string memberSignature, Type type) + { + MemberSignature = memberSignature; + Type = type; + } + + /// + /// Initializes a new instance of the class + /// with the specified signature of a member on a type in an assembly. + /// + /// The signature of the member depended on. + /// The full name of the type containing the specified member. + /// The assembly name of the type containing the specified member. + public DynamicDependencyAttribute(string memberSignature, string typeName, string assemblyName) + { + MemberSignature = memberSignature; + TypeName = typeName; + AssemblyName = assemblyName; + } + + /// + /// Initializes a new instance of the class + /// with the specified types of members on a . + /// + /// The types of members depended on. + /// The containing the specified members. + public DynamicDependencyAttribute(DynamicallyAccessedMemberTypes memberTypes, Type type) + { + MemberTypes = memberTypes; + Type = type; + } + + /// + /// Initializes a new instance of the class + /// with the specified types of members on a type in an assembly. + /// + /// The types of members depended on. + /// The full name of the type containing the specified members. + /// The assembly name of the type containing the specified members. + public DynamicDependencyAttribute(DynamicallyAccessedMemberTypes memberTypes, string typeName, string assemblyName) + { + MemberTypes = memberTypes; + TypeName = typeName; + AssemblyName = assemblyName; + } + + /// + /// Gets the signature of the member depended on. + /// + /// + /// Either must be a valid string or + /// must not equal , but not both. + /// + public string? MemberSignature { get; } + + /// + /// Gets the which specifies the type + /// of members depended on. + /// + /// + /// Either must be a valid string or + /// must not equal , but not both. + /// + public DynamicallyAccessedMemberTypes MemberTypes { get; } + + /// + /// Gets the containing the specified member. + /// + /// + /// If neither nor are specified, + /// the type of the consumer is assumed. + /// + public Type? Type { get; } + + /// + /// Gets the full name of the type containing the specified member. + /// + /// + /// If neither nor are specified, + /// the type of the consumer is assumed. + /// + public string? TypeName { get; } + + /// + /// Gets the assembly name of the specified type. + /// + /// + /// is only valid when is specified. + /// + public string? AssemblyName { get; } + + /// + /// Gets or sets the condition in which the dependency is applicable, e.g. "DEBUG". + /// + public string? Condition { get; set; } +} diff --git a/src/LegacySupport/TrimAttributes/DynamicallyAccessedMemberTypes.cs b/src/LegacySupport/TrimAttributes/DynamicallyAccessedMemberTypes.cs new file mode 100644 index 0000000000..eb6326d5a3 --- /dev/null +++ b/src/LegacySupport/TrimAttributes/DynamicallyAccessedMemberTypes.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0079 +#pragma warning disable CA2217 +#pragma warning disable SA1413 +#pragma warning disable SA1512 +#pragma warning disable S4070 + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Specifies the types of members that are dynamically accessed. +/// +/// This enumeration has a attribute that allows a +/// bitwise combination of its member values. +/// +[Flags] +internal enum DynamicallyAccessedMemberTypes +{ + /// + /// Specifies no members. + /// + None = 0, + + /// + /// Specifies the default, parameterless public constructor. + /// + PublicParameterlessConstructor = 0x0001, + + /// + /// Specifies all public constructors. + /// + PublicConstructors = 0x0002 | PublicParameterlessConstructor, + + /// + /// Specifies all non-public constructors. + /// + NonPublicConstructors = 0x0004, + + /// + /// Specifies all public methods. + /// + PublicMethods = 0x0008, + + /// + /// Specifies all non-public methods. + /// + NonPublicMethods = 0x0010, + + /// + /// Specifies all public fields. + /// + PublicFields = 0x0020, + + /// + /// Specifies all non-public fields. + /// + NonPublicFields = 0x0040, + + /// + /// Specifies all public nested types. + /// + PublicNestedTypes = 0x0080, + + /// + /// Specifies all non-public nested types. + /// + NonPublicNestedTypes = 0x0100, + + /// + /// Specifies all public properties. + /// + PublicProperties = 0x0200, + + /// + /// Specifies all non-public properties. + /// + NonPublicProperties = 0x0400, + + /// + /// Specifies all public events. + /// + PublicEvents = 0x0800, + + /// + /// Specifies all non-public events. + /// + NonPublicEvents = 0x1000, + + /// + /// Specifies all interfaces implemented by the type. + /// + Interfaces = 0x2000, + + /// + /// Specifies all members. + /// + All = ~None +} diff --git a/src/LegacySupport/TrimAttributes/DynamicallyAccessedMembersAttribute.cs b/src/LegacySupport/TrimAttributes/DynamicallyAccessedMembersAttribute.cs new file mode 100644 index 0000000000..88d96b47ee --- /dev/null +++ b/src/LegacySupport/TrimAttributes/DynamicallyAccessedMembersAttribute.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0079 +#pragma warning disable SA1101 +#pragma warning disable SA1116 +#pragma warning disable SA1117 +#pragma warning disable SA1512 +#pragma warning disable SA1623 +#pragma warning disable SA1642 +#pragma warning disable S3903 + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Indicates that certain members on a specified are accessed dynamically, +/// for example through . +/// +/// +/// This allows tools to understand which members are being accessed during the execution +/// of a program. +/// +/// This attribute is valid on members whose type is or . +/// +/// When this attribute is applied to a location of type , the assumption is +/// that the string represents a fully qualified type name. +/// +/// When this attribute is applied to a class, interface, or struct, the members specified +/// can be accessed dynamically on instances returned from calling +/// on instances of that class, interface, or struct. +/// +/// If the attribute is applied to a method it's treated as a special case and it implies +/// the attribute should be applied to the "this" parameter of the method. As such the attribute +/// should only be used on instance methods of types assignable to System.Type (or string, but no methods +/// will use it there). +/// +[AttributeUsage( + AttributeTargets.Field | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter | + AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Method | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct, + Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class DynamicallyAccessedMembersAttribute : Attribute +{ + /// + /// Initializes a new instance of the class + /// with the specified member types. + /// + /// The types of members dynamically accessed. + public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes) + { + MemberTypes = memberTypes; + } + + /// + /// Gets the which specifies the type + /// of members dynamically accessed. + /// + public DynamicallyAccessedMemberTypes MemberTypes { get; } +} diff --git a/src/LegacySupport/TrimAttributes/README.md b/src/LegacySupport/TrimAttributes/README.md new file mode 100644 index 0000000000..da51e6cc13 --- /dev/null +++ b/src/LegacySupport/TrimAttributes/README.md @@ -0,0 +1,7 @@ +To use this source in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/LegacySupport/TrimAttributes/RequiresAssemblyFilesAttribute.cs b/src/LegacySupport/TrimAttributes/RequiresAssemblyFilesAttribute.cs new file mode 100644 index 0000000000..0729f9f970 --- /dev/null +++ b/src/LegacySupport/TrimAttributes/RequiresAssemblyFilesAttribute.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0079 +#pragma warning disable SA1101 +#pragma warning disable SA1116 +#pragma warning disable SA1117 +#pragma warning disable SA1512 +#pragma warning disable SA1623 +#pragma warning disable SA1642 +#pragma warning disable S3903 +#pragma warning disable S3996 + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// /// Indicates that the specified member requires assembly files to be on disk. +/// +[AttributeUsage(AttributeTargets.Constructor | + AttributeTargets.Event | + AttributeTargets.Method | + AttributeTargets.Property, + Inherited = false, + AllowMultiple = false)] +[ExcludeFromCodeCoverage] +internal sealed class RequiresAssemblyFilesAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + public RequiresAssemblyFilesAttribute() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// A message that contains information about the need for assembly files to be on disk. + /// + public RequiresAssemblyFilesAttribute(string message) + { + Message = message; + } + + /// + /// Gets an optional message that contains information about the need for + /// assembly files to be on disk. + /// + public string? Message { get; } + + /// + /// Gets or sets an optional URL that contains more information about the member, + /// why it requires assembly files to be on disk, and what options a consumer has + /// to deal with it. + /// + public string? Url { get; set; } +} diff --git a/src/LegacySupport/TrimAttributes/RequiresUnreferencedCodeAttribute.cs b/src/LegacySupport/TrimAttributes/RequiresUnreferencedCodeAttribute.cs new file mode 100644 index 0000000000..6ee43055e5 --- /dev/null +++ b/src/LegacySupport/TrimAttributes/RequiresUnreferencedCodeAttribute.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0079 +#pragma warning disable SA1101 +#pragma warning disable SA1116 +#pragma warning disable SA1117 +#pragma warning disable SA1512 +#pragma warning disable SA1623 +#pragma warning disable SA1642 +#pragma warning disable S3903 +#pragma warning disable S3996 + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// /// Indicates that the specified method requires dynamic access to code that is not referenced +/// statically, for example through . +/// +/// +/// This allows tools to understand which methods are unsafe to call when removing unreferenced +/// code from an application. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class, Inherited = false)] +[ExcludeFromCodeCoverage] +internal sealed class RequiresUnreferencedCodeAttribute : Attribute +{ + /// + /// Initializes a new instance of the class + /// with the specified message. + /// + /// + /// A message that contains information about the usage of unreferenced code. + /// + public RequiresUnreferencedCodeAttribute(string message) + { + Message = message; + } + + /// + /// Gets a message that contains information about the usage of unreferenced code. + /// + public string Message { get; } + + /// + /// Gets or sets an optional URL that contains more information about the method, + /// why it requires unreferenced code, and what options a consumer has to deal with it. + /// + public string? Url { get; set; } +} diff --git a/src/LegacySupport/TrimAttributes/UnconditionalSuppressMessageAttribute.cs b/src/LegacySupport/TrimAttributes/UnconditionalSuppressMessageAttribute.cs new file mode 100644 index 0000000000..c9095d2261 --- /dev/null +++ b/src/LegacySupport/TrimAttributes/UnconditionalSuppressMessageAttribute.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0079 +#pragma warning disable SA1101 +#pragma warning disable SA1116 +#pragma warning disable SA1117 +#pragma warning disable SA1512 +#pragma warning disable SA1623 +#pragma warning disable SA1642 +#pragma warning disable S3903 + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// /// Suppresses reporting of a specific rule violation, allowing multiple suppressions on a +/// single code artifact. +/// +/// +/// is different than +/// in that it doesn't have a +/// . So it is always preserved in the compiled assembly. +/// +[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] +[ExcludeFromCodeCoverage] +internal sealed class UnconditionalSuppressMessageAttribute : Attribute +{ + /// + /// Initializes a new instance of the + /// class, specifying the category of the tool and the identifier for an analysis rule. + /// + /// The category for the attribute. + /// The identifier of the analysis rule the attribute applies to. + public UnconditionalSuppressMessageAttribute(string category, string checkId) + { + Category = category; + CheckId = checkId; + } + + /// + /// Gets the category identifying the classification of the attribute. + /// + /// + /// The property describes the tool or tool analysis category + /// for which a message suppression attribute applies. + /// + public string Category { get; } + + /// + /// Gets the identifier of the analysis tool rule to be suppressed. + /// + /// + /// Concatenated together, the and + /// properties form a unique check identifier. + /// + public string CheckId { get; } + + /// + /// Gets or sets the scope of the code that is relevant for the attribute. + /// + /// + /// The Scope property is an optional argument that specifies the metadata scope for which + /// the attribute is relevant. + /// + public string? Scope { get; set; } + + /// + /// Gets or sets a fully qualified path that represents the target of the attribute. + /// + /// + /// The property is an optional argument identifying the analysis target + /// of the attribute. An example value is "System.IO.Stream.ctor():System.Void". + /// Because it is fully qualified, it can be long, particularly for targets such as parameters. + /// The analysis tool user interface should be capable of automatically formatting the parameter. + /// + public string? Target { get; set; } + + /// + /// Gets or sets an optional argument expanding on exclusion criteria. + /// + /// + /// The property is an optional argument that specifies additional + /// exclusion where the literal metadata target is not sufficiently precise. For example, + /// the cannot be applied within a method, + /// and it may be desirable to suppress a violation against a statement in the method that will + /// give a rule violation, but not against all statements in the method. + /// + public string? MessageId { get; set; } + + /// + /// Gets or sets the justification for suppressing the code analysis message. + /// + public string? Justification { get; set; } +} diff --git a/src/LegacySupport/xxH3/README.md b/src/LegacySupport/xxH3/README.md new file mode 100644 index 0000000000..285163aa43 --- /dev/null +++ b/src/LegacySupport/xxH3/README.md @@ -0,0 +1,7 @@ +To use this source in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/LegacySupport/xxH3/XxHash3.cs b/src/LegacySupport/xxH3/XxHash3.cs new file mode 100644 index 0000000000..52bf41dd13 --- /dev/null +++ b/src/LegacySupport/xxH3/XxHash3.cs @@ -0,0 +1,1069 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable CS3021 +#pragma warning disable S109 +#pragma warning disable SA1513 +#pragma warning disable SA1005 +#pragma warning disable S2148 +#pragma warning disable SA1405 +#pragma warning disable SA1519 +#pragma warning disable S907 +#pragma warning disable S1199 +#pragma warning disable IDE0055 +#pragma warning disable SA1407 +#pragma warning disable S103 +#pragma warning disable SA1623 +#pragma warning disable SA1128 +#pragma warning disable SA1629 +#pragma warning disable SA1131 +#pragma warning disable SA1106 +#pragma warning disable S2333 +#pragma warning disable SA1202 +#pragma warning disable S1135 +#pragma warning disable S2156 +#pragma warning disable R9A015 + +// Based on the XXH3 implementation from https://github.com/Cyan4973/xxHash. + +using System.Buffers.Binary; +using System.Diagnostics; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +#if NET7_0_OR_GREATER +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; +#endif + +namespace System.IO.Hashing +{ + /// Provides an implementation of the XXH3 hash algorithm. + /// + /// For methods that persist the computed numerical hash value as bytes, + /// the value is written in the Big Endian byte order. + /// +#if NET5_0_OR_GREATER + [SkipLocalsInit] +#endif + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal sealed unsafe partial class XxHash3 + { + /// XXH3 produces 8-byte hashes. + private const int StripeLengthBytes = 64; + private const int SecretLengthBytes = 192; + private const int SecretLastAccStartBytes = 7; + private const int SecretConsumeRateBytes = 8; + private const int AccumulatorCount = StripeLengthBytes / sizeof(ulong); + private const int MidSizeMaxBytes = 240; + + /// The default secret for when no seed is provided. + /// This is the same as a custom secret derived from a seed of 0. + private static ReadOnlySpan DefaultSecret => new byte[] + { + 0xb8, 0xfe, 0x6c, 0x39, 0x23, 0xa4, 0x4b, 0xbe, 0x7c, 0x01, 0x81, 0x2c, 0xf7, 0x21, 0xad, 0x1c, + 0xde, 0xd4, 0x6d, 0xe9, 0x83, 0x90, 0x97, 0xdb, 0x72, 0x40, 0xa4, 0xa4, 0xb7, 0xb3, 0x67, 0x1f, + 0xcb, 0x79, 0xe6, 0x4e, 0xcc, 0xc0, 0xe5, 0x78, 0x82, 0x5a, 0xd0, 0x7d, 0xcc, 0xff, 0x72, 0x21, + 0xb8, 0x08, 0x46, 0x74, 0xf7, 0x43, 0x24, 0x8e, 0xe0, 0x35, 0x90, 0xe6, 0x81, 0x3a, 0x26, 0x4c, + 0x3c, 0x28, 0x52, 0xbb, 0x91, 0xc3, 0x00, 0xcb, 0x88, 0xd0, 0x65, 0x8b, 0x1b, 0x53, 0x2e, 0xa3, + 0x71, 0x64, 0x48, 0x97, 0xa2, 0x0d, 0xf9, 0x4e, 0x38, 0x19, 0xef, 0x46, 0xa9, 0xde, 0xac, 0xd8, + 0xa8, 0xfa, 0x76, 0x3f, 0xe3, 0x9c, 0x34, 0x3f, 0xf9, 0xdc, 0xbb, 0xc7, 0xc7, 0x0b, 0x4f, 0x1d, + 0x8a, 0x51, 0xe0, 0x4b, 0xcd, 0xb4, 0x59, 0x31, 0xc8, 0x9f, 0x7e, 0xc9, 0xd9, 0x78, 0x73, 0x64, + 0xea, 0xc5, 0xac, 0x83, 0x34, 0xd3, 0xeb, 0xc3, 0xc5, 0x81, 0xa0, 0xff, 0xfa, 0x13, 0x63, 0xeb, + 0x17, 0x0d, 0xdd, 0x51, 0xb7, 0xf0, 0xda, 0x49, 0xd3, 0x16, 0x55, 0x26, 0x29, 0xd4, 0x68, 0x9e, + 0x2b, 0x16, 0xbe, 0x58, 0x7d, 0x47, 0xa1, 0xfc, 0x8f, 0xf8, 0xb8, 0xd1, 0x7a, 0xd0, 0x31, 0xce, + 0x45, 0xcb, 0x3a, 0x8f, 0x95, 0x16, 0x04, 0x28, 0xaf, 0xd7, 0xfb, 0xca, 0xbb, 0x4b, 0x40, 0x7e, + }; + +#if DEBUG + static XxHash3() + { + // Make sure DefaultSecret is the custom secret derived from a seed of 0. + byte* secret = stackalloc byte[SecretLengthBytes]; + DeriveSecretFromSeed(secret, 0); + Debug.Assert(new Span(secret, SecretLengthBytes).SequenceEqual(DefaultSecret)); + } +#endif + +#if false + private State _state; + + /// Initializes a new instance of the class using the default seed value 0. + public XxHash3() : this(0) + { + } + + /// Initializes a new instance of the class using the specified seed. + public XxHash3(long seed) + { + _state.Seed = (ulong)seed; + + fixed (byte* secret = _state.Secret) + { + if (seed == 0) + { + DefaultSecret.CopyTo(new Span(secret, SecretLengthBytes)); + } + else + { + DeriveSecretFromSeed(secret, (ulong)seed); + } + } + + Reset(); + } + + /// Computes the XXH3 hash of the provided data. + /// The data to hash. + /// The XXH3 64-bit hash code of the provided data. + /// is null. + public static byte[] Hash(byte[] source) => Hash(source, seed: 0); + + /// Computes the XXH3 hash of the provided data using the provided seed. + /// The data to hash. + /// The seed value for this hash computation. + /// The XXH3 64-bit hash code of the provided data. + /// is null. + public static byte[] Hash(byte[] source, long seed) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); +#else + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } +#endif + + return Hash(new ReadOnlySpan(source), seed); + } + + /// Computes the XXH3 hash of the provided data using the optionally provided . + /// The data to hash. + /// The seed value for this hash computation. The default is zero. + /// The XXH3 64-bit hash code of the provided data. + public static byte[] Hash(ReadOnlySpan source, long seed = 0) + { + byte[] result = new byte[HashLengthInBytes]; + ulong hash = HashToUInt64(source, seed); + BinaryPrimitives.WriteUInt64BigEndian(result, hash); + return result; + } + + /// Computes the XXH3 hash of the provided data into the provided using the optionally provided . + /// The data to hash. + /// The buffer that receives the computed 64-bit hash code. + /// The seed value for this hash computation. The default is zero. + /// The number of bytes written to . + /// is shorter than (8 bytes). + public static int Hash(ReadOnlySpan source, Span destination, long seed = 0) + { + if (!TryHash(source, destination, out int bytesWritten, seed)) + { + throw new InvalidOperationException(); + } + + return bytesWritten; + } + + /// Attempts to compute the XXH3 hash of the provided data into the provided using the optionally provided . + /// The data to hash. + /// The buffer that receives the computed 64-bit hash code. + /// When this method returns, contains the number of bytes written to . + /// The seed value for this hash computation. The default is zero. + /// if is long enough to receive the computed hash value (8 bytes); otherwise, . + public static bool TryHash(ReadOnlySpan source, Span destination, out int bytesWritten, long seed = 0) + { + if (destination.Length >= sizeof(long)) + { + ulong hash = HashToUInt64(source, seed); + + if (BitConverter.IsLittleEndian) + { + hash = BinaryPrimitives.ReverseEndianness(hash); + } + Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(destination), hash); + + bytesWritten = HashLengthInBytes; + return true; + } + + bytesWritten = 0; + return false; + } +#endif + + /// Computes the XXH3 hash of the provided data. + /// The data to hash. + /// The seed value for this hash computation. + /// The computed XXH3 hash. + [CLSCompliant(false)] + public static ulong HashToUInt64(ReadOnlySpan source, long seed = 0) + { + uint length = (uint)source.Length; + fixed (byte* sourcePtr = &MemoryMarshal.GetReference(source)) + { + if (length <= 16) + { + return HashLength0To16(sourcePtr, length, (ulong)seed); + } + + if (length <= 128) + { + return HashLength17To128(sourcePtr, length, (ulong)seed); + } + + if (length <= MidSizeMaxBytes) + { + return HashLength129To240(sourcePtr, length, (ulong)seed); + } + + return HashLengthOver240(sourcePtr, length, (ulong)seed); + } + } + +#if false + /// Resets the hash computation to the initial state. + public void Reset() + { + _state.BufferedCount = 0; + _state.StripesProcessedInCurrentBlock = 0; + _state.TotalLength = 0; + + fixed (ulong* accumulators = _state.Accumulators) + { + InitializeAccumulators(accumulators); + } + } + + /// Appends the contents of to the data already processed for the current hash computation. + /// The data to process. + public void Append(ReadOnlySpan source) + { + Debug.Assert(_state.BufferedCount <= InternalBufferLengthBytes); + + _state.TotalLength += (uint)source.Length; + + fixed (byte* buffer = _state.Buffer) + { + // Small input: just copy the data to the buffer. + if (source.Length <= InternalBufferLengthBytes - _state.BufferedCount) + { + source.CopyTo(new Span(buffer + _state.BufferedCount, source.Length)); + _state.BufferedCount += (uint)source.Length; + return; + } + + fixed (byte* secret = _state.Secret) + fixed (ulong* accumulators = _state.Accumulators) + fixed (byte* sourcePtr = &MemoryMarshal.GetReference(source)) + { + // Internal buffer is partially filled (always, except at beginning). Complete it, then consume it. + int sourceIndex = 0; + if (_state.BufferedCount != 0) + { + int loadSize = InternalBufferLengthBytes - (int)_state.BufferedCount; + + source.Slice(0, loadSize).CopyTo(new Span(buffer + _state.BufferedCount, loadSize)); + sourceIndex = loadSize; + + ConsumeStripes(accumulators, ref _state.StripesProcessedInCurrentBlock, NumStripesPerBlock, buffer, InternalBufferStripes, secret); + _state.BufferedCount = 0; + } + Debug.Assert(sourceIndex < source.Length); + + // Large input to consume: ingest per full block. + if (source.Length - sourceIndex > NumStripesPerBlock * StripeLengthBytes) + { + ulong stripes = (ulong)(source.Length - sourceIndex - 1) / StripeLengthBytes; + Debug.Assert(NumStripesPerBlock >= _state.StripesProcessedInCurrentBlock); + + // Join to current block's end. + ulong stripesToEnd = NumStripesPerBlock - _state.StripesProcessedInCurrentBlock; + Debug.Assert(stripesToEnd <= stripes); + Accumulate(accumulators, sourcePtr + sourceIndex, secret + ((int)_state.StripesProcessedInCurrentBlock * SecretConsumeRateBytes), (int)stripesToEnd); + ScrambleAccumulators(accumulators, secret + (SecretLengthBytes - StripeLengthBytes)); + _state.StripesProcessedInCurrentBlock = 0; + sourceIndex += (int)stripesToEnd * StripeLengthBytes; + stripes -= stripesToEnd; + + // Consume entire blocks. + while (stripes >= NumStripesPerBlock) + { + Accumulate(accumulators, sourcePtr + sourceIndex, secret, NumStripesPerBlock); + ScrambleAccumulators(accumulators, secret + (SecretLengthBytes - StripeLengthBytes)); + sourceIndex += NumStripesPerBlock * StripeLengthBytes; + stripes -= NumStripesPerBlock; + } + + // Consume complete stripes in the last partial block. + Accumulate(accumulators, sourcePtr + sourceIndex, secret, (int)stripes); + sourceIndex += (int)stripes * StripeLengthBytes; + Debug.Assert(sourceIndex < source.Length); // at least some bytes left + _state.StripesProcessedInCurrentBlock = stripes; + + // Copy the last stripe into the end of the buffer so it is available to GetCurrentHashCore when processing the "stripe from the end". + source.Slice(sourceIndex - StripeLengthBytes, StripeLengthBytes).CopyTo(new Span(buffer + InternalBufferLengthBytes - StripeLengthBytes, StripeLengthBytes)); + } + else if (source.Length - sourceIndex > InternalBufferLengthBytes) + { + // Content to consume <= block size. Consume source by a multiple of internal buffer size. + do + { + ConsumeStripes(accumulators, ref _state.StripesProcessedInCurrentBlock, NumStripesPerBlock, sourcePtr + sourceIndex, InternalBufferStripes, secret); + sourceIndex += InternalBufferLengthBytes; + } + while (source.Length - sourceIndex > InternalBufferLengthBytes); + + // Copy the last stripe into the end of the buffer so it is available to GetCurrentHashCore when processing the "stripe from the end". + source.Slice(sourceIndex - StripeLengthBytes, StripeLengthBytes).CopyTo(new Span(buffer + InternalBufferLengthBytes - StripeLengthBytes, StripeLengthBytes)); + } + + // Buffer the remaining input. + Span remaining = new Span(buffer, source.Length - sourceIndex); + Debug.Assert(sourceIndex < source.Length); + Debug.Assert(remaining.Length <= InternalBufferLengthBytes); + source.Slice(sourceIndex).CopyTo(remaining); + _state.BufferedCount = (uint)remaining.Length; + } + } + } + + /// Gets the current computed hash value without modifying accumulated state. + /// The hash value for the data already provided. + [CLSCompliant(false)] + public ulong GetCurrentHashAsUInt64() + { + ulong current; + + if (_state.TotalLength > MidSizeMaxBytes) + { + // Digest on a local copy to ensure the accumulators remain unaltered. + ulong* accumulators = stackalloc ulong[AccumulatorCount]; + fixed (ulong* stateAccumulators = _state.Accumulators) + { +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) + { + Vector256.Store(Vector256.Load(stateAccumulators), accumulators); + Vector256.Store(Vector256.Load(stateAccumulators + 4), accumulators + 4); + } + else if (Vector128.IsHardwareAccelerated) + { + Vector128.Store(Vector128.Load(stateAccumulators), accumulators); + Vector128.Store(Vector128.Load(stateAccumulators + 2), accumulators + 2); + Vector128.Store(Vector128.Load(stateAccumulators + 4), accumulators + 4); + Vector128.Store(Vector128.Load(stateAccumulators + 6), accumulators + 6); + } + else +#endif + { + for (int i = 0; i < 8; i++) + { + accumulators[i] = stateAccumulators[i]; + } + } + } + + fixed (byte* secret = _state.Secret) + { + DigestLong(accumulators, secret); + current = MergeAccumulators(accumulators, secret + SecretMergeAccsStartBytes, _state.TotalLength * XxHash64.Prime64_1); + } + } + else + { + fixed (byte* buffer = _state.Buffer) + { + current = HashToUInt64(new ReadOnlySpan(buffer, (int)_state.TotalLength), (long)_state.Seed); + } + } + + return current; + + void DigestLong(ulong* accumulators, byte* secret) + { + Debug.Assert(_state.BufferedCount > 0); + + fixed (byte* buffer = _state.Buffer) + { + byte* accumulateData; + if (_state.BufferedCount >= StripeLengthBytes) + { + uint stripes = (_state.BufferedCount - 1) / StripeLengthBytes; + ulong stripesSoFar = _state.StripesProcessedInCurrentBlock; + + ConsumeStripes(accumulators, ref stripesSoFar, NumStripesPerBlock, buffer, stripes, secret); + + accumulateData = buffer + _state.BufferedCount - StripeLengthBytes; + } + else + { + byte* lastStripe = stackalloc byte[StripeLengthBytes]; + int catchupSize = StripeLengthBytes - (int)_state.BufferedCount; + + new ReadOnlySpan(buffer + InternalBufferLengthBytes - catchupSize, catchupSize).CopyTo(new Span(lastStripe, StripeLengthBytes)); + new ReadOnlySpan(buffer, (int)_state.BufferedCount).CopyTo(new Span(lastStripe + catchupSize, (int)_state.BufferedCount)); + + accumulateData = lastStripe; + } + + Accumulate512(accumulators, accumulateData, secret + (SecretLengthBytes - StripeLengthBytes - SecretLastAccStartBytes)); + } + } + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong HashLength0To16(byte* source, uint length, ulong seed) + { + if (length > 8) + { + return HashLength9To16(source, length, seed); + } + + if (length >= 4) + { + return HashLength4To8(source, length, seed); + } + + if (length != 0) + { + return HashLength1To3(source, length, seed); + } + + return XxHash64.Avalanche(seed ^ 0x8726F9105DC21DDC); // DefaultSecretUInt64[7] ^ DefaultSecretUInt64[8] + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong HashLength1To3(byte* source, uint length, ulong seed) + { + Debug.Assert(length >= 1 && length <= 3); + + // When source.Length == 1, c1 == source[0], c2 == source[0], c3 == source[0] + // When source.Length == 2, c1 == source[0], c2 == source[1], c3 == source[1] + // When source.Length == 3, c1 == source[0], c2 == source[1], c3 == source[2] + byte c1 = *source; + byte c2 = source[length >> 1]; + byte c3 = source[length - 1]; + + uint combined = ((uint)c1 << 16) | ((uint)c2 << 24) | c3 | (length << 8); + + return XxHash64.Avalanche(combined ^ (0x87275A9B + seed)); // DefaultSecretUInt32[0] ^ DefaultSecretUInt32[1] + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong HashLength4To8(byte* source, uint length, ulong seed) + { + Debug.Assert(length >= 4 && length <= 8); + + seed ^= (ulong)BinaryPrimitives.ReverseEndianness((uint)seed) << 32; + + uint inputLow = ReadUInt32LE(source); + uint inputHigh = ReadUInt32LE(source + length - sizeof(uint)); + + ulong bitflip = 0xC73AB174C5ECD5A2 - seed; // DefaultSecretUInt64[1] ^ DefaultSecretUInt64[2] + ulong input64 = inputHigh + (((ulong)inputLow) << 32); + + return Rrmxmx(input64 ^ bitflip, length); + } + + /// This is a stronger avalanche, preferable when input has not been previously mixed. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Rrmxmx(ulong hash, uint length) + { + hash ^= BitOperations.RotateLeft(hash, 49) ^ BitOperations.RotateLeft(hash, 24); + hash *= 0x9FB21C651E98DF25; + hash ^= (hash >> 35) + length; + hash *= 0x9FB21C651E98DF25; + return XorShift(hash, 28); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong HashLength9To16(byte* source, uint length, ulong seed) + { + Debug.Assert(length >= 9 && length <= 16); + + ulong bitflipLow = 0x6782737BEA4239B9 + seed; // DefaultSecretUInt64[3] ^ DefaultSecretUInt64[4] + ulong bitflipHigh = 0xAF56BC3B0996523A - seed; // DefaultSecretUInt64[5] ^ DefaultSecretUInt64[6] + + ulong inputLow = ReadUInt64LE(source) ^ bitflipLow; + ulong inputHigh = ReadUInt64LE(source + length - sizeof(ulong)) ^ bitflipHigh; + + return Avalanche( + length + + BinaryPrimitives.ReverseEndianness(inputLow) + + inputHigh + + Multiply64To128ThenFold(inputLow, inputHigh)); + } + + private static ulong HashLength17To128(byte* source, uint length, ulong seed) + { + Debug.Assert(length >= 17 && length <= 128); + + ulong hash = length * XxHash64.Prime64_1; + + switch ((length - 1) / 32) + { + default: // case 3 + hash += Mix16Bytes(source + 48, 0x3F349CE33F76FAA8, 0x1D4F0BC7C7BBDCF9, seed); // DefaultSecretUInt64[12], DefaultSecretUInt64[13] + hash += Mix16Bytes(source + length - 64, 0x3159B4CD4BE0518A, 0x647378D9C97E9FC8, seed); // DefaultSecretUInt64[14], DefaultSecretUInt64[15] + goto case 2; + case 2: + hash += Mix16Bytes(source + 32, 0xCB00C391BB52283C, 0xA32E531B8B65D088, seed); // DefaultSecretUInt64[8], DefaultSecretUInt64[9] + hash += Mix16Bytes(source + length - 48, 0x4EF90DA297486471, 0xD8ACDEA946EF1938, seed); // DefaultSecretUInt64[10], DefaultSecretUInt64[11] + goto case 1; + case 1: + hash += Mix16Bytes(source + 16, 0x78E5C0CC4EE679CB, 0x2172FFCC7DD05A82, seed); // DefaultSecretUInt64[4], DefaultSecretUInt64[5] + hash += Mix16Bytes(source + length - 32, 0x8E2443F7744608B8, 0x4C263A81E69035E0, seed); // DefaultSecretUInt64[6], DefaultSecretUInt64[7] + goto case 0; + case 0: + hash += Mix16Bytes(source, 0xBE4BA423396CFEB8, 0x1CAD21F72C81017C, seed); // DefaultSecretUInt64[0], DefaultSecretUInt64[1] + hash += Mix16Bytes(source + length - 16, 0xDB979083E96DD4DE, 0x1F67B3B7A4A44072, seed); // DefaultSecretUInt64[2], DefaultSecretUInt64[3] + break; + } + + return Avalanche(hash); + } + + private static ulong HashLength129To240(byte* source, uint length, ulong seed) + { + Debug.Assert(length >= 129 && length <= 240); + + ulong hash = length * XxHash64.Prime64_1; + + hash += Mix16Bytes(source + (16 * 0), 0xBE4BA423396CFEB8, 0x1CAD21F72C81017C, seed); // DefaultSecretUInt64[0], DefaultSecretUInt64[1] + hash += Mix16Bytes(source + (16 * 1), 0xDB979083E96DD4DE, 0x1F67B3B7A4A44072, seed); // DefaultSecretUInt64[2], DefaultSecretUInt64[3] + hash += Mix16Bytes(source + (16 * 2), 0x78E5C0CC4EE679CB, 0x2172FFCC7DD05A82, seed); // DefaultSecretUInt64[4], DefaultSecretUInt64[5] + hash += Mix16Bytes(source + (16 * 3), 0x8E2443F7744608B8, 0x4C263A81E69035E0, seed); // DefaultSecretUInt64[6], DefaultSecretUInt64[7] + hash += Mix16Bytes(source + (16 * 4), 0xCB00C391BB52283C, 0xA32E531B8B65D088, seed); // DefaultSecretUInt64[8], DefaultSecretUInt64[9] + hash += Mix16Bytes(source + (16 * 5), 0x4EF90DA297486471, 0xD8ACDEA946EF1938, seed); // DefaultSecretUInt64[10], DefaultSecretUInt64[11] + hash += Mix16Bytes(source + (16 * 6), 0x3F349CE33F76FAA8, 0x1D4F0BC7C7BBDCF9, seed); // DefaultSecretUInt64[12], DefaultSecretUInt64[13] + hash += Mix16Bytes(source + (16 * 7), 0x3159B4CD4BE0518A, 0x647378D9C97E9FC8, seed); // DefaultSecretUInt64[14], DefaultSecretUInt64[15] + + hash = Avalanche(hash); + + switch ((length - (16 * 8)) / 16) + { + default: // case 7 + Debug.Assert((length - 16 * 8) / 16 == 7); + hash += Mix16Bytes(source + (16 * 14), 0xBBDCF93F349CE33F, 0xE0518A1D4F0BC7C7, seed); // Read(ref DefaultSecret[99]), Read(ref DefaultSecret[107]) + goto case 6; + case 6: + hash += Mix16Bytes(source + (16 * 13), 0xEF19384EF90DA297, 0x76FAA8D8ACDEA946, seed); // Read(ref DefaultSecret[83]), Read(ref DefaultSecret[91]) + goto case 5; + case 5: + hash += Mix16Bytes(source + (16 * 12), 0x65D088CB00C391BB, 0x486471A32E531B8B, seed); // Read(ref DefaultSecret[67]), Read(ref DefaultSecret[75]) + goto case 4; + case 4: + hash += Mix16Bytes(source + (16 * 11), 0x9035E08E2443F774, 0x52283C4C263A81E6, seed); // Read(ref DefaultSecret[51]), Read(ref DefaultSecret[59]) + goto case 3; + case 3: + hash += Mix16Bytes(source + (16 * 10), 0xD05A8278E5C0CC4E, 0x4608B82172FFCC7D, seed); // Read(ref DefaultSecret[35]), Read(ref DefaultSecret[43]) + goto case 2; + case 2: + hash += Mix16Bytes(source + (16 * 9), 0xA44072DB979083E9, 0xE679CB1F67B3B7A4, seed); // Read(ref DefaultSecret[19]), Read(ref DefaultSecret[27]) + goto case 1; + case 1: + hash += Mix16Bytes(source + (16 * 8), 0x81017CBE4BA42339, 0x6DD4DE1CAD21F72C, seed); // Read(ref DefaultSecret[3]), Read(ref DefaultSecret[11]) + goto case 0; + case 0: + break; + } + + // Handle the last 16 bytes. + hash += Mix16Bytes(source + length - 16, 0x7378D9C97E9FC831, 0xEBD33483ACC5EA64, seed); // DefaultSecret[119], DefaultSecret[127] + return Avalanche(hash); + } + + private static ulong HashLengthOver240(byte* source, uint length, ulong seed) + { + Debug.Assert(length > 240); + + fixed (byte* defaultSecret = &MemoryMarshal.GetReference(DefaultSecret)) + { + byte* secret = defaultSecret; + if (seed != 0) + { + byte* customSecret = stackalloc byte[SecretLengthBytes]; + DeriveSecretFromSeed(customSecret, seed); + secret = customSecret; + } + + ulong* accumulators = stackalloc ulong[AccumulatorCount]; + InitializeAccumulators(accumulators); + + const int StripesPerBlock = (SecretLengthBytes - StripeLengthBytes) / SecretConsumeRateBytes; + const int BlockLen = StripeLengthBytes * StripesPerBlock; + int blocksNum = (int)((length - 1) / BlockLen); + + Accumulate(accumulators, source, secret, StripesPerBlock, true, blocksNum); + int offset = BlockLen * blocksNum; + + int stripesNumber = (int)((length - 1 - offset) / StripeLengthBytes); + Accumulate(accumulators, source + offset, secret, stripesNumber); + Accumulate512(accumulators, source + length - StripeLengthBytes, secret + (SecretLengthBytes - StripeLengthBytes - SecretLastAccStartBytes)); + + return MergeAccumulators(accumulators, secret + 11, length * XxHash64.Prime64_1); + } + } + +#if false + private static void ConsumeStripes(ulong* accumulators, ref ulong stripesSoFar, ulong stripesPerBlock, byte* source, ulong stripes, byte* secret) + { + Debug.Assert(stripes <= stripesPerBlock); // can handle max 1 scramble per invocation + Debug.Assert(stripesSoFar < stripesPerBlock); + + ulong stripesToEndOfBlock = stripesPerBlock - stripesSoFar; + if (stripesToEndOfBlock <= stripes) + { + // need a scrambling operation + ulong stripesAfterBlock = stripes - stripesToEndOfBlock; + Accumulate(accumulators, source, secret + ((int)stripesSoFar * SecretConsumeRateBytes), (int)stripesToEndOfBlock); + ScrambleAccumulators(accumulators, secret + (SecretLengthBytes - StripeLengthBytes)); + Accumulate(accumulators, source + ((int)stripesToEndOfBlock * StripeLengthBytes), secret, (int)stripesAfterBlock); + stripesSoFar = stripesAfterBlock; + } + else + { + Accumulate(accumulators, source, secret + ((int)stripesSoFar * SecretConsumeRateBytes), (int)stripes); + stripesSoFar += stripes; + } + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InitializeAccumulators(ulong* accumulators) + { +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) + { + Vector256.Store(Vector256.Create(XxHash32.Prime32_3, XxHash64.Prime64_1, XxHash64.Prime64_2, XxHash64.Prime64_3), accumulators); + Vector256.Store(Vector256.Create(XxHash64.Prime64_4, XxHash32.Prime32_2, XxHash64.Prime64_5, XxHash32.Prime32_1), accumulators + 4); + } + else if (Vector128.IsHardwareAccelerated) + { + Vector128.Store(Vector128.Create(XxHash32.Prime32_3, XxHash64.Prime64_1), accumulators); + Vector128.Store(Vector128.Create(XxHash64.Prime64_2, XxHash64.Prime64_3), accumulators + 2); + Vector128.Store(Vector128.Create(XxHash64.Prime64_4, XxHash32.Prime32_2), accumulators + 4); + Vector128.Store(Vector128.Create(XxHash64.Prime64_5, XxHash32.Prime32_1), accumulators + 6); + } + else +#endif + { + accumulators[0] = XxHash32.Prime32_3; + accumulators[1] = XxHash64.Prime64_1; + accumulators[2] = XxHash64.Prime64_2; + accumulators[3] = XxHash64.Prime64_3; + accumulators[4] = XxHash64.Prime64_4; + accumulators[5] = XxHash32.Prime32_2; + accumulators[6] = XxHash64.Prime64_5; + accumulators[7] = XxHash32.Prime32_1; + } + } + + private static ulong MergeAccumulators(ulong* accumulators, byte* secret, ulong start) + { + ulong result64 = start; + + result64 += Multiply64To128ThenFold(accumulators[0] ^ ReadUInt64LE(secret), accumulators[1] ^ ReadUInt64LE(secret + 8)); + result64 += Multiply64To128ThenFold(accumulators[2] ^ ReadUInt64LE(secret + 16), accumulators[3] ^ ReadUInt64LE(secret + 24)); + result64 += Multiply64To128ThenFold(accumulators[4] ^ ReadUInt64LE(secret + 32), accumulators[5] ^ ReadUInt64LE(secret + 40)); + result64 += Multiply64To128ThenFold(accumulators[6] ^ ReadUInt64LE(secret + 48), accumulators[7] ^ ReadUInt64LE(secret + 56)); + + return Avalanche(result64); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Mix16Bytes(byte* source, ulong secretLow, ulong secretHigh, ulong seed) => + Multiply64To128ThenFold( + ReadUInt64LE(source) ^ (secretLow + seed), + ReadUInt64LE(source + sizeof(ulong)) ^ (secretHigh - seed)); + + /// Calculates a 32-bit to 64-bit long multiply. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Multiply32To64(ulong v1, ulong v2) => (uint)v1 * (ulong)(uint)v2; + + /// "This is a fast avalanche stage, suitable when input bits are already partially mixed." + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Avalanche(ulong hash) + { + hash = XorShift(hash, 37); + hash *= 0x165667919E3779F9; + hash = XorShift(hash, 32); + return hash; + } + + /// Calculates a 64-bit to 128-bit multiply, then XOR folds it. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong Multiply64To128ThenFold(ulong left, ulong right) + { +#if NET5_0_OR_GREATER + ulong upper = Math.BigMul(left, right, out ulong lower); +#else + ulong lowerLow = Multiply32To64(left & 0xFFFFFFFF, right & 0xFFFFFFFF); + ulong higherLow = Multiply32To64(left >> 32, right & 0xFFFFFFFF); + ulong lowerHigh = Multiply32To64(left & 0xFFFFFFFF, right >> 32); + ulong higherHigh = Multiply32To64(left >> 32, right >> 32); + + ulong cross = (lowerLow >> 32) + (higherLow & 0xFFFFFFFF) + lowerHigh; + ulong upper = (higherLow >> 32) + (cross >> 32) + higherHigh; + ulong lower = (cross << 32) | (lowerLow & 0xFFFFFFFF); +#endif + return lower ^ upper; + } + + private static void DeriveSecretFromSeed(byte* destinationSecret, ulong seed) + { + fixed (byte* defaultSecret = &MemoryMarshal.GetReference(DefaultSecret)) + { +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated && BitConverter.IsLittleEndian) + { + Vector256 seedVec = Vector256.Create(seed, 0u - seed, seed, 0u - seed); + for (int i = 0; i < SecretLengthBytes; i += Vector256.Count) + { + Vector256.Store(Vector256.Load((ulong*)(defaultSecret + i)) + seedVec, (ulong*)(destinationSecret + i)); + } + } + else if (Vector128.IsHardwareAccelerated && BitConverter.IsLittleEndian) + { + Vector128 seedVec = Vector128.Create(seed, 0u - seed); + for (int i = 0; i < SecretLengthBytes; i += Vector128.Count) + { + Vector128.Store(Vector128.Load((ulong*)(defaultSecret + i)) + seedVec, (ulong*)(destinationSecret + i)); + } + } + else +#endif + { + for (int i = 0; i < SecretLengthBytes; i += sizeof(ulong) * 2) + { + WriteUInt64LE(destinationSecret + i, ReadUInt64LE(defaultSecret + i) + seed); + WriteUInt64LE(destinationSecret + i + 8, ReadUInt64LE(defaultSecret + i + 8) - seed); + } + } + } + } + + /// Optimized version of looping over . + [MethodImpl(MethodImplOptions.NoInlining)] + private static void Accumulate(ulong* accumulators, byte* source, byte* secret, int stripesToProcess, bool scramble = false, int blockCount = 1) + { + byte* secretForAccumulate = secret; + byte* secretForScramble = secret + (SecretLengthBytes - StripeLengthBytes); + +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated && BitConverter.IsLittleEndian) + { + Vector256 acc1 = Vector256.Load(accumulators); + Vector256 acc2 = Vector256.Load(accumulators + Vector256.Count); + + for (int j = 0; j < blockCount; j++) + { + secret = secretForAccumulate; + for (int i = 0; i < stripesToProcess; i++) + { + Vector256 secretVal = Vector256.Load((uint*)secret); + acc1 = Accumulate256(acc1, source, secretVal); + source += Vector256.Count; + + secretVal = Vector256.Load((uint*)secret + Vector256.Count); + acc2 = Accumulate256(acc2, source, secretVal); + source += Vector256.Count; + + secret += SecretConsumeRateBytes; + } + + if (scramble) + { + acc1 = ScrambleAccumulator256(acc1, Vector256.Load((ulong*)secretForScramble)); + acc2 = ScrambleAccumulator256(acc2, Vector256.Load((ulong*)secretForScramble + Vector256.Count)); + } + } + + Vector256.Store(acc1, accumulators); + Vector256.Store(acc2, accumulators + Vector256.Count); + } + else if (Vector128.IsHardwareAccelerated && BitConverter.IsLittleEndian) + { + Vector128 acc1 = Vector128.Load(accumulators); + Vector128 acc2 = Vector128.Load(accumulators + Vector128.Count); + Vector128 acc3 = Vector128.Load(accumulators + Vector128.Count * 2); + Vector128 acc4 = Vector128.Load(accumulators + Vector128.Count * 3); + + for (int j = 0; j < blockCount; j++) + { + secret = secretForAccumulate; + for (int i = 0; i < stripesToProcess; i++) + { + Vector128 secretVal = Vector128.Load((uint*)secret); + acc1 = Accumulate128(acc1, source, secretVal); + source += Vector128.Count; + + secretVal = Vector128.Load((uint*)secret + Vector128.Count); + acc2 = Accumulate128(acc2, source, secretVal); + source += Vector128.Count; + + secretVal = Vector128.Load((uint*)secret + Vector128.Count * 2); + acc3 = Accumulate128(acc3, source, secretVal); + source += Vector128.Count; + + secretVal = Vector128.Load((uint*)secret + Vector128.Count * 3); + acc4 = Accumulate128(acc4, source, secretVal); + source += Vector128.Count; + + secret += SecretConsumeRateBytes; + } + + if (scramble) + { + acc1 = ScrambleAccumulator128(acc1, Vector128.Load((ulong*)secretForScramble)); + acc2 = ScrambleAccumulator128(acc2, Vector128.Load((ulong*)secretForScramble + Vector128.Count)); + acc3 = ScrambleAccumulator128(acc3, Vector128.Load((ulong*)secretForScramble + Vector128.Count * 2)); + acc4 = ScrambleAccumulator128(acc4, Vector128.Load((ulong*)secretForScramble + Vector128.Count * 3)); + } + } + + Vector128.Store(acc1, accumulators); + Vector128.Store(acc2, accumulators + Vector128.Count); + Vector128.Store(acc3, accumulators + Vector128.Count * 2); + Vector128.Store(acc4, accumulators + Vector128.Count * 3); + } + else +#endif + { + for (int j = 0; j < blockCount; j++) + { + for (int i = 0; i < stripesToProcess; i++) + { + Accumulate512Inlined(accumulators, source, secret + (i * SecretConsumeRateBytes)); + source += StripeLengthBytes; + } + + if (scramble) + { + ScrambleAccumulators(accumulators, secretForScramble); + } + } + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void Accumulate512(ulong* accumulators, byte* source, byte* secret) + { + Accumulate512Inlined(accumulators, source, secret); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Accumulate512Inlined(ulong* accumulators, byte* source, byte* secret) + { +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated && BitConverter.IsLittleEndian) + { + for (int i = 0; i < AccumulatorCount / Vector256.Count; i++) + { + Vector256 accVec = Accumulate256(Vector256.Load(accumulators), source, Vector256.Load((uint*)secret)); + Vector256.Store(accVec, accumulators); + + accumulators += Vector256.Count; + secret += Vector256.Count; + source += Vector256.Count; + } + } + else if (Vector128.IsHardwareAccelerated && BitConverter.IsLittleEndian) + { + for (int i = 0; i < AccumulatorCount / Vector128.Count; i++) + { + Vector128 accVec = Accumulate128(Vector128.Load(accumulators), source, Vector128.Load((uint*)secret)); + Vector128.Store(accVec, accumulators); + + accumulators += Vector128.Count; + secret += Vector128.Count; + source += Vector128.Count; + } + } + else +#endif + { + for (int i = 0; i < AccumulatorCount; i++) + { + ulong sourceVal = ReadUInt64LE(source + (8 * i)); + ulong sourceKey = sourceVal ^ ReadUInt64LE(secret + (i * 8)); + + accumulators[i ^ 1] += sourceVal; // swap adjacent lanes + accumulators[i] += Multiply32To64(sourceKey & 0xFFFFFFFF, sourceKey >> 32); + } + } + } + +#if NET7_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Accumulate256(Vector256 accVec, byte* source, Vector256 secret) + { + Vector256 sourceVec = Vector256.Load((uint*)source); + Vector256 sourceKey = sourceVec ^ secret; + + // TODO: Figure out how to unwind this shuffle and just use Vector256.Multiply + Vector256 sourceKeyLow = Vector256.Shuffle(sourceKey, Vector256.Create(1u, 0, 3, 0, 5, 0, 7, 0)); + Vector256 sourceSwap = Vector256.Shuffle(sourceVec, Vector256.Create(2u, 3, 0, 1, 6, 7, 4, 5)); + Vector256 sum = accVec + sourceSwap.AsUInt64(); + Vector256 product = Avx2.IsSupported ? + Avx2.Multiply(sourceKey, sourceKeyLow) : + (sourceKey & Vector256.Create(~0u, 0u, ~0u, 0u, ~0u, 0u, ~0u, 0u)).AsUInt64() * (sourceKeyLow & Vector256.Create(~0u, 0u, ~0u, 0u, ~0u, 0u, ~0u, 0u)).AsUInt64(); + + accVec = product + sum; + return accVec; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Accumulate128(Vector128 accVec, byte* source, Vector128 secret) + { + Vector128 sourceVec = Vector128.Load((uint*)source); + Vector128 sourceKey = sourceVec ^ secret; + + // TODO: Figure out how to unwind this shuffle and just use Vector128.Multiply + Vector128 sourceSwap = Vector128.Shuffle(sourceVec, Vector128.Create(2u, 3, 0, 1)); + Vector128 sum = accVec + sourceSwap.AsUInt64(); + + Vector128 product = MultiplyWideningLower(sourceKey); + accVec = product + sum; + return accVec; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 MultiplyWideningLower(Vector128 source) + { + if (AdvSimd.IsSupported) + { + Vector64 sourceLow = Vector128.Shuffle(source, Vector128.Create(0u, 2, 0, 0)).GetLower(); + Vector64 sourceHigh = Vector128.Shuffle(source, Vector128.Create(1u, 3, 0, 0)).GetLower(); + return AdvSimd.MultiplyWideningLower(sourceLow, sourceHigh); + } + else + { + Vector128 sourceLow = Vector128.Shuffle(source, Vector128.Create(1u, 0, 3, 0)); + return Sse2.IsSupported ? + Sse2.Multiply(source, sourceLow) : + (source & Vector128.Create(~0u, 0u, ~0u, 0u)).AsUInt64() * (sourceLow & Vector128.Create(~0u, 0u, ~0u, 0u)).AsUInt64(); + } + } +#endif + + private static void ScrambleAccumulators(ulong* accumulators, byte* secret) + { +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated && BitConverter.IsLittleEndian) + { + for (int i = 0; i < AccumulatorCount / Vector256.Count; i++) + { + Vector256 accVec = ScrambleAccumulator256(Vector256.Load(accumulators), Vector256.Load((ulong*)secret)); + Vector256.Store(accVec, accumulators); + + accumulators += Vector256.Count; + secret += Vector256.Count; + } + } + else if (Vector128.IsHardwareAccelerated && BitConverter.IsLittleEndian) + { + for (int i = 0; i < AccumulatorCount / Vector128.Count; i++) + { + Vector128 accVec = ScrambleAccumulator128(Vector128.Load(accumulators), Vector128.Load((ulong*)secret)); + Vector128.Store(accVec, accumulators); + + accumulators += Vector128.Count; + secret += Vector128.Count; + } + } + else +#endif + { + for (int i = 0; i < AccumulatorCount; i++) + { + ulong xorShift = XorShift(*accumulators, 47); + ulong xorWithKey = xorShift ^ ReadUInt64LE(secret); + *accumulators = xorWithKey * XxHash32.Prime32_1; + + accumulators++; + secret += sizeof(ulong); + } + } + } + +#if NET7_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ScrambleAccumulator256(Vector256 accVec, Vector256 secret) + { + Vector256 xorShift = accVec ^ Vector256.ShiftRightLogical(accVec, 47); + Vector256 xorWithKey = xorShift ^ secret; + accVec = xorWithKey * Vector256.Create((ulong)XxHash32.Prime32_1); + return accVec; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ScrambleAccumulator128(Vector128 accVec, Vector128 secret) + { + Vector128 xorShift = accVec ^ Vector128.ShiftRightLogical(accVec, 47); + Vector128 xorWithKey = xorShift ^ secret; + accVec = xorWithKey * Vector128.Create((ulong)XxHash32.Prime32_1); + return accVec; + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong XorShift(ulong value, int shift) + { + Debug.Assert(shift >= 0 && shift < 64); + return value ^ (value >> shift); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint ReadUInt32LE(byte* data) => + BitConverter.IsLittleEndian ? + Unsafe.ReadUnaligned(data) : + BinaryPrimitives.ReverseEndianness(Unsafe.ReadUnaligned(data)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong ReadUInt64LE(byte* data) => + BitConverter.IsLittleEndian ? + Unsafe.ReadUnaligned(data) : + BinaryPrimitives.ReverseEndianness(Unsafe.ReadUnaligned(data)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteUInt64LE(byte* data, ulong value) + { + if (!BitConverter.IsLittleEndian) + { + value = BinaryPrimitives.ReverseEndianness(value); + } + Unsafe.WriteUnaligned(data, value); + } +#if false + [StructLayout(LayoutKind.Auto)] + private struct State + { + /// The accumulators. Length is . + internal fixed ulong Accumulators[AccumulatorCount]; + + /// Used to store a custom secret generated from a seed. Length is . + internal fixed byte Secret[SecretLengthBytes]; + + /// The internal buffer. Length is . + internal fixed byte Buffer[InternalBufferLengthBytes]; + + /// The amount of memory in . + internal uint BufferedCount; + + /// Number of stripes processed in the current block. + internal ulong StripesProcessedInCurrentBlock; + + /// Total length hashed. + internal ulong TotalLength; + + /// The seed employed (possibly 0). + internal ulong Seed; + }; +#endif + } +} diff --git a/src/LegacySupport/xxH3/XxHash32.State.cs b/src/LegacySupport/xxH3/XxHash32.State.cs new file mode 100644 index 0000000000..222ab4ba6f --- /dev/null +++ b/src/LegacySupport/xxH3/XxHash32.State.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable SA1310 +#pragma warning disable S2148 + +namespace System.IO.Hashing; + +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +internal static class XxHash32 +{ + internal const uint Prime32_1 = 0x9E3779B1; + internal const uint Prime32_2 = 0x85EBCA77; + internal const uint Prime32_3 = 0xC2B2AE3D; + internal const uint Prime32_4 = 0x27D4EB2F; + internal const uint Prime32_5 = 0x165667B1; +} + diff --git a/src/LegacySupport/xxH3/XxHash64.State.cs b/src/LegacySupport/xxH3/XxHash64.State.cs new file mode 100644 index 0000000000..cc0d0bc7e0 --- /dev/null +++ b/src/LegacySupport/xxH3/XxHash64.State.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable SA1310 +#pragma warning disable S2148 +#pragma warning disable S109 + +using System.Runtime.CompilerServices; + +namespace System.IO.Hashing; + +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +internal static class XxHash64 +{ + internal const ulong Prime64_1 = 0x9E3779B185EBCA87; + internal const ulong Prime64_2 = 0xC2B2AE3D27D4EB4F; + internal const ulong Prime64_3 = 0x165667B19E3779F9; + internal const ulong Prime64_4 = 0x85EBCA77C2B2AE63; + internal const ulong Prime64_5 = 0x27D4EB2F165667C5; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ulong Avalanche(ulong hash) + { + hash ^= hash >> 33; + hash *= Prime64_2; + hash ^= hash >> 29; + hash *= Prime64_3; + hash ^= hash >> 32; + return hash; + } +} diff --git a/src/Libraries/Directory.Build.props b/src/Libraries/Directory.Build.props new file mode 100644 index 0000000000..3d0c4d6723 --- /dev/null +++ b/src/Libraries/Directory.Build.props @@ -0,0 +1,16 @@ + + + + + $(NetCoreTargetFrameworks)$(ConditionalNet462) + true + + true + true + true + true + true + true + true + + diff --git a/src/Libraries/Microsoft.AspNetCore.AsyncState/AsyncContextHttpContext.cs b/src/Libraries/Microsoft.AspNetCore.AsyncState/AsyncContextHttpContext.cs new file mode 100644 index 0000000000..99c908d126 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.AsyncState/AsyncContextHttpContext.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.AsyncState; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.AspNetCore.AsyncState; + +internal sealed class AsyncContextHttpContext : IAsyncContext + where T : class +{ + private readonly IAsyncLocalContext _localContext; + private readonly IHttpContextAccessor _httpContextAccessor; + + public AsyncContextHttpContext( + IAsyncLocalContext localContext, + IHttpContextAccessor httpContextAccessor) + { + _localContext = localContext; + _httpContextAccessor = httpContextAccessor; + } + + public T? Get() + { + if (!TryGet(out var value)) + { + Throw.InvalidOperationException("Async context not available"); + } + + return value; + } + + public void Set(T? value) + { + var httpContext = _httpContextAccessor.HttpContext; + if (httpContext == null) + { + // the call is made outside of an HTTP context + _localContext.Set(value); + return; + } + + httpContext.Features[typeof(TypeWrapper)] = value; + } + + public bool TryGet(out T? value) + { + var httpContext = _httpContextAccessor.HttpContext; + if (httpContext == null) + { + // the call is made outside of an HTTP context + return _localContext.TryGet(out value); + } + + value = (T?)httpContext.Features[typeof(TypeWrapper)]; + return true; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.AsyncState/AsyncStateHttpContextExtensions.cs b/src/Libraries/Microsoft.AspNetCore.AsyncState/AsyncStateHttpContextExtensions.cs new file mode 100644 index 0000000000..dfe79fe290 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.AsyncState/AsyncStateHttpContextExtensions.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.AsyncState; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.AspNetCore.AsyncState; + +/// +/// Extension methods to add the async state feature with the HttpContext lifetime to a dependency injection container. +/// +public static class AsyncStateHttpContextExtensions +{ + /// + /// Adds default implementations for , , and services, + /// scoped to the lifetime of instances. + /// + /// The to add the service to. + /// The value of . + /// If is . + public static IServiceCollection AddAsyncStateHttpContext(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + services + .AddHttpContextAccessor() + .AddAsyncStateCore() + .TryRemoveAsyncStateCore() + .TryAddSingleton(typeof(IAsyncContext<>), typeof(AsyncContextHttpContext<>)); + + return services; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.AsyncState/Microsoft.AspNetCore.AsyncState.csproj b/src/Libraries/Microsoft.AspNetCore.AsyncState/Microsoft.AspNetCore.AsyncState.csproj new file mode 100644 index 0000000000..57556dcd18 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.AsyncState/Microsoft.AspNetCore.AsyncState.csproj @@ -0,0 +1,30 @@ + + + Microsoft.AspNetCore.AsyncState + ASP.NET initializer for async state. + $(PackageTags);aspnetcore + Fundamentals + + + + normal + 100 + 100 + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.AspNetCore.AsyncState/TypeWrapper.cs b/src/Libraries/Microsoft.AspNetCore.AsyncState/TypeWrapper.cs new file mode 100644 index 0000000000..83bd97fb51 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.AsyncState/TypeWrapper.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.AsyncState; + +/// +/// We use this generic type to store values into . +/// Instead of storing the value under it's type T, we use the type . +/// Even if T is publicly available type and another value was stored into +/// under type T (by the application or another library), +/// this other value will not conflict with the one stored under . +/// Note that is not public, so nobody else can use it. +/// +/// The type of the value to store into . +#pragma warning disable S1694 // Convert this 'abstract' class to a concrete type with protected constructor. +internal abstract class TypeWrapper +#pragma warning restore S1694 // Convert this 'abstract' class to a concrete type with protected constructor. +{ +} diff --git a/src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/ConnectionTimeoutDelegate.cs b/src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/ConnectionTimeoutDelegate.cs new file mode 100644 index 0000000000..b699c63a30 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/ConnectionTimeoutDelegate.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.AspNetCore.Connections; + +internal sealed class ConnectionTimeoutDelegate +{ + internal TimeProvider TimeProvider = TimeProvider.System; + + private readonly ConnectionDelegate _next; + private readonly TimeSpan _timeout; + + public ConnectionTimeoutDelegate(ConnectionDelegate next, IOptions options) + { + _next = next; + _timeout = options.Value.Timeout; + } + + public async Task OnConnectionAsync(ConnectionContext context) + { + var connectionLifetimeNotification = context.Features.Get(); + if (connectionLifetimeNotification == null) + { + Throw.InvalidOperationException("IConnectionLifetimeNotificationFeature hasn't been registered."); + } + + var delayTask = TimeProvider.Delay(_timeout, CancellationToken.None); + var next = _next(context); + + var completedTask = await Task.WhenAny(next, delayTask).ConfigureAwait(false); + + if (completedTask == delayTask) + { + connectionLifetimeNotification.RequestClose(); + } + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/ConnectionTimeoutExtensions.cs b/src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/ConnectionTimeoutExtensions.cs new file mode 100644 index 0000000000..57a3fa48ef --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/ConnectionTimeoutExtensions.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.AspNetCore.Connections; + +/// +/// Extensions used to register the connection timeout middleware. +/// +public static class ConnectionTimeoutExtensions +{ + /// + /// Add the connection timeout middleware. + /// + /// The server options to use. + /// The value of . + public static ListenOptions UseConnectionTimeout(this ListenOptions listenOptions) + { + _ = Throw.IfNull(listenOptions); + + _ = listenOptions.Use(next => + { + var connectionTimeoutOptions = listenOptions.ApplicationServices.GetRequiredService>(); + return new ConnectionTimeoutDelegate(next, connectionTimeoutOptions).OnConnectionAsync; + }); + + return listenOptions; + } + + /// + /// Adds option handling for the connection timeout middleware. + /// + /// The dependency injection container to add the service to. + /// Delegate to configure the timeout options. + /// The value of . + public static IServiceCollection AddConnectionTimeout(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + _ = services + .AddValidatedOptions() + .Configure(configure); + + return services; + } + + /// + /// Adds option handling for the connection timeout middleware. + /// + /// The dependency injection container to add the service to. + /// The configuration section used to configure the feature. + /// The value of . + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(ConnectionTimeoutOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed with [DynamicDependency]")] + public static IServiceCollection AddConnectionTimeout(this IServiceCollection services, IConfigurationSection section) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(section); + + _ = services + .AddValidatedOptions() + .Bind(section); + + return services; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/ConnectionTimeoutOptions.cs b/src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/ConnectionTimeoutOptions.cs new file mode 100644 index 0000000000..f6333eedd7 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/ConnectionTimeoutOptions.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Data.Validation; + +namespace Microsoft.AspNetCore.Connections; + +/// +/// Options to configure the connection timeout middleware. +/// +public class ConnectionTimeoutOptions +{ + private static readonly TimeSpan _defaultTimeout = TimeSpan.FromMinutes(5); + + /// + /// Gets or sets the time after which a connection will be shut down. + /// + /// + /// Default set to 5 minutes. + /// + [TimeSpan(0, Exclusive = true)] + public TimeSpan Timeout { get; set; } = _defaultTimeout; +} diff --git a/src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/ConnectionTimeoutValidator.cs b/src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/ConnectionTimeoutValidator.cs new file mode 100644 index 0000000000..1323832b75 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/ConnectionTimeoutValidator.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Options; +using Validation = Microsoft.Extensions.Options.Validation; + +namespace Microsoft.AspNetCore.Connections; + +internal sealed class ConnectionTimeoutValidator : IValidateOptions +{ + /// + /// Minimum possible timeout. + /// + private static readonly TimeSpan _minimumTimeout = TimeSpan.FromSeconds(1); + + /// + /// Maximum possible timeout. + /// + private static readonly TimeSpan _maximumTimeout = TimeSpan.FromHours(1); + + public ValidateOptionsResult Validate(string? name, ConnectionTimeoutOptions options) + { + var builder = new ValidateOptionsResultBuilder(); + + if (options.Timeout < _minimumTimeout || options.Timeout > _maximumTimeout) + { + builder.AddError( + "must be in the range [{_minimumTimeout}..{_maximumTimeout}].", + nameof(options.Timeout)); + } + + return builder.Build(); + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/Microsoft.AspNetCore.ConnectionTimeout.csproj b/src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/Microsoft.AspNetCore.ConnectionTimeout.csproj new file mode 100644 index 0000000000..e9d691fe35 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.ConnectionTimeout/Microsoft.AspNetCore.ConnectionTimeout.csproj @@ -0,0 +1,37 @@ + + + Microsoft.AspNetCore.Connections + Mechanism to trigger network connection timeouts to force clients to reestablisih fresh connections. + $(PackageTags);aspnetcore + Fundamentals + + + + $(NetCoreTargetFrameworks) + true + true + + + + dev + 82 + 100 + + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/CommonHeaders.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/CommonHeaders.cs new file mode 100644 index 0000000000..1cf3a99f40 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/CommonHeaders.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net; +using Microsoft.AspNetCore.HeaderParsing.Parsers; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.HeaderParsing; + +/// +/// Common header setups. +/// +public static class CommonHeaders +{ + /// + /// Gets Host header setup. + /// + public static HeaderSetup Host => new(HeaderNames.Host, HostHeaderValueParser.Instance); + + /// + /// Gets Accept header setup. + /// + public static HeaderSetup> Accept => new(HeaderNames.Accept, MediaTypeHeaderValueListParser.Instance); + + /// + /// Gets AcceptEncoding header setup. + /// + public static HeaderSetup> AcceptEncoding => new(HeaderNames.AcceptEncoding, StringWithQualityHeaderValueListParser.Instance, cacheable: true); + + /// + /// Gets AcceptLanguage header setup. + /// + public static HeaderSetup> AcceptLanguage => new(HeaderNames.AcceptLanguage, StringWithQualityHeaderValueListParser.Instance, cacheable: true); + + /// + /// Gets CacheControl header setup. + /// + public static HeaderSetup CacheControl => new(HeaderNames.CacheControl, CacheControlHeaderValueParser.Instance, cacheable: true); + + /// + /// Gets ContentDisposition header setup. + /// + public static HeaderSetup ContentDisposition => new(HeaderNames.ContentDisposition, ContentDispositionHeaderValueParser.Instance, cacheable: true); + + /// + /// Gets ContentType header setup. + /// + public static HeaderSetup ContentType => new(HeaderNames.ContentType, MediaTypeHeaderValueParser.Instance, cacheable: true); + + /// + /// Gets Cookie header setup. + /// + public static HeaderSetup> Cookie => new(HeaderNames.Cookie, CookieHeaderValueListParser.Instance); + + /// + /// Gets Date header setup. + /// + public static HeaderSetup Date => new(HeaderNames.Date, DateTimeOffsetParser.Instance); + + /// + /// Gets IfMatch header setup. + /// + public static HeaderSetup> IfMatch => new(HeaderNames.IfMatch, EntityTagHeaderValueListParser.Instance); + + /// + /// Gets IfModifiedSince header setup. + /// + public static HeaderSetup> IfModifiedSince => new(HeaderNames.IfModifiedSince, EntityTagHeaderValueListParser.Instance); + + /// + /// Gets IfNoneMatch header setup. + /// + public static HeaderSetup> IfNoneMatch => new(HeaderNames.IfNoneMatch, EntityTagHeaderValueListParser.Instance); + + /// + /// Gets IfRange header setup. + /// + public static HeaderSetup IfRange => new(HeaderNames.IfRange, RangeConditionHeaderValueParser.Instance); + + /// + /// Gets IfUnmodifiedSince header setup. + /// + public static HeaderSetup IfUnmodifiedSince => new(HeaderNames.IfUnmodifiedSince, DateTimeOffsetParser.Instance); + + /// + /// Gets Range header setup. + /// + public static HeaderSetup Range => new(HeaderNames.Range, RangeHeaderValueParser.Instance); + + /// + /// Gets Referer header setup. + /// + public static HeaderSetup Referer => new(HeaderNames.Referer, Parsers.UriParser.Instance, cacheable: true); + + /// + /// Gets XForwardedFor header setup. + /// + public static HeaderSetup> XForwardedFor => new("X-Forwarded-For", IPAddressListParser.Instance); +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderKey.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderKey.cs new file mode 100644 index 0000000000..10e33c1697 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderKey.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.HeaderParsing; + +/// +/// Used to indicate which header to parse. +/// +/// The type of the header value. +[SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "Header keys are never disposed, so we don't bother with this.")] +[SuppressMessage("Blocker Bug", "S2931:Classes with \"IDisposable\" members should implement \"IDisposable\"", Justification = "Header keys are never disposed, so we don't bother with this.")] +public sealed class HeaderKey + where T : notnull +{ + /// + /// Gets the name of the header. + /// + public string Name { get; } + + internal bool HasDefaultValue { get; } + internal T? DefaultValue { get; } + internal int Position { get; } + internal HeaderParser Parser { get; } + private readonly IMemoryCache? _valueCache; + + internal HeaderKey(string name, HeaderParser parser, int position, int maxCachedValues = 0) + { + Name = name; + Position = position; + Parser = parser; + + if (maxCachedValues > 0) + { + var o = new MemoryCacheOptions + { + SizeLimit = maxCachedValues, + }; + + _valueCache = new MemoryCache(Options.Create(o)); + } + } + + internal HeaderKey(string name, HeaderParser parser, int position, int maxCachedValues, T defaultValue) + : this(name, parser, position, maxCachedValues) + { + DefaultValue = defaultValue; + HasDefaultValue = true; + } + + /// + /// Returns a string representing this instance. + /// + /// + /// The name of this instance. + /// + public override string ToString() => Name; + + private static readonly MemoryCacheEntryOptions _cacheEntryOptions = new() + { + Size = 1, + }; + + internal bool TryParse(StringValues values, out T? result, out string? error) => Parser.TryParse(values, out result, out error); + internal void AddCachedValue(StringValues values, object o) => _valueCache!.Set(values, o, _cacheEntryOptions); + internal object? GetCachedValue(StringValues values) => _valueCache!.Get(values); + internal bool Cacheable => _valueCache != null; +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParser.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParser.cs new file mode 100644 index 0000000000..34e78e3b37 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParser.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.HeaderParsing; + +/// +/// Parses raw header value to a header type. +/// +/// The resulting strong type representing the header's value. +[SuppressMessage("Minor Code Smell", "S1694:An abstract class should have both abstract and concrete methods", Justification = "Want abstract class for extensibility and perf")] +public abstract class HeaderParser + where T : notnull +{ + /// + /// Parses a raw header value to a strong type. + /// + /// The original value. + /// A resulting value. + /// An error if parsing failed. + /// Parsing result. + [SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "There is no such keyword in C#.")] + public abstract bool TryParse(StringValues values, [NotNullWhen(true)] out T? result, [NotNullWhen(false)] out string? error); +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingExtensions.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingExtensions.cs new file mode 100644 index 0000000000..db68c47e28 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingExtensions.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Pools; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.AspNetCore.HeaderParsing; + +/// +/// Extensions exposing HeaderParsing feature. +/// +public static class HeaderParsingExtensions +{ + /// + /// Adds header parsing feature. + /// + /// The to add the services to. + /// The same instance for chaining. + public static IServiceCollection AddHeaderParsing(this IServiceCollection services) + { + if (!Throw.IfNull(services).Any(x => x.ServiceType == typeof(HeaderParsingFeature.PoolHelper))) + { + _ = services + .AddPool() + .AddSingleton() + .AddSingleton() + .AddScoped(provider => provider.GetRequiredService>().Get()) + .AddScoped(provider => provider.GetRequiredService().Feature) + .RegisterMetering(); + } + + return services; + } + + /// + /// Adds header parsing feature. + /// + /// The to add the services to. + /// A delegate to setup parsing for the header. + /// The same instance for chaining. + public static IServiceCollection AddHeaderParsing(this IServiceCollection services, Action configuration) + { + _ = Throw.IfNull(services); + + _ = services.AddValidatedOptions(); + _ = services.AddValidatedOptions(); + + return services + .AddHeaderParsing() + .Configure(configuration); + } + + /// + /// Adds header parsing feature. + /// + /// The to add the services to. + /// A configuration section. + /// The same instance for chaining. + public static IServiceCollection AddHeaderParsing(this IServiceCollection services, IConfigurationSection section) + { + _ = Throw.IfNull(services); + + _ = services + .AddValidatedOptions() + .Bind(section); + + _ = services + .AddValidatedOptions() + .Bind(section); + + return services + .AddHeaderParsing(); + } + + /// + /// Gets the header parsing feature to access parsed header values. + /// + /// The instance. + /// The to access parsed header values. + public static HeaderParsingFeature GetHeaderParsing(this HttpRequest request) + { + var context = Throw.IfNull(request).HttpContext; + + var feature = context.Features.Get(); + + if (feature is null) + { + feature = context.RequestServices.GetRequiredService(); + feature.Context = context; + context.Features.Set(feature); + } + + return feature; + } + + /// + /// Tries to get a header value if it exists and can be parsed. + /// + /// The type of the header value. + /// The instance. + /// The header to parse. + /// A resulting value. + /// if the header value was successfully fetched parsed. + public static bool TryGetHeaderValue(this HttpRequest request, HeaderKey header, [NotNullWhen(true)] out T? value) + where T : notnull + { + return Throw.IfNull(request) + .GetHeaderParsing() + .TryGetHeaderValue(Throw.IfNull(header), out value); + } + + /// + /// Tries to get a header value if it exists and can be parsed. + /// + /// The type of the header value. + /// The instance. + /// The header to parse. + /// A resulting value. + /// Details on the parsing operation. + /// if the header value was successfully fetched parsed. + public static bool TryGetHeaderValue(this HttpRequest request, HeaderKey header, [NotNullWhen(true)] out T? value, out ParsingResult result) + where T : notnull + { + return Throw.IfNull(request) + .GetHeaderParsing() + .TryGetHeaderValue(Throw.IfNull(header), out value, out result); + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingFeature.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingFeature.cs new file mode 100644 index 0000000000..bb1f65bcf8 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingFeature.cs @@ -0,0 +1,208 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Telemetry.Logging; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.AspNetCore.HeaderParsing; + +/// +/// Keeps header parsing state and provides parsing features. +/// +public sealed partial class HeaderParsingFeature +{ + private readonly IHeaderRegistry _registry; + private readonly ILogger _logger; + private readonly ParsingErrorCounter _parsingErrorCounter; + private readonly CacheAccessCounter _cacheAccessCounter; + + // This is a heterogeneous array of Box instances. These boxes let us keep different Ts + // in a single array, while preventing boxing of the T. The boxes are allocated up front + // and are reused over time, avoid further allocations. Clever, eh? + private Box?[] _boxes = Array.Empty(); + + internal HttpContext? Context { get; set; } + + internal HeaderParsingFeature(IHeaderRegistry registry, ILogger logger, Meter meter) + { + _logger = logger; + _registry = registry; + _parsingErrorCounter = Metric.CreateParsingErrorCounter(meter); + _cacheAccessCounter = Metric.CreateCacheAccessCounter(meter); + } + + /// + /// Tries to get a header value if it exists and can be parsed. + /// + /// The type of the header value. + /// The header to parse. + /// A resulting value. + /// if the header value was successfully fetched parsed. + public bool TryGetHeaderValue(HeaderKey header, [NotNullWhen(true)] out T? value) + where T : notnull + { + return TryGetHeaderValue(header, out value, out _); + } + + /// + /// Tries to get a header value if it exists and can be parsed. + /// + /// The type of the header value. + /// The header to parse. + /// A resulting value. + /// Details on the parsing operation. + /// if the header value was successfully fetched parsed. + public bool TryGetHeaderValue(HeaderKey header, [NotNullWhen(true)] out T? value, out ParsingResult result) + where T : notnull + { + _ = Throw.IfNull(header); + + if (header.Position >= _boxes.Length) + { + Array.Resize(ref _boxes, header.Position + 1); + } + + var box = (Box?)_boxes[header.Position]; + if (box is null) + { + box = new Box(); + _boxes[header.Position] = box; + } + + return box.Process(this, header, out value, out result); + } + + private void Reset() + { + Context = null; + foreach (var box in _boxes) + { + box?.Reset(); + } + } + + internal sealed class PoolHelper : IDisposable + { + public HeaderParsingFeature Feature { get; } + private readonly ObjectPool _pool; + + public PoolHelper(ObjectPool pool, IHeaderRegistry registry, ILogger logger, Meter meter) + { + _pool = pool; + Feature = new HeaderParsingFeature(registry, logger, meter); + } + + public void Dispose() + { + Feature.Reset(); + _pool.Return(this); + } + } + + private enum BoxState + { + Uninitialized = -1, + Success = ParsingResult.Success, + Error = ParsingResult.Error, + NotFound = ParsingResult.NotFound, + } + + [SuppressMessage("Minor Code Smell", "S1694:An abstract class should have both abstract and concrete methods", Justification = "Analyzer issue")] + private abstract class Box + { + public abstract void Reset(); + } + + private sealed class Box : Box + where T : notnull + { + private BoxState _state = BoxState.Uninitialized; + private T? _value; + + public override void Reset() + { + _state = BoxState.Uninitialized; + _value = default; + } + + public bool Process(HeaderParsingFeature feature, HeaderKey header, out T? value, out ParsingResult result) + { + if (_state == BoxState.Uninitialized) + { + if (feature.Context!.Request.Headers.TryGetValue(header.Name, out var values)) + { + if (header.Cacheable) + { + var o = header.GetCachedValue(values); + if (o != null) + { + feature._cacheAccessCounter.Add(1, header.Name, "Hit"); + var b = (Box)o; + b.CopyTo(this); + value = _value; + result = (ParsingResult)_state; + return result == ParsingResult.Success; + } + + feature._cacheAccessCounter.Add(1, header.Name, "Miss"); + } + + if (header.TryParse(values, out _value, out var error)) + { + _state = BoxState.Success; + + if (header.Cacheable) + { + var b = new Box(); + CopyTo(b); + header.AddCachedValue(values, b); + } + } + else + { + _state = BoxState.Error; + feature.LogParsingError(header.Name, error!); + feature._parsingErrorCounter.Add(1, header.Name, error); + } + } + else if (header.HasDefaultValue) + { + _state = BoxState.Success; + _value = header.DefaultValue; + feature.LogDefaultUsage(header.Name); + } + else + { + _state = BoxState.NotFound; + feature.LogNotFound(header.Name); + } + } + + value = _value; + result = (ParsingResult)_state; + + return result == ParsingResult.Success; + } + + private void CopyTo(Box to) + { + to._state = _state; + to._value = _value; + } + } + + [LogMethod(LogLevel.Debug, "Can't parse header '{headerName}' due to '{error}'.")] + private partial void LogParsingError(string headerName, string error); + + [LogMethod(LogLevel.Debug, "Using a default value for header '{headerName}'.")] + private partial void LogDefaultUsage(string headerName); + + [LogMethod(LogLevel.Debug, "Header '{headerName}' not found.")] + private partial void LogNotFound(string headerName); +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingOptions.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingOptions.cs new file mode 100644 index 0000000000..f8aa8680fb --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingOptions.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Primitives; + +#pragma warning disable CA2227 // Collection properties should be read only + +namespace Microsoft.AspNetCore.HeaderParsing; + +/// +/// Options for the header parsing infrastructure. +/// +public class HeaderParsingOptions +{ + /// + /// Gets or sets default header values for when the given headers aren't present. + /// + /// + /// The keys represent the header name. + /// + public IDictionary DefaultValues { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets or sets the default number of cached values per cacheable header. + /// + /// + /// Default value is 128 values. + /// The number of cached values can be overridden for specific headers using the property. + /// + [Range(0, int.MaxValue)] + public int DefaultMaxCachedValuesPerHeader { get; set; } = 128; + + /// + /// Gets or sets the maximum number of cached values for specific headers. + /// + /// + /// The keys represent the header name. + /// + public IDictionary MaxCachedValuesPerHeader { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingOptionsManualValidator.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingOptionsManualValidator.cs new file mode 100644 index 0000000000..48d98c3b51 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingOptionsManualValidator.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.HeaderParsing; + +internal sealed class HeaderParsingOptionsManualValidator : IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, HeaderParsingOptions options) + { + var builder = new ValidateOptionsResultBuilder(); + + foreach (var item in options.MaxCachedValuesPerHeader) + { + if (item.Value < 0) + { + builder.AddError( + $"Negative cached value count of {item.Value} specified for the {item.Key} header", + nameof(options.MaxCachedValuesPerHeader)); + } + } + + return builder.Build(); + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingOptionsValidator.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingOptionsValidator.cs new file mode 100644 index 0000000000..0e46ce7a34 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderParsingOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.AspNetCore.HeaderParsing; + +[OptionsValidator] +internal sealed partial class HeaderParsingOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderRegistry.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderRegistry.cs new file mode 100644 index 0000000000..7624c138dd --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderRegistry.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.AspNetCore.HeaderParsing; + +internal sealed class HeaderRegistry : IHeaderRegistry +{ + private readonly HeaderParsingOptions _options; + private readonly IServiceProvider _provider; + private readonly ConcurrentDictionary _headerKeys = new(); + private int _current = -1; + + public HeaderRegistry(IServiceProvider provider, IOptions options) + { + _provider = provider; + _options = options.Value; + } + + public HeaderKey Register(HeaderSetup setup) + where T : notnull + { + var parser = setup.ParserInstance ?? (HeaderParser)_provider.GetRequiredService(setup.ParserType!); + var id = new HeaderKeyIdentity(setup.HeaderName, parser, setup.Cacheable); + return (HeaderKey)_headerKeys.GetOrAdd(id, CreateKey, parser); + } + + private HeaderKey CreateKey(HeaderKeyIdentity id, HeaderParser parser) + where T : notnull + { + int maxCachedValues = 0; + if (id.Cacheable) + { + if (!_options.MaxCachedValuesPerHeader.TryGetValue(id.HeaderName, out maxCachedValues)) + { + maxCachedValues = _options.DefaultMaxCachedValuesPerHeader; + } + } + + var pos = Interlocked.Increment(ref _current); + if (_options.DefaultValues.TryGetValue(id.HeaderName, out var defValue)) + { + if (!parser.TryParse(defValue, out var parsedValue, out var error)) + { + Throw.InvalidOperationException($"Can't parse default value '{defValue}' for header '{id.HeaderName}': {error}."); + } + + return new HeaderKey(id.HeaderName, parser, pos, maxCachedValues, parsedValue); + } + + return new HeaderKey(id.HeaderName, parser, pos, maxCachedValues); + } + + [ExcludeFromCodeCoverage] + private readonly record struct HeaderKeyIdentity( + string HeaderName, + object Parser, + bool Cacheable); +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderSetup.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderSetup.cs new file mode 100644 index 0000000000..e8ec2683e2 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HeaderSetup.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.AspNetCore.HeaderParsing; + +/// +/// Stores all setup information for a header. +/// +/// The type of the header value. +public class HeaderSetup + where THeader : notnull +{ + /// + /// Gets the name of the header. + /// + public string HeaderName { get; } + + /// + /// Gets the type of the parser to parse header values. + /// + /// Not null when is and vice versa. + public Type? ParserType { get; } + + /// + /// Gets the parser to parse header values. + /// + /// Not null when is and vice versa. + public HeaderParser? ParserInstance { get; } + + /// + /// Gets a value indicating whether this header's parsed values can and/or should be cached. + /// + public bool Cacheable { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the header. + /// The type of the parser to parse header values. + /// Indicates whether the header's values can be cached. + public HeaderSetup(string headerName, Type parserType, bool cacheable = false) + { + HeaderName = Throw.IfNullOrWhitespace(headerName); + ParserType = Throw.IfNull(parserType); + Cacheable = cacheable; + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the header. + /// The parser to parse header values. + /// Indicates whether the header's values can be cached. + public HeaderSetup(string headerName, HeaderParser instance, bool cacheable = false) + { + HeaderName = Throw.IfNullOrWhitespace(headerName); + ParserInstance = Throw.IfNull(instance); + Cacheable = cacheable; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HostHeaderValue.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HostHeaderValue.cs new file mode 100644 index 0000000000..8827129146 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/HostHeaderValue.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Http; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.AspNetCore.HeaderParsing; + +/// +/// Holds parsed data for the HTTP host header. +/// +public readonly struct HostHeaderValue : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The address of the host. + /// The optional TCP port number on which the host is listening. + public HostHeaderValue(string host, int? port) + { + Host = Throw.IfNull(host); + Port = port; + } + + /// + /// Gets the host address. + /// + /// + /// The address of the server. + /// + public string Host { get; } + + /// + /// Gets the port value. + /// + /// + /// The optional TCP port number on which the server is listening. + /// + public int? Port { get; } + + /// + /// Equality operator. + /// + /// First value. + /// Second value. + /// , if its operands are equal, otherwise. + public static bool operator ==(HostHeaderValue left, HostHeaderValue right) + { + return left.Equals(right); + } + + /// + /// Inequality operator. + /// + /// First value. + /// Second value. + /// , if its operands are inequal, otherwise. + public static bool operator !=(HostHeaderValue left, HostHeaderValue right) + { + return !(left == right); + } + + /// + /// Parses a host header value. + /// + /// The value to parse. + /// The parsed result. + /// if the value was parsed successfully, otherwise. + public static bool TryParse(string value, [NotNullWhen(true)] out HostHeaderValue result) + { +#pragma warning disable CA2234 // Pass system uri objects instead of strings + var hs = HostString.FromUriComponent(value); +#pragma warning restore CA2234 // Pass system uri objects instead of strings + + var parsedHost = hs.Host; + if (!string.IsNullOrEmpty(parsedHost)) + { + result = new HostHeaderValue(parsedHost, hs.Port); + return true; + } + + result = default; + return false; + } + + /// + /// Determines whether this host header value and a specified host header value are identical. + /// + /// The other host header value. + /// if the two values are identical; otherwise, . + public bool Equals(HostHeaderValue other) => Host.Equals(other.Host, StringComparison.Ordinal) && Port == other.Port; + + /// + /// Determines whether the specified object is equal to the current host header value. + /// + /// The object to compare. + /// if the specified object is identical to the current host header value; otherwise, . + public override bool Equals(object? obj) => obj is HostHeaderValue hostHeader && Equals(hostHeader); + + /// + /// Gets a hash code for this object. + /// + /// A hash code for the current object. + public override int GetHashCode() => HashCode.Combine(Host, Port); + + /// + /// Gets a string representation of this object. + /// + /// A string representing this object. + public override string ToString() + { + if (Port.HasValue) + { + return $"{Host}:{Port.Value}"; + } + + return Host; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/IHeaderRegistry.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/IHeaderRegistry.cs new file mode 100644 index 0000000000..9ce1c2952f --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/IHeaderRegistry.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.HeaderParsing; + +/// +/// Provides typed header values. +/// +public interface IHeaderRegistry +{ + /// + /// Registers a header parser and returns an object to let you read the header's value at runtime. + /// + /// The type of the header. + /// The header setup. + /// If the header already exists, the current instance is returned. + /// An instance. + HeaderKey Register(HeaderSetup setup) + where T : notnull; +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Metric.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Metric.cs new file mode 100644 index 0000000000..be4f251215 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Metric.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +namespace Microsoft.AspNetCore.HeaderParsing; + +internal static partial class Metric +{ + [Counter("HeaderName", "Kind", Name = @"R9.HeaderParsing.ParsingErrors")] + public static partial ParsingErrorCounter CreateParsingErrorCounter(Meter meter); + + [Counter("HeaderName", "Type", Name = @"R9.HeaderParsing.CacheAccess")] + public static partial CacheAccessCounter CreateCacheAccessCounter(Meter meter); +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Microsoft.AspNetCore.HeaderParsing.csproj b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Microsoft.AspNetCore.HeaderParsing.csproj new file mode 100644 index 0000000000..3138ddb602 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Microsoft.AspNetCore.HeaderParsing.csproj @@ -0,0 +1,50 @@ + + + Microsoft.AspNetCore.HeaderParsing + Strong type header parsing + $(PackageTags);aspnetcore + Fundamentals + + + + true + true + true + true + true + + + + dev + 100 + 91 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/CacheControlHeaderValueParser.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/CacheControlHeaderValueParser.cs new file mode 100644 index 0000000000..6dd1fe69e8 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/CacheControlHeaderValueParser.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.HeaderParsing.Parsers; + +internal sealed class CacheControlHeaderValueParser : HeaderParser +{ + public static CacheControlHeaderValueParser Instance { get; } = new(); + + public override bool TryParse(StringValues values, [NotNullWhen(true)] out CacheControlHeaderValue? result, [NotNullWhen(false)] out string? error) + { + if (values.Count != 1 || !CacheControlHeaderValue.TryParse(values[0], out var parsedValue)) + { + error = "Unable to parse cache control value."; + result = default; + return false; + } + + error = default; + result = parsedValue; + return true; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/ContentDispositionHeaderValueParser.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/ContentDispositionHeaderValueParser.cs new file mode 100644 index 0000000000..577bac4f8d --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/ContentDispositionHeaderValueParser.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.HeaderParsing.Parsers; + +internal sealed class ContentDispositionHeaderValueParser : HeaderParser +{ + public static ContentDispositionHeaderValueParser Instance { get; } = new(); + + public override bool TryParse(StringValues values, [NotNullWhen(true)] out ContentDispositionHeaderValue? result, [NotNullWhen(false)] out string? error) + { + if (values.Count != 1 || !ContentDispositionHeaderValue.TryParse(values[0], out var parsedValue)) + { + error = "Unable to parse content disposition value."; + result = default; + return false; + } + + error = default; + result = parsedValue; + return true; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/CookieHeaderValueListParser.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/CookieHeaderValueListParser.cs new file mode 100644 index 0000000000..9855cf8f19 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/CookieHeaderValueListParser.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.HeaderParsing.Parsers; + +internal sealed class CookieHeaderValueListParser : HeaderParser> +{ + public static CookieHeaderValueListParser Instance { get; } = new(); + + public override bool TryParse(StringValues values, [NotNullWhen(true)] out IReadOnlyList? result, [NotNullWhen(false)] out string? error) + { + if (!CookieHeaderValue.TryParseList(values, out var parsedValue)) + { + error = "Unable to parse cookie value."; + result = default; + return false; + } + + error = default; + result = (IReadOnlyList)parsedValue; + return true; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/DateTimeOffsetParser.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/DateTimeOffsetParser.cs new file mode 100644 index 0000000000..11f38e9f7b --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/DateTimeOffsetParser.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.HeaderParsing.Parsers; + +internal sealed class DateTimeOffsetParser : HeaderParser +{ + public static DateTimeOffsetParser Instance { get; } = new(); + + public override bool TryParse(StringValues values, [NotNullWhen(true)] out DateTimeOffset result, [NotNullWhen(false)] out string? error) + { + if (values.Count != 1 || !HeaderUtilities.TryParseDate(values[0], out var parsedValue)) + { + error = "Unable to parse date time offset value."; + result = default; + return false; + } + + error = default; + result = parsedValue; + return true; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/EntityTagHeaderValueListParser.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/EntityTagHeaderValueListParser.cs new file mode 100644 index 0000000000..82829f15cf --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/EntityTagHeaderValueListParser.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.HeaderParsing.Parsers; + +internal sealed class EntityTagHeaderValueListParser : HeaderParser> +{ + public static EntityTagHeaderValueListParser Instance { get; } = new(); + + public override bool TryParse(StringValues values, [NotNullWhen(true)] out IReadOnlyList? result, [NotNullWhen(false)] out string? error) + { + if (!EntityTagHeaderValue.TryParseList(values, out var parsedValues)) + { + error = "Unable to parse entity tag values."; + result = default; + return false; + } + + error = default; + result = (IReadOnlyList)parsedValues; + return true; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/HostHeaderValueParser.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/HostHeaderValueParser.cs new file mode 100644 index 0000000000..654c721301 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/HostHeaderValueParser.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.HeaderParsing.Parsers; + +internal sealed class HostHeaderValueParser : HeaderParser +{ + public static readonly HostHeaderValueParser Instance = new(); + + public override bool TryParse(StringValues values, [NotNullWhen(true)] out HostHeaderValue result, [NotNullWhen(false)] out string? error) + { + if (values.Count != 1 || !HostHeaderValue.TryParse(values[0]!, out var parsedValue)) + { + error = "Unable to parse host header value."; + result = default; + return false; + } + + error = default; + result = parsedValue; + return true; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/IPAddressListParser.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/IPAddressListParser.cs new file mode 100644 index 0000000000..d6afec0404 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/IPAddressListParser.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.HeaderParsing.Parsers; + +internal sealed class IPAddressListParser : HeaderParser> +{ + public static IPAddressListParser Instance { get; } = new(); + + public override bool TryParse(StringValues values, [NotNullWhen(true)] out IReadOnlyList? result, [NotNullWhen(false)] out string? error) + { + var list = new List(); + + foreach (var value in values) + { + var startIndex = 0; + int nextSeparatorIndex; + + do + { + nextSeparatorIndex = value!.IndexOf(',', startIndex); + var length = (nextSeparatorIndex >= 0 ? nextSeparatorIndex : value.Length) - startIndex; + + if (length == 0) + { + error = "IP address cannot be empty."; + result = null; + return false; + } + +#if NETCOREAPP3_1_OR_GREATER + var addressToParse = value.AsSpan(startIndex, length).Trim(); +#else + var addressToParse = value.AsSpan(startIndex, length).Trim().ToString(); +#endif + + if (IPAddress.TryParse(addressToParse, out var address)) + { + list.Add(address); + } + else + { + error = "Unable to parse IP address."; + result = null; + return false; + } + + startIndex = nextSeparatorIndex + 1; + } + while (nextSeparatorIndex >= 0); + } + + result = list; + error = null; + return true; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/MediaTypeHeaderValueListParser.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/MediaTypeHeaderValueListParser.cs new file mode 100644 index 0000000000..fdaa789a3c --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/MediaTypeHeaderValueListParser.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.HeaderParsing.Parsers; + +internal sealed class MediaTypeHeaderValueListParser : HeaderParser> +{ + public static MediaTypeHeaderValueListParser Instance { get; } = new(); + + public override bool TryParse(StringValues values, [NotNullWhen(true)] out IReadOnlyList? result, [NotNullWhen(false)] out string? error) + { + if (!MediaTypeHeaderValue.TryParseList(values, out var parsedValues)) + { + error = "Unable to parse media type values."; + result = default; + return false; + } + + error = default; + result = (IReadOnlyList)parsedValues; + return true; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/MediaTypeHeaderValueParser.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/MediaTypeHeaderValueParser.cs new file mode 100644 index 0000000000..3842548f32 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/MediaTypeHeaderValueParser.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.HeaderParsing.Parsers; + +internal sealed class MediaTypeHeaderValueParser : HeaderParser +{ + public static MediaTypeHeaderValueParser Instance { get; } = new(); + + public override bool TryParse(StringValues values, [NotNullWhen(true)] out MediaTypeHeaderValue? result, [NotNullWhen(false)] out string? error) + { + if (values.Count != 1 || !MediaTypeHeaderValue.TryParse(values[0], out var parsedValue)) + { + error = "Unable to parse media type value."; + result = default; + return false; + } + + error = default; + result = parsedValue; + return true; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/RangeConditionHeaderValueParser.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/RangeConditionHeaderValueParser.cs new file mode 100644 index 0000000000..c0cbdaacc2 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/RangeConditionHeaderValueParser.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.HeaderParsing.Parsers; + +internal sealed class RangeConditionHeaderValueParser : HeaderParser +{ + public static RangeConditionHeaderValueParser Instance { get; } = new(); + + public override bool TryParse(StringValues values, [NotNullWhen(true)] out RangeConditionHeaderValue? result, [NotNullWhen(false)] out string? error) + { + if (values.Count != 1 || !RangeConditionHeaderValue.TryParse(values[0], out var parsedValue)) + { + error = "Unable to parse range condition value."; + result = default; + return false; + } + + error = default; + result = parsedValue; + return true; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/RangeHeaderValueParser.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/RangeHeaderValueParser.cs new file mode 100644 index 0000000000..c3bfadbe2e --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/RangeHeaderValueParser.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.HeaderParsing.Parsers; + +internal sealed class RangeHeaderValueParser : HeaderParser +{ + public static RangeHeaderValueParser Instance { get; } = new(); + + public override bool TryParse(StringValues values, [NotNullWhen(true)] out RangeHeaderValue? result, [NotNullWhen(false)] out string? error) + { + if (values.Count != 1 || !RangeHeaderValue.TryParse(values[0], out var parsedValue)) + { + error = "Unable to parse range value."; + result = default; + return false; + } + + error = default; + result = parsedValue; + return true; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/StringWithQualityHeaderValueListParser.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/StringWithQualityHeaderValueListParser.cs new file mode 100644 index 0000000000..383c7a4dbb --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/StringWithQualityHeaderValueListParser.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.HeaderParsing.Parsers; + +internal sealed class StringWithQualityHeaderValueListParser : HeaderParser> +{ + public static StringWithQualityHeaderValueListParser Instance { get; } = new(); + + public override bool TryParse(StringValues values, [NotNullWhen(true)] out IReadOnlyList? result, [NotNullWhen(false)] out string? error) + { + if (!StringWithQualityHeaderValue.TryParseList(values, out var parsedValues)) + { + error = "Unable to parse string with quality values."; + result = default; + return false; + } + + error = default; + result = (IReadOnlyList)parsedValues; + return true; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/UriParser.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/UriParser.cs new file mode 100644 index 0000000000..2e375f2871 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/Parsers/UriParser.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.HeaderParsing.Parsers; + +internal sealed class UriParser : HeaderParser +{ + public static UriParser Instance { get; } = new(); + + public override bool TryParse(StringValues values, [NotNullWhen(true)] out Uri? result, [NotNullWhen(false)] out string? error) + { + if (values.Count != 1 || !Uri.TryCreate(values[0], UriKind.RelativeOrAbsolute, out var parsedValue)) + { + error = "Unable to parse URI."; + result = default; + return false; + } + + error = default; + result = parsedValue; + return true; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.HeaderParsing/ParsingResult.cs b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/ParsingResult.cs new file mode 100644 index 0000000000..a35af281b7 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.HeaderParsing/ParsingResult.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.HeaderParsing; + +/// +/// Result of trying to parse a header. +/// +public enum ParsingResult +{ + /// + /// Indicates the header was successfully parsed. + /// + Success, + + /// + /// Indicates the header's value was malformed and couldn't be parsed. + /// + Error, + + /// + /// Indicates the header was not present. + /// + NotFound, +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/AddServerTimingHeaderMiddleware.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/AddServerTimingHeaderMiddleware.cs new file mode 100644 index 0000000000..2b0945def5 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/AddServerTimingHeaderMiddleware.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; +using Microsoft.Extensions.Telemetry.Latency; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// A middleware that populates Server-Timing header with response processing time. +/// +internal sealed class AddServerTimingHeaderMiddleware : IMiddleware +{ + internal const string ServerTimingHeaderName = "Server-Timing"; + + /// + /// Request handling method. + /// + /// The for the current request. + /// The delegate representing the remaining middleware in the request pipeline. + /// A that represents the execution of this middleware. + public Task InvokeAsync(HttpContext context, RequestDelegate next) + { + context.Response.OnStarting(ctx => + { + var httpContext = (HttpContext)ctx; + var latencyContext = httpContext.RequestServices.GetRequiredService(); + + if (latencyContext.TryGetCheckpoint(RequestCheckpointConstants.ElapsedTillHeaders, out var timestamp, out var timestampFrequency)) + { + var elapsedMs = (long)(((double)timestamp / timestampFrequency) * 1000); + + if (httpContext.Response.Headers.TryGetValue(ServerTimingHeaderName, out var existing)) + { + httpContext.Response.Headers[ServerTimingHeaderName] = $"{existing}, reqlatency;dur={elapsedMs}"; + } + else + { + httpContext.Response.Headers.Add(ServerTimingHeaderName, $"reqlatency;dur={elapsedMs}"); + } + } + + return Task.CompletedTask; + }, context); + + return next(context); + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/CapturePipelineEntryMiddleware.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/CapturePipelineEntryMiddleware.cs new file mode 100644 index 0000000000..ce8c26b3a5 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/CapturePipelineEntryMiddleware.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Telemetry.Latency; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Middleware that should be put at the beginning of the middleware pipeline to capture time. +/// +internal sealed class CapturePipelineEntryMiddleware : IMiddleware +{ + private readonly CheckpointToken _elapsedTillEntry; + + public CapturePipelineEntryMiddleware(ILatencyContextTokenIssuer tokenIssuer) + { + _elapsedTillEntry = tokenIssuer.GetCheckpointToken(RequestCheckpointConstants.ElapsedTillEntryMiddleware); + } + + /// + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + await next(context).ConfigureAwait(false); + + var latencyContext = context.RequestServices.GetRequiredService(); + latencyContext.AddCheckpoint(_elapsedTillEntry); + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/CapturePipelineEntryStartupFilter.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/CapturePipelineEntryStartupFilter.cs new file mode 100644 index 0000000000..85818eb9a4 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/CapturePipelineEntryStartupFilter.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Adds at the beginning of the middleware pipeline. +/// +internal sealed class CapturePipelineEntryStartupFilter : IStartupFilter +{ + /// + /// Wraps the directly adds + /// at the beginning the middleware pipeline. + /// + /// The Configure method to extend. + /// A modified . + public Action Configure(Action next) + { + return builder => + { + _ = builder.UseMiddleware(Array.Empty()); + next(builder); + }; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/CapturePipelineExitMiddleware.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/CapturePipelineExitMiddleware.cs new file mode 100644 index 0000000000..2e6c839e75 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/CapturePipelineExitMiddleware.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Telemetry.Latency; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Middleware that should be put at the end of the pipeline to capture time. +/// +internal sealed class CapturePipelineExitMiddleware : IMiddleware +{ + private readonly CheckpointToken _elapsedTillPipelineExit; + + private readonly CheckpointToken _elapsedResponseProcessed; + + public CapturePipelineExitMiddleware(ILatencyContextTokenIssuer tokenIssuer) + { + _elapsedTillPipelineExit = tokenIssuer.GetCheckpointToken(RequestCheckpointConstants.ElapsedTillPipelineExitMiddleware); + _elapsedResponseProcessed = tokenIssuer.GetCheckpointToken(RequestCheckpointConstants.ElapsedResponseProcessed); + } + + /// + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + var latencyContext = context.RequestServices.GetRequiredService(); + latencyContext.AddCheckpoint(_elapsedTillPipelineExit); + + await next(context).ConfigureAwait(false); + + latencyContext.AddCheckpoint(_elapsedResponseProcessed); + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/CaptureResponseTimeMiddleware.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/CaptureResponseTimeMiddleware.cs new file mode 100644 index 0000000000..5876a51562 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/CaptureResponseTimeMiddleware.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Telemetry.Latency; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// A middleware that captures response times. +/// +internal sealed class CaptureResponseTimeMiddleware : IMiddleware +{ + private readonly CheckpointToken _elapsedTillHeaders; + + private readonly CheckpointToken _elapsedTillFinished; + + public CaptureResponseTimeMiddleware(ILatencyContextTokenIssuer tokenIssuer) + { + _elapsedTillHeaders = tokenIssuer.GetCheckpointToken(RequestCheckpointConstants.ElapsedTillHeaders); + _elapsedTillFinished = tokenIssuer.GetCheckpointToken(RequestCheckpointConstants.ElapsedTillFinished); + } + + /// + /// Request handling method. + /// + /// The for the current request. + /// The delegate representing the remaining middleware in the request pipeline. + /// A that represents the execution of this middleware. + public Task InvokeAsync(HttpContext context, RequestDelegate next) + { + var latencyContext = context.RequestServices.GetRequiredService(); + + // Capture the time just before response headers will be sent to the client. + context.Response.OnStarting(l => + { + var latencyContext = l as ILatencyContext; + latencyContext!.AddCheckpoint(_elapsedTillHeaders); + return Task.CompletedTask; + }, latencyContext); + + // Capture the time after the response has finished being sent to the client. + context.Response.OnCompleted(l => + { + var latencyContext = l as ILatencyContext; + latencyContext!.AddCheckpoint(_elapsedTillFinished); + return Task.CompletedTask; + }, latencyContext); + + // Call the next delegate/middleware in the pipeline + return next(context); + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/LatencyContextControlExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/LatencyContextControlExtensions.cs new file mode 100644 index 0000000000..2d095470ee --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/LatencyContextControlExtensions.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Telemetry.Latency; + +namespace Microsoft.AspNetCore.Telemetry; + +internal static class LatencyContextControlExtensions +{ + public static bool TryGetCheckpoint(this ILatencyContext latencyContext, string checkpointName, out long elapsed, out long frequency) + { + var checkpoints = latencyContext.LatencyData.Checkpoints; + foreach (var checkpoint in checkpoints) + { + if (string.Equals(checkpoint.Name, checkpointName, StringComparison.Ordinal)) + { + elapsed = checkpoint.Elapsed; + frequency = checkpoint.Frequency; + return true; + } + } + + elapsed = 0; + frequency = 0; + return false; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/RequestCheckpointConstants.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/RequestCheckpointConstants.cs new file mode 100644 index 0000000000..7382129b56 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/RequestCheckpointConstants.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Project constants. +/// +public static class RequestCheckpointConstants +{ + /// + /// The time elapsed before the response headers have been sent to the client. + /// + public const string ElapsedTillHeaders = "elthdr"; + + /// + /// The time elapsed before the response has finished being sent to the client. + /// + public const string ElapsedTillFinished = "eltltf"; + + /// + /// The time elapsed before hitting the middleware. + /// + public const string ElapsedTillPipelineExitMiddleware = "eltexm"; + + /// + /// The time elapsed before the response back to middleware pipeline. + /// + public const string ElapsedResponseProcessed = "eltrspproc"; + + /// + /// The time elapsed before hitting the first middleware. + /// + public const string ElapsedTillEntryMiddleware = "eltenm"; + + /// + /// List of checkpoints added by the middlewares. + /// + internal static readonly string[] RequestCheckpointNames = new[] + { + ElapsedTillHeaders, + ElapsedTillFinished, + ElapsedTillEntryMiddleware, + ElapsedTillPipelineExitMiddleware, + ElapsedResponseProcessed + }; +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/RequestCheckpointExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/RequestCheckpointExtensions.cs new file mode 100644 index 0000000000..2aa566268c --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Checkpoint/RequestCheckpointExtensions.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Extensions used to register Request Checkpoint feature. +/// +public static class RequestCheckpointExtensions +{ + /// + /// Adds all Request Checkpoint services. + /// + /// The to add the service to. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddRequestCheckpoint(this IServiceCollection services) + { + _ = Throw.IfNull(services); + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddPipelineEntryCheckpoint(); + _ = services.AddSingleton(); + _ = services.RegisterCheckpointNames(RequestCheckpointConstants.RequestCheckpointNames); + + return services; + } + + /// + /// Registers Request Checkpoint related middlewares into the pipeline. + /// + /// The . + /// The instance. + public static IApplicationBuilder UseRequestCheckpoint(this IApplicationBuilder builder) + { + _ = Throw.IfNull(builder); + + // the order DOES matter + _ = builder.UseMiddleware(Array.Empty()); + _ = builder.UseMiddleware(Array.Empty()); + _ = builder.UseMiddleware(Array.Empty()); + + return builder; + } + + /// + /// Adds at the beginning of the middleware pipeline using . + /// + /// The to add the service to. + /// A reference to this instance after the operation has completed. + internal static IServiceCollection AddPipelineEntryCheckpoint(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + return services; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Internal/RequestLatencyTelemetryMiddleware.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Internal/RequestLatencyTelemetryMiddleware.cs new file mode 100644 index 0000000000..bebc8ee66f --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Internal/RequestLatencyTelemetryMiddleware.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Shared.Pools; + +namespace Microsoft.AspNetCore.Telemetry.Internal; + +/// +/// Middleware that manages latency context for requests. +/// +internal sealed class RequestLatencyTelemetryMiddleware : IMiddleware +{ + private static readonly ObjectPool> _exporterTaskPool = PoolFactory.CreateListPool(); + private static readonly ObjectPool _cancellationTokenSourcePool = PoolFactory.CreateCancellationTokenSourcePool(); + + private readonly TimeSpan _exportTimeout; + + private readonly ILatencyDataExporter[] _latencyDataExporters; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// The list of exporters for latency data. + public RequestLatencyTelemetryMiddleware(IOptions options, IEnumerable latencyDataExporters) + { + _exportTimeout = options.Value.LatencyDataExportTimeout; + _latencyDataExporters = latencyDataExporters.ToArray(); + } + + /// + /// Request handling method. + /// + /// The for the current request. + /// The delegate representing the remaining middleware in the request pipeline. + /// A that represents the execution of this middleware. + public Task InvokeAsync(HttpContext context, RequestDelegate next) + { + var latencyContext = context.RequestServices.GetRequiredService(); + + context.Response.OnCompleted(async l => + { + var latencyContext = l as ILatencyContext; + latencyContext!.Freeze(); + await ExportAsync(latencyContext.LatencyData).ConfigureAwait(false); + }, latencyContext); + + return next.Invoke(context); + } + + [SuppressMessage("Resilience", "R9A061:The async method doesn't support cancellation", Justification = "The time limit is enforced inside of the method")] + private async Task ExportAsync(LatencyData latencyData) + { + var tokenSource = _cancellationTokenSourcePool.Get(); + tokenSource.CancelAfter(_exportTimeout); + + List exports = _exporterTaskPool.Get(); + foreach (ILatencyDataExporter latencyDataExporter in _latencyDataExporters) + { + exports.Add(latencyDataExporter.ExportAsync(latencyData, tokenSource.Token)); + } + + await Task.WhenAll(exports).ConfigureAwait(false); + + _exporterTaskPool.Return(exports); + _cancellationTokenSourcePool.Return(tokenSource); + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Internal/RequestLatencyTelemetryOptionsValidator.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Internal/RequestLatencyTelemetryOptionsValidator.cs new file mode 100644 index 0000000000..0266535464 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/Internal/RequestLatencyTelemetryOptionsValidator.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.AspNetCore.Telemetry.Internal; + +[OptionsValidator] +internal sealed partial class RequestLatencyTelemetryOptionsValidator : IValidateOptions +{ + /// + /// Minimum possible timeout. + /// + internal const int MinimumTimeoutInMs = 1000; +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/RequestLatencyTelemetryExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/RequestLatencyTelemetryExtensions.cs new file mode 100644 index 0000000000..7366244eab --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/RequestLatencyTelemetryExtensions.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Telemetry.Internal; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Extensions for registering the request latency telemetry middleware. +/// +public static class RequestLatencyTelemetryExtensions +{ + /// + /// Adds request latency telemetry middleware to the specified . + /// + /// The to add to. + /// Provided service collection with request latency telemetry middleware added. + /// When is . + public static IServiceCollection AddRequestLatencyTelemetry(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + services.TryAddScoped(p => p.GetRequiredService().CreateContext()); + services.TryAddSingleton(); + + _ = services.AddValidatedOptions(); + + return services; + } + + /// + /// Adds request latency telemetry middleware to the specified . + /// + /// The to add to. + /// Configuration of . + /// Provided service collection with request latency telemetry middleware added. + /// When either or is . + public static IServiceCollection AddRequestLatencyTelemetry(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + _ = services + .Configure(configure); + + return AddRequestLatencyTelemetry(services); + } + + /// + /// Adds request latency telemetry middleware to the specified . + /// + /// The to add to. + /// Configuration of . + /// Provided service collection with request latency telemetry middleware added. + /// When either or is . + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, type: typeof(RequestLatencyTelemetryOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed with [DynamicDependency]")] + public static IServiceCollection AddRequestLatencyTelemetry(this IServiceCollection services, IConfigurationSection section) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(section); + + _ = services + .Configure(section); + + return AddRequestLatencyTelemetry(services); + } + + /// + /// Adds the request latency telemetry middleware to request execution pipeline. + /// + /// The . + /// The so that additional calls can be chained. + /// When is . + public static IApplicationBuilder UseRequestLatencyTelemetry(this IApplicationBuilder builder) + { + _ = Throw.IfNull(builder); + + _ = builder.UseMiddleware(Array.Empty()); + + return builder; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/RequestLatencyTelemetryOptions.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/RequestLatencyTelemetryOptions.cs new file mode 100644 index 0000000000..67b06f7792 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Latency/RequestLatencyTelemetryOptions.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.Telemetry.Internal; +using Microsoft.Shared.Data.Validation; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Options to configure the request latency middleware. +/// +public class RequestLatencyTelemetryOptions +{ + private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(5); + + /// + /// Gets or sets the amount of time to wait for export of latency data. + /// + /// + /// Default set to 5 seconds. + /// + [TimeSpan(RequestLatencyTelemetryOptionsValidator.MinimumTimeoutInMs)] + public TimeSpan LatencyDataExportTimeout { get; set; } = _defaultTimeout; +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/HttpLoggingDimensions.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/HttpLoggingDimensions.cs new file mode 100644 index 0000000000..c7457ef08d --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/HttpLoggingDimensions.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Constants used for incoming HTTP request logging dimensions. +/// +public static class HttpLoggingDimensions +{ + /// + /// HTTP Request duration in milliseconds. + /// + public const string Duration = "duration"; + + /// + /// HTTP Host. + /// + public const string Host = "httpHost"; + + /// + /// HTTP Method. + /// + public const string Method = "httpMethod"; + + /// + /// HTTP Path. + /// + public const string Path = "httpPath"; + + /// + /// HTTP Request Headers prefix. + /// + public const string RequestHeaderPrefix = "httpRequestHeader_"; + + /// + /// HTTP Response Headers prefix. + /// + public const string ResponseHeaderPrefix = "httpResponseHeader_"; + + /// + /// HTTP Request Body. + /// + public const string RequestBody = "httpRequestBody"; + + /// + /// HTTP Response Body. + /// + public const string ResponseBody = "httpResponseBody"; + + /// + /// HTTP Status Code. + /// + public const string StatusCode = "httpStatusCode"; + + /// + /// Gets a list of all dimension names. + /// + /// A read-only of all dimension names. + public static IReadOnlyList DimensionNames { get; } = + Array.AsReadOnly(new[] + { + Duration, + Host, + Method, + Path, + RequestHeaderPrefix, + ResponseHeaderPrefix, + RequestBody, + ResponseBody, + StatusCode + }); +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/HttpLoggingServiceExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/HttpLoggingServiceExtensions.cs new file mode 100644 index 0000000000..bfa5f65e80 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/HttpLoggingServiceExtensions.cs @@ -0,0 +1,130 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Telemetry.Http.Logging; +using Microsoft.AspNetCore.Telemetry.Internal; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.IO; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Extension methods to register the HTTP logging feature within the service. +/// +public static class HttpLoggingServiceExtensions +{ + /// + /// Adds components for incoming HTTP requests logging into . + /// + /// The to add the service to. + /// The so that additional calls can be chained. + /// The is . + public static IServiceCollection AddHttpLogging(this IServiceCollection services) + { + _ = Throw.IfNull(services); + return AddHttpLoggingInternal(services); + } + + /// + /// Adds components for incoming HTTP requests logging into . + /// + /// The to add the service to. + /// + /// An to configure the . + /// + /// The so that additional calls can be chained. + /// + /// Either or is . + /// + public static IServiceCollection AddHttpLogging(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + return AddHttpLoggingInternal(services, x => x.Configure(configure)); + } + + /// + /// Adds components for incoming HTTP requests logging into . + /// + /// The to add the service to. + /// The configuration section to bind to. + /// The so that additional calls can be chained. + /// + /// Either or is . + /// + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(LoggingOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed with [DynamicDependency]")] + public static IServiceCollection AddHttpLogging(this IServiceCollection services, IConfigurationSection section) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(section); + + return AddHttpLoggingInternal(services, x => x.Bind(section)); + } + + /// + /// Adds an enricher instance of to the to enrich incoming HTTP requests logs. + /// + /// Type of enricher. + /// The to add the instance of to. + /// The so that additional calls can be chained. + /// The is . + public static IServiceCollection AddHttpLogEnricher(this IServiceCollection services) + where T : class, IHttpLogEnricher + { + _ = Throw.IfNull(services); + + return services.AddActivatedSingleton(); + } + + /// + /// Registers incoming HTTP request logging middleware into . + /// + /// + /// Request logging middleware should be placed after call. + /// + /// An application's request pipeline builder. + /// The so that additional calls can be chained. + /// The is . + public static IApplicationBuilder UseHttpLoggingMiddleware(this IApplicationBuilder builder) + { + _ = Throw.IfNull(builder); + + return builder.UseMiddleware(Array.Empty()); + } + + private static IServiceCollection AddHttpLoggingInternal( + IServiceCollection services, + Action>? configureOptionsBuilder = null) + { + var builder = services + .AddValidatedOptions(); + + configureOptionsBuilder?.Invoke(builder); + + // Register recyclable memory stream manager: + services.TryAddSingleton(); + + // Register our middleware: + services.TryAddActivatedSingleton(); + + // Internal stuff for route processing: + _ = services.AddHttpRouteProcessor(); + _ = services.AddHttpRouteUtilities(); + + return services; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/IHttpLogEnricher.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/IHttpLogEnricher.cs new file mode 100644 index 0000000000..f60ddc98dc --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/IHttpLogEnricher.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Interface for implementing log enrichers for incoming HTTP requests. +/// +public interface IHttpLogEnricher +{ + /// + /// Enrich logs. + /// + /// Property bag to add enriched properties to. + /// object associated with the incoming HTTP request. + /// object associated with the response to an incoming HTTP request. + void Enrich(IEnrichmentPropertyBag enrichmentBag, HttpRequest request, HttpResponse response); +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/IncomingPathLoggingMode.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/IncomingPathLoggingMode.cs new file mode 100644 index 0000000000..1bf758bcc6 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/IncomingPathLoggingMode.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Strategy to decide how request path is logged. +/// +public enum IncomingPathLoggingMode +{ + /// + /// Request path is logged formatted, its params are not logged. + /// + Formatted, + + /// + /// Request path is logged in a structured way (as route), its params are logged. + /// + Structured +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/HeaderReader.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/HeaderReader.cs new file mode 100644 index 0000000000..f6c0fca756 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/HeaderReader.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging; + +internal sealed class HeaderReader +{ + private readonly IRedactorProvider _redactorProvider; + private readonly KeyValuePair[] _headers; + + public HeaderReader(IDictionary headersToLog, IRedactorProvider redactorProvider) + { + _redactorProvider = redactorProvider; + + _headers = headersToLog.Count == 0 + ? Array.Empty>() + : headersToLog.ToArray(); + } + + /// + /// Reads headers and applies filtering (if required). + /// + /// A collection of headers to be read from. + /// A list to be filled with headers. + public void Read(IHeaderDictionary headers, List> listToFill) + { + if (headers.Count == 0) + { + return; + } + + foreach (var header in _headers) + { + if (headers.TryGetValue(header.Key, out var headerValue)) + { + var provider = _redactorProvider.GetRedactor(header.Value); + var redacted = provider.Redact(headerValue.ToString()); + listToFill.Add(new(header.Key, redacted)); + } + } + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/HttpLogPropertiesProvider.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/HttpLogPropertiesProvider.cs new file mode 100644 index 0000000000..ae4d0affe0 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/HttpLogPropertiesProvider.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging; + +internal static class HttpLogPropertiesProvider +{ + private static readonly ConcurrentDictionary _requestPrefixedNamesCache = new(); + private static readonly ConcurrentDictionary _responsePrefixedNamesCache = new(); + + public static void GetProperties(LogMethodHelper props, IncomingRequestLogRecord logRecord) + { + props.Add(HttpLoggingDimensions.Method, logRecord.Method); + props.Add(HttpLoggingDimensions.Host, logRecord.Host); + props.Add(HttpLoggingDimensions.Path, logRecord.Path); + + if (logRecord.Duration.HasValue) + { + props.Add(HttpLoggingDimensions.Duration, logRecord.Duration.Value); + } + + if (logRecord.StatusCode.HasValue) + { + props.Add(HttpLoggingDimensions.StatusCode, logRecord.StatusCode.Value); + } + + if (logRecord.PathParameters is not null) + { + for (int i = 0; i < logRecord.PathParametersCount; i++) + { + var p = logRecord.PathParameters[i]; + props.Add(p.Name, p.Value); + } + } + + if (logRecord.RequestBody is not null) + { + props.Add(HttpLoggingDimensions.RequestBody, logRecord.RequestBody); + } + + if (logRecord.ResponseBody is not null) + { + props.Add(HttpLoggingDimensions.ResponseBody, logRecord.ResponseBody); + } + + if (logRecord.RequestHeaders is not null) + { + var count = logRecord.RequestHeaders.Count; + for (int i = 0; i < count; i++) + { + var header = logRecord.RequestHeaders[i]; + var prefixedName = _requestPrefixedNamesCache.GetOrAdd(header.Key, static x => HttpLoggingDimensions.RequestHeaderPrefix + x); + props.Add(prefixedName, header.Value); + } + } + + if (logRecord.ResponseHeaders is not null) + { + var count = logRecord.ResponseHeaders.Count; + for (int i = 0; i < count; i++) + { + var header = logRecord.ResponseHeaders[i]; + var prefixedName = _responsePrefixedNamesCache.GetOrAdd(header.Key, static x => HttpLoggingDimensions.ResponseHeaderPrefix + x); + props.Add(prefixedName, header.Value); + } + } + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/HttpLoggingMiddleware.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/HttpLoggingMiddleware.cs new file mode 100644 index 0000000000..9ec8276889 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/HttpLoggingMiddleware.cs @@ -0,0 +1,461 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Telemetry.Internal; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Extensions.Telemetry.Logging; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Pools; + +// Keeping this namespace so that users are able to control logging: +namespace Microsoft.AspNetCore.Telemetry.Http.Logging; + +internal sealed class HttpLoggingMiddleware : IMiddleware +{ + // These three fields are "internal" solely for testing purposes: + internal int BodyReadSizeLimit; + internal TimeProvider TimeProvider = TimeProvider.System; + internal Func> GetResponseBodyInterceptedData = static stream => stream.GetInterceptedSequence(); + internal TimeSpan RequestBodyReadTimeout; + + private readonly bool _logRequestStart; + private readonly bool _logRequestBody; + private readonly bool _logResponseBody; + private readonly bool _logRequestHeaders; + private readonly bool _logResponseHeaders; + + private readonly IncomingPathLoggingMode _requestPathLogMode; + private readonly HttpRouteParameterRedactionMode _parameterRedactionMode; + private readonly ILogger _logger; + private readonly IHttpRouteParser _httpRouteParser; + private readonly IHttpRouteFormatter _httpRouteFormatter; + private readonly IIncomingHttpRouteUtility _httpRouteUtility; + private readonly HeaderReader _requestHeadersReader; + private readonly HeaderReader _responseHeadersReader; + private readonly string[] _excludePathStartsWith; + private readonly IHttpLogEnricher[] _enrichers; + private readonly MediaType[] _requestMediaTypes; + private readonly MediaType[] _responseMediaTypes; + private readonly FrozenDictionary _parametersToRedactMap; + + private readonly ObjectPool _logRecordPool = + PoolFactory.CreatePool(); + + private readonly ObjectPool>> _headersPool = + PoolFactory.CreateListPool>(); + + [SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", Justification = "Technical debt accepted.")] + public HttpLoggingMiddleware( + IOptions options, + ILogger logger, + IEnumerable httpLogEnrichers, + IHttpRouteParser httpRouteParser, + IHttpRouteFormatter httpRouteFormatter, + IRedactorProvider redactorProvider, + IIncomingHttpRouteUtility httpRouteUtility, + IDebuggerState? debugger = null) + { + var optionsValue = options.Value; + _logger = logger; + _httpRouteParser = httpRouteParser; + _httpRouteFormatter = httpRouteFormatter; + _httpRouteUtility = httpRouteUtility; + _logRequestStart = optionsValue.LogRequestStart; + if (optionsValue.LogBody) + { + _logRequestBody = optionsValue.RequestBodyContentTypes.Count > 0; + _logResponseBody = optionsValue.ResponseBodyContentTypes.Count > 0; + } + + _parametersToRedactMap = optionsValue.RouteParameterDataClasses.ToFrozenDictionary(StringComparer.Ordinal, optimizeForReading: true); + + _requestPathLogMode = EnsureRequestPathLoggingModeIsValid(optionsValue.RequestPathLoggingMode); + _parameterRedactionMode = optionsValue.RequestPathParameterRedactionMode; + + BodyReadSizeLimit = optionsValue.BodySizeLimit; + + debugger ??= DebuggerState.System; + RequestBodyReadTimeout = debugger.IsAttached + ? Timeout.InfiniteTimeSpan + : optionsValue.RequestBodyReadTimeout; + + _requestMediaTypes = optionsValue.RequestBodyContentTypes + .Select(static x => new MediaType(x)) + .ToArray(); + + _responseMediaTypes = optionsValue.ResponseBodyContentTypes + .Select(static x => new MediaType(x)) + .ToArray(); + + _logRequestHeaders = optionsValue.RequestHeadersDataClasses.Count > 0; + _logResponseHeaders = optionsValue.ResponseHeadersDataClasses.Count > 0; + _requestHeadersReader = new(optionsValue.RequestHeadersDataClasses, redactorProvider); + _responseHeadersReader = new(optionsValue.ResponseHeadersDataClasses, redactorProvider); + + _excludePathStartsWith = optionsValue.ExcludePathStartsWith.ToArray(); + + _enrichers = httpLogEnrichers.ToArray(); + + // There's no need to use this middleware, + // so log a warning and hope that "LogLevel.Warning" is enabled: + if (!_logger.IsEnabled(Log.DefaultLogLevel)) + { + _logger.MiddlewareIsMisused(Log.DefaultLogLevel, nameof(HttpLoggingServiceExtensions.UseHttpLoggingMiddleware)); + } + } + + public Task InvokeAsync(HttpContext context, RequestDelegate next) + { + if (ShouldExcludePath(context.Request.Path)) + { + return next(context); + } + else + { + return InvokeAsyncWithPathAsync(context, next); + } + } + + private static IncomingPathLoggingMode EnsureRequestPathLoggingModeIsValid(IncomingPathLoggingMode mode) + => mode switch + { + IncomingPathLoggingMode.Structured or IncomingPathLoggingMode.Formatted => mode, + _ => throw new InvalidOperationException($"Unsupported value '{mode}' for enum type '{nameof(IncomingPathLoggingMode)}'"), + }; + + private async Task InvokeAsyncWithPathAsync(HttpContext context, RequestDelegate next) + { + ResponseInterceptingStream? bufferingResponseStream = null; + string? requestBody = null; + var timestamp = TimeProvider.GetTimestamp(); + try + { + if (_logResponseBody) + { + // Swapping response stream: + var oldFeature = context.Features.Get()!; + bufferingResponseStream = ResponseInterceptingStreamPool.Get(oldFeature, BodyReadSizeLimit); + context.Features.Set(bufferingResponseStream); + } + + requestBody = _logRequestBody + ? await GetRequestBodyAsync(context.Request, context.RequestAborted).ConfigureAwait(false) + : null; + + if (_logRequestStart) + { + LogRequest(context, timestamp, isRequestStart: true, requestBody, responseBody: null); + } + + await next(context).ConfigureAwait(false); + + var responseBody = _logResponseBody + ? GetResponseBody(context.Response, bufferingResponseStream!) + : null; + + context.Response.OnCompleted(() => + { + LogRequest(context, timestamp, isRequestStart: false, requestBody, responseBody); + + return Task.CompletedTask; + }); + } + catch (Exception ex) + { + // Even if the response body has been already read, we can re-read it safely: + var responseBody = _logResponseBody + ? GetResponseBody(context.Response, bufferingResponseStream!) + : null; + + LogRequest(context, timestamp, isRequestStart: false, requestBody, responseBody, ex); + + throw; + } + finally + { + if (bufferingResponseStream is not null) + { + context.Features.Set(bufferingResponseStream.InnerBodyFeature); + ResponseInterceptingStreamPool.Return(bufferingResponseStream); + } + } + } + + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Intentional")] + private string? GetResponseBody(HttpResponse response, ResponseInterceptingStream stream) + { + if (!MediaTypeSetExtensions.Covers(_responseMediaTypes, response.ContentType)) + { + return null; + } + + try + { + var sequenceSpan = GetResponseBodyInterceptedData(stream).Span; + + return Encoding.UTF8.GetString(sequenceSpan); + } + catch (Exception ex) + { + // We are intentionally catching and logging any exceptions which may happen. + _logger.ErrorReadingResponseBody(ex); + return null; + } + } + + private void LogRequest( + HttpContext context, + long timestamp, + bool isRequestStart, + string? requestBody, + string? responseBody, + Exception? exception = null) + { + const int StatusCodeOnException = 0; + const int LowestUnsuccessfulStatusCode = 400; + + // Don't get an enrichment bag for "RequestStart" log record: + var enrichmentBag = isRequestStart || _enrichers.Length == 0 + ? null + : LogMethodHelper.GetHelper(); + + var requestHeaders = _logRequestHeaders + ? _headersPool.Get() + : null; + + // Checking response headers, since we can possibly don't have a response: + var responseHeaders = _logResponseHeaders && context.Response.Headers.Count > 0 + ? _headersPool.Get() + : null; + + var logRecord = _logRecordPool.Get(); + try + { + logRecord.RequestBody = requestBody; + logRecord.ResponseBody = responseBody; + logRecord.RequestHeaders = requestHeaders; + logRecord.ResponseHeaders = responseHeaders; + + if (requestHeaders != null) + { + _requestHeadersReader.Read(context.Request.Headers, requestHeaders); + } + + if (responseHeaders != null) + { + _responseHeadersReader.Read(context.Response.Headers, responseHeaders); + } + + FillLogRecord(logRecord, context, enrichmentBag); + + if (isRequestStart) + { + // Don't emit both status code and duration dimensions on request start: + logRecord.Duration = null; + logRecord.StatusCode = null; + } + else + { + // Catching duration at the end: + logRecord.Duration = (long)TimeProvider.GetElapsedTime(timestamp, TimeProvider.GetTimestamp()).TotalMilliseconds; + } + + if (exception == null) + { + _logger.IncomingRequest(logRecord); + } + else + { + // Logging status code == 0 when exception occurs and no middleware has set a meaningful status code: + if (logRecord.StatusCode < LowestUnsuccessfulStatusCode) + { + logRecord.StatusCode = StatusCodeOnException; + } + + _logger.RequestProcessingError(exception, logRecord); + } + } + finally + { + if (logRecord.PathParameters != null) + { + ArrayPool.Shared.Return(logRecord.PathParameters); + logRecord.PathParameters = null; + } + + if (enrichmentBag != null) + { + LogMethodHelper.ReturnHelper(enrichmentBag); + logRecord.EnrichmentPropertyBag = null; + } + + if (requestHeaders != null) + { + _headersPool.Return(requestHeaders); + logRecord.RequestHeaders = null; + } + + if (responseHeaders != null) + { + _headersPool.Return(responseHeaders); + logRecord.ResponseHeaders = null; + } + + // Return log record at the end: + _logRecordPool.Return(logRecord); + } + } + + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Intentional")] + private async Task GetRequestBodyAsync(HttpRequest request, CancellationToken token) + { + if (!MediaTypeSetExtensions.Covers(_requestMediaTypes, request.ContentType)) + { + return null; + } + + try + { + var sequence = + await request.ReadBodyAsync(RequestBodyReadTimeout, BodyReadSizeLimit, token) + .ConfigureAwait(false); + +#if NET5_0_OR_GREATER + var stringifiedBody = Encoding.UTF8.GetString(in sequence); +#else + string stringifiedBody; + if (sequence.IsSingleSegment) + { + stringifiedBody = Encoding.UTF8.GetString(sequence.FirstSpan); + } + else + { + var buffer = ArrayPool.Shared.Rent((int)sequence.Length); + try + { + sequence.CopyTo(buffer); + stringifiedBody = Encoding.UTF8.GetString(buffer.AsSpan(0, (int)sequence.Length)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } +#endif + + return stringifiedBody; + } + catch (OperationCanceledException) + { + // Rethrow cancellation exceptions. + throw; + } + catch (Exception ex) + { + // We are intentionally catching and logging any exceptions which may happen. + _logger.ErrorReadingRequestBody(ex); + return null; + } + } + + private void FillLogRecord( + IncomingRequestLogRecord logRecord, + HttpContext context, + LogMethodHelper? enrichmentBag) + { + var request = context.Request; + var response = context.Response; + + string path = TelemetryConstants.Unknown; + var pathParamsCount = 0; + + if (_parameterRedactionMode != HttpRouteParameterRedactionMode.None) + { + var endpoint = context.GetEndpoint() as RouteEndpoint; + + if (endpoint?.RoutePattern.RawText != null) + { + var httpRoute = endpoint.RoutePattern.RawText; + var paramsToRedact = _httpRouteUtility.GetSensitiveParameters(httpRoute, request, _parametersToRedactMap); + + var routeSegments = _httpRouteParser.ParseRoute(httpRoute); + + if (_requestPathLogMode == IncomingPathLoggingMode.Formatted) + { + path = _httpRouteFormatter.Format(in routeSegments, request.Path, _parameterRedactionMode, paramsToRedact); + logRecord.PathParameters = null; + } + else + { + // Case when logging mode is IncomingPathLoggingMode.Structured + path = httpRoute; + var routeParams = ArrayPool.Shared.Rent(routeSegments.ParameterCount); + + // Setting this value right away to be able to return it back to pool in a callee's "finally" block: + logRecord.PathParameters = routeParams; + if (_httpRouteParser.TryExtractParameters(request.Path, in routeSegments, _parameterRedactionMode, paramsToRedact, ref routeParams)) + { + pathParamsCount = routeSegments.ParameterCount; + } + } + } + else + { + logRecord.PathParameters = null; + } + } + else if (request.Path.HasValue) + { + path = request.Path.Value!; + } + + // We need to set all the values (logRecord was taken from the pool): + logRecord.Path = path; + logRecord.PathParametersCount = pathParamsCount; + logRecord.Method = request.Method; + logRecord.StatusCode = response.StatusCode; + logRecord.Host = request.Host.Value; + + if (enrichmentBag != null) + { + foreach (var enricher in _enrichers) + { + enricher.Enrich(enrichmentBag, request, response); + } + } + + logRecord.EnrichmentPropertyBag = enrichmentBag; + } + + private bool ShouldExcludePath(string path) + { + foreach (var excludedPath in _excludePathStartsWith) + { + if (path.StartsWith(excludedPath, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/HttpRequestBodyReader.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/HttpRequestBodyReader.cs new file mode 100644 index 0000000000..32805740d8 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/HttpRequestBodyReader.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging; + +internal static class HttpRequestBodyReader +{ + internal const string ReadCancelled = "[read-cancelled]"; + + private static readonly ReadOnlySequence _readCancelled = new(Encoding.UTF8.GetBytes(ReadCancelled)); + + public static async ValueTask> ReadBodyAsync( + this HttpRequest request, + TimeSpan readTimeout, + int readSizeLimit, + CancellationToken token) + { + using var joinedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); + joinedTokenSource.CancelAfter(readTimeout); + + try + { + /* + * We enable buffering with max threshold and max limit. + * - Max threshold practically means we don't start writing into a file. + * - Max limit practically means we won't get a capacity exception. + */ + request.EnableBuffering(int.MaxValue, long.MaxValue); + + return await request.BodyReader.ReadAsync(readSizeLimit, joinedTokenSource.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Token source hides triggering token (https://github.com/dotnet/runtime/issues/22172) + if (!token.IsCancellationRequested) + { + return _readCancelled; + } + + throw; + } + finally + { + if (request.Body.CanSeek) + { + _ = request.Body.Seek(0, SeekOrigin.Begin); + } + } + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/IncomingRequestLogRecord.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/IncomingRequestLogRecord.cs new file mode 100644 index 0000000000..a0149fda05 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/IncomingRequestLogRecord.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging; + +internal sealed class IncomingRequestLogRecord +{ + /// + /// Gets or sets a request host. + /// + public string Host { get; set; } = string.Empty; + + /// + /// Gets or sets a request method. + /// + public string Method { get; set; } = string.Empty; + + /// + /// Gets or sets a request path. + /// + public string Path { get; set; } = string.Empty; + + /// + /// Gets or sets request path parameters. + /// + public HttpRouteParameter[]? PathParameters { get; set; } + + /// + /// Gets or sets request path parameters count for . + /// + public int PathParametersCount { get; set; } + + /// + /// Gets or sets a request's duration in milliseconds. + /// + public long? Duration { get; set; } + + /// + /// Gets or sets response status code. + /// + public int? StatusCode { get; set; } + + /// + /// Gets or sets a list of request headers. + /// + public List>? RequestHeaders { get; set; } + + /// + /// Gets or sets a list of response headers. + /// + public List>? ResponseHeaders { get; set; } + + /// + /// Gets or sets enrichment properties. + /// + public LogMethodHelper? EnrichmentPropertyBag { get; set; } + + /// + /// Gets or sets parsed request body. + /// + public string? RequestBody { get; set; } + + /// + /// Gets or sets parsed response body. + /// + public string? ResponseBody { get; set; } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/Log.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/Log.cs new file mode 100644 index 0000000000..da52a2d748 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/Log.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging; + +#pragma warning disable S109 + +internal static partial class Log +{ + internal const LogLevel DefaultLogLevel = LogLevel.Information; + internal const LogLevel ErrorLogLevel = LogLevel.Error; + internal const string OriginalFormatValue = ""; + internal const string ReadingRequestBodyError = "Error on reading HTTP request body."; + internal const string ReadingResponseBodyError = "Error on reading HTTP response body."; + + private const int IncomingRequestEventId = 1; + private const int RequestProcessingErrorEventId = 2; + + #region Non-generated logging + + public static void IncomingRequest(this ILogger logger, IncomingRequestLogRecord req) + { + if (logger.IsEnabled(DefaultLogLevel)) + { + var collector = req.EnrichmentPropertyBag ?? LogMethodHelper.GetHelper(); + + try + { + collector.ParameterName = string.Empty; + HttpLogPropertiesProvider.GetProperties(collector, req); + + logger.Log( + DefaultLogLevel, + new(IncomingRequestEventId, nameof(IncomingRequest)), + new IncomingRequestStruct(collector), + null, + static (_, _) => OriginalFormatValue); + } + finally + { + // Stryker disable once all + if (collector != req.EnrichmentPropertyBag) + { + LogMethodHelper.ReturnHelper(collector); + } + } + } + } + + public static void RequestProcessingError(this ILogger logger, Exception ex, IncomingRequestLogRecord req) + { + if (logger.IsEnabled(ErrorLogLevel)) + { + var collector = req.EnrichmentPropertyBag ?? LogMethodHelper.GetHelper(); + + try + { + collector.ParameterName = string.Empty; + HttpLogPropertiesProvider.GetProperties(collector, req); + + logger.Log( + ErrorLogLevel, + new(RequestProcessingErrorEventId, nameof(RequestProcessingError)), + new IncomingRequestStruct(collector), + ex, + static (_, _) => OriginalFormatValue); + } + finally + { + // Stryker disable once all + if (collector != req.EnrichmentPropertyBag) + { + LogMethodHelper.ReturnHelper(collector); + } + } + } + } + + internal readonly struct IncomingRequestStruct : IReadOnlyList> + { + private readonly LogMethodHelper _collector; + + public IncomingRequestStruct(LogMethodHelper collector) + { + _collector = collector; + } + + public int Count + => _collector.Count; + + public KeyValuePair this[int index] + => _collector[index]; + + public IEnumerator> GetEnumerator() + { + for (int i = 0; i < Count; i++) + { + yield return this[i]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + } + + #endregion + + [LogMethod(3, LogLevel.Error, ReadingRequestBodyError)] + public static partial void ErrorReadingRequestBody(this ILogger logger, Exception ex); + + [LogMethod(4, LogLevel.Error, ReadingResponseBodyError)] + public static partial void ErrorReadingResponseBody(this ILogger logger, Exception ex); + +#pragma warning disable R9G001 + [LogMethod(5, LogLevel.Warning, + $"HttpLogging middleware is injected into application pipeline, but {nameof(LogLevel)} '{{logLevel}}' is disabled in logger. " + + "Remove {methodName}() call from pipeline configuration in that case.")] + public static partial void MiddlewareIsMisused(this ILogger logger, LogLevel logLevel, string methodName); +#pragma warning restore R9G001 +} + diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/LoggingOptionsValidator.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/LoggingOptionsValidator.cs new file mode 100644 index 0000000000..851a41a3c8 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/LoggingOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging; + +[OptionsValidator] +internal sealed partial class LoggingOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/MediaTypeSetExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/MediaTypeSetExtensions.cs new file mode 100644 index 0000000000..20a7f65591 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/MediaTypeSetExtensions.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Mvc.Formatters; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging; + +internal static class MediaTypeSetExtensions +{ + public static bool Covers(this MediaType[] supportedMediaTypes, string? mediaTypeToCheck) + { + if (string.IsNullOrEmpty(mediaTypeToCheck)) + { + return false; + } + + var sampleContentType = new MediaType(mediaTypeToCheck); + foreach (var supportedMediaType in supportedMediaTypes) + { + if (sampleContentType.IsSubsetOf(supportedMediaType)) + { + return true; + } + } + + return false; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/PipeReaderExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/PipeReaderExtensions.cs new file mode 100644 index 0000000000..3aa68c4e6f --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/PipeReaderExtensions.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging; + +internal static class PipeReaderExtensions +{ + [SuppressMessage("Major Code Smell", "S125:Sections commented out", Justification = "Diagram")] + public static async Task> ReadAsync(this PipeReader pipeReader, int numBytes, + CancellationToken token) + { + long pointer = 0L; + while (true) + { + ReadResult result; + try + { + result = await pipeReader.ReadAsync(token).ConfigureAwait(false); + } + catch (TaskCanceledException ex) + { + throw new OperationCanceledException(ex.Message, ex, ex.CancellationToken); + } + + /* + * Move pointer to (N*buffer) lower than numBytes. + * +---------+ +---------+ +---------+ + * ||||||||||| ||||||||||| | | + * +---------+ +---------+ +-+-------+ + * ^ ^ ^ + * pointer ------ + | | + * num bytes -----------+ | + * advanced to -------------------+ + * + */ + + if (!result.IsCompleted && result.Buffer.Length < numBytes) + { + var bufferStart = result.Buffer.Start; + var bufferEnd = result.Buffer.End; + + pipeReader.AdvanceTo(bufferStart, bufferEnd); + pointer += bufferEnd.GetInteger() - bufferStart.GetInteger(); + + continue; + } + + /* + * Move pointer by bytes remaining after (N*buffer). + * +---------+ +---------+ +---------+ + * ||||||||||| ||||||||||| ||| | + * +---------+ +---------+ +-+-------+ + * ^ ^ + * pointer -----------+ | + * num bytes -----------+ | + * advanced to -------------------+ + * + */ + if (!result.IsCompleted && pointer < numBytes) + { + pointer = numBytes; + } + + return result.Buffer.Slice(0L, pointer); + } + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/ResponseInterceptingStream.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/ResponseInterceptingStream.cs new file mode 100644 index 0000000000..115f6091f1 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/ResponseInterceptingStream.cs @@ -0,0 +1,224 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.IO; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Shared.Pools; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging; + +/// +/// Intercepts data being written to stream and writes its copy to another data structure. +/// +/// +/// Copied from ASP .NET and adjusted to R9 needs. +/// +internal sealed class ResponseInterceptingStream : Stream, IHttpResponseBodyFeature +{ + private const bool LeavePipeWriterOpened = true; + private static readonly StreamPipeWriterOptions _pipeWriterOptions + = new(leaveOpen: LeavePipeWriterOpened); + + private PipeWriter? _pipeAdapter; + + public PipeWriter Writer => _pipeAdapter ??= PipeWriter.Create(this, _pipeWriterOptions); + + public Stream Stream => this; + + public override bool CanSeek => InterceptedStream.CanSeek; + + public override bool CanRead => InterceptedStream.CanRead; + + public override bool CanWrite => InterceptedStream.CanWrite; + + public override long Length => InterceptedStream.Length; + + public override long Position + { + get => InterceptedStream.Position; + set => InterceptedStream.Position = value; + } + + public override int WriteTimeout + { + get => InterceptedStream.WriteTimeout; + set => InterceptedStream.WriteTimeout = value; + } + + /// + /// Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + /// Justification: I need parameterless ctor to use pooling on this object. I don't want to declare it as nullable because + /// using current pattern it is never null after initialization. + /// +#pragma warning disable CS8618 + public ResponseInterceptingStream() + { + } +#pragma warning restore CS8618 + + public ResponseInterceptingStream( + Stream interceptedStream, + IHttpResponseBodyFeature responseBodyFeature, + BufferWriter bufferWriter, + int interceptedValueWriteLimit) + { + InterceptedStream = interceptedStream; + InnerBodyFeature = responseBodyFeature; + InterceptedValueBuffer = bufferWriter; + InterceptedValueWriteLimit = interceptedValueWriteLimit; + } + + internal Stream InterceptedStream { get; set; } + + internal IHttpResponseBodyFeature InnerBodyFeature { get; set; } + + internal int InterceptedValueWriteLimit { get; set; } + + internal BufferWriter InterceptedValueBuffer { get; set; } + + public override void Flush() + { + InterceptedStream.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return InterceptedStream.FlushAsync(cancellationToken); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return InterceptedStream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + InterceptedStream.SetLength(value); + } + +#if NET5_0_OR_GREATER + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + { + return InterceptedStream.BeginWrite(buffer, offset, count, callback, state); + } +#else + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object? state) + { + return InterceptedStream.BeginWrite(buffer, offset, count, callback, state); + } +#endif + + public override void EndWrite(IAsyncResult asyncResult) + { + InterceptedStream.EndWrite(asyncResult); + } + + public override int Read(Span buffer) + { + return InterceptedStream.Read(buffer); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return InterceptedStream.Read(buffer, offset, count); + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + return InterceptedStream.ReadAsync(buffer, cancellationToken); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return InterceptedStream.ReadAsync(buffer, offset, count, cancellationToken); + } + +#if NET5_0_OR_GREATER + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + { + return InterceptedStream.BeginRead(buffer, offset, count, callback, state); + } +#else + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object? state) + { + return InterceptedStream.BeginRead(buffer, offset, count, callback, state); + } +#endif + + public override int EndRead(IAsyncResult asyncResult) + { + return InterceptedStream.EndRead(asyncResult); + } + + public override void CopyTo(Stream destination, int bufferSize) + { + InterceptedStream.CopyTo(destination, bufferSize); + } + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + return InterceptedStream.CopyToAsync(destination, bufferSize, cancellationToken); + } + + public override async ValueTask DisposeAsync() + { + await base.DisposeAsync().ConfigureAwait(false); + await InterceptedStream.DisposeAsync().ConfigureAwait(false); + } + + public Task StartAsync(CancellationToken cancellationToken = default) + { + return InnerBodyFeature.StartAsync(cancellationToken); + } + + public override void Write(byte[] buffer, int offset, int count) + { + Write(buffer.AsSpan(offset, count)); + } + + public override void Write(ReadOnlySpan buffer) + { + var valueToWriteUntilLimit = InterceptedValueWriteLimit - InterceptedValueBuffer.WrittenCount; + var innerCount = Math.Min(valueToWriteUntilLimit, buffer.Length); + + InterceptedValueBuffer.Write(buffer.Slice(0, innerCount)); + InterceptedStream.Write(buffer); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return WriteAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + var valueToWriteUntilLimit = InterceptedValueWriteLimit - InterceptedValueBuffer.WrittenCount; + var innerCount = Math.Min(valueToWriteUntilLimit, buffer.Length); + + InterceptedValueBuffer.Write(buffer.Span.Slice(0, innerCount)); + + return InterceptedStream.WriteAsync(buffer, cancellationToken); + } + + public void DisableBuffering() + { + InnerBodyFeature.DisableBuffering(); + } + + public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default) + { + return InnerBodyFeature.SendFileAsync(path, offset, count, cancellationToken); + } + + public Task CompleteAsync() + { + return InnerBodyFeature.CompleteAsync(); + } + + internal ReadOnlyMemory GetInterceptedSequence() => InterceptedValueBuffer.WrittenMemory; +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/ResponseInterceptingStreamPool.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/ResponseInterceptingStreamPool.cs new file mode 100644 index 0000000000..c9907c681e --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/Internal/ResponseInterceptingStreamPool.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.Pools; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging; + +internal static class ResponseInterceptingStreamPool +{ + private static readonly StreamResponseBodyFeature _dummyBodyFeature = new(Stream.Null); + private static readonly Stream _dummyStream = Stream.Null; + private static readonly BufferWriter _dummyBufferWriter = new(); + + private static ObjectPool StreamPool { get; } + = PoolFactory.CreatePool(); + + private static ObjectPool> BufferWriterPool { get; } = Microsoft.Shared.Pools.BufferWriterPool.SharedBufferWriterPool; + + public static ResponseInterceptingStream Get(IHttpResponseBodyFeature innerBodyFeature, int limit) + { + var instance = StreamPool.Get(); + var bufferWriter = BufferWriterPool.Get(); + + instance.InnerBodyFeature = innerBodyFeature; + instance.InterceptedStream = innerBodyFeature.Stream; + instance.InterceptedValueWriteLimit = limit; + instance.InterceptedValueBuffer = bufferWriter; + + return instance; + } + + public static void Return(ResponseInterceptingStream stream) + { + stream.InnerBodyFeature = _dummyBodyFeature; + stream.InterceptedStream = _dummyStream; + stream.InterceptedValueWriteLimit = 0; + + BufferWriterPool.Return(stream.InterceptedValueBuffer); + stream.InterceptedValueBuffer = _dummyBufferWriter; + + StreamPool.Return(stream); + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/LoggingOptions.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/LoggingOptions.cs new file mode 100644 index 0000000000..0433145b15 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Logging/LoggingOptions.cs @@ -0,0 +1,203 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Shared.Data.Validation; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Top-level model for formatting incoming HTTP requests and their corresponding responses. +/// +public class LoggingOptions +{ + private const int Millisecond = 1; + private const int Minute = 60_000; + private const int MaxBodyReadSize = 1_572_864; // 1.5 MB + private const int DefaultBodyReadSizeLimit = 32 * 1024; // ≈ 32K + private const IncomingPathLoggingMode DefaultRequestPathLoggingMode = IncomingPathLoggingMode.Formatted; + private const HttpRouteParameterRedactionMode DefaultPathParameterRedactionMode = HttpRouteParameterRedactionMode.Strict; + + private static readonly TimeSpan _defaultReadTimeout = TimeSpan.FromSeconds(1); + + /// + /// Gets or sets a value indicating whether request will be logged additionally before any further processing. + /// + /// + /// When enabled, two entries will be logged for each incoming request. Note, that the first log record won't be enriched. + /// When disabled, only one entry will be logged for each incoming request (with corresponding response's data). + /// Default set to . + /// + public bool LogRequestStart { get; set; } + + /// + /// Gets or sets a value indicating whether HTTP request and response body will be logged. + /// + /// + /// Please avoid enabling this options in production environment as it might lead to leaking privacy information. + /// Default set to . + /// + public bool LogBody { get; set; } + + /// + /// Gets or sets a strategy how request path should be logged. + /// + /// + /// Make sure you add redactors to ensure that sensitive information doesn't find its way into your log records. + /// Default set to . + /// This option only applies when the + /// option is not set to . + /// + public IncomingPathLoggingMode RequestPathLoggingMode { get; set; } = DefaultRequestPathLoggingMode; + + /// + /// Gets or sets a value indicating how request path parameter should be redacted. + /// + /// + /// Default set to . + /// + [Experimental] + public HttpRouteParameterRedactionMode RequestPathParameterRedactionMode { get; set; } = DefaultPathParameterRedactionMode; + + /// + /// Gets or sets a maximum amount of time to wait for the request body to be read. + /// + /// + /// The number should be above 1 millisecond and below 1 minute. + /// Default set to 1 second. + /// + [TimeSpan(Millisecond, Minute)] + public TimeSpan RequestBodyReadTimeout { get; set; } = _defaultReadTimeout; + + /// + /// Gets or sets a value indicating the maximum number of bytes of the request/response body to be read. + /// + /// + /// The number should ideally be below 85K to not be allocated on the large object heap. + /// Default set to ≈ 32K. + /// + [Range(1, MaxBodyReadSize)] + public int BodySizeLimit { get; set; } = DefaultBodyReadSizeLimit; + + /// + /// Gets or sets a map between HTTP path parameters and their data classification. + /// + /// + /// Default set to an empty dictionary. + /// If a parameter within a controller's action is not annotated with a data classification attribute and + /// it's not found in this map, it will be redacted as if it was . + /// If you don't want a parameter to be redacted, mark it as . + /// + [Required] + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Options pattern.")] + public IDictionary RouteParameterDataClasses { get; set; } = new Dictionary(); + + /// + /// Gets or sets a map between request headers to be logged and their data classification. + /// + /// + /// Default set to an empty dictionary. + /// That means that no request header will be logged by default. + /// + [Required] + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", + Justification = "Options pattern.")] + public IDictionary RequestHeadersDataClasses { get; set; } = new Dictionary(); + + /// + /// Gets or sets the set of request body content types which are considered text and thus possible to log. + /// + /// + /// Make sure to not enable body logging in production environment, as it will cause + /// both performance impact and leakage of sensitive data. + /// If you need to log body in production, please go through compliance and security. + /// Default set to an empty . + /// That means that request's body will not be logged by default. + /// + /// + /// A typical set of known text content-types like json, xml or text would be: + /// + /// RequestBodyContentTypesToLog = new HashSet<string> + /// { + /// "application/*+json", + /// "application/*+xml", + /// "application/json", + /// "application/xml", + /// "text/*" + /// }; + /// + /// + [Required] + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", + Justification = "Options pattern.")] + public ISet RequestBodyContentTypes { get; set; } = new HashSet(); + + /// + /// Gets or sets a map between response headers to be logged and their data classification. + /// + /// + /// Default set to an empty dictionary. + /// That means that no response header will be logged by default. + /// + [Required] + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", + Justification = "Options pattern.")] + public IDictionary ResponseHeadersDataClasses { get; set; } = new Dictionary(); + + /// + /// Gets or sets the set of response body content types which are considered text and thus possible to log. + /// + /// + /// Make sure to not enable body logging in production environment, as it will cause + /// both performance impact and leakage of sensitive data. + /// If you need to log body in production, please go through compliance and security. + /// Default set to an empty . + /// That means that response's body will not be logged by default. + /// + /// + /// A typical set of known text content-types like json, xml or text would be: + /// + /// ResponseBodyContentTypesToLog = new HashSet<string> + /// { + /// "application/*+json", + /// "application/*+xml", + /// "application/json", + /// "application/xml", + /// "text/*" + /// }; + /// + /// + [Required] + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", + Justification = "Options pattern.")] + public ISet ResponseBodyContentTypes { get; set; } = new HashSet(); + + /// + /// Gets or sets the set of HTTP paths that should be excluded from logging. + /// + /// + /// Any path added to the set will not be logged. + /// Paths are case insensitive. + /// Default set to an empty . + /// + /// + /// A typical set of HTTP paths would be: + /// + /// ExcludePathStartsWith = new HashSet<string> + /// { + /// "/probe/live", + /// "/probe/ready" + /// }; + /// + /// + [Experimental] + [Required] + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", + Justification = "Options pattern.")] + public ISet ExcludePathStartsWith { get; set; } = new HashSet(); +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/HttpMeteringBuilder.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/HttpMeteringBuilder.cs new file mode 100644 index 0000000000..ad9a810d75 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/HttpMeteringBuilder.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Interface for creating a builder. +/// +public class HttpMeteringBuilder +{ + /// + /// Initializes a new instance of the class. + /// + /// Application services. + public HttpMeteringBuilder(IServiceCollection services) + { + Services = services; + } + + /// + /// Gets the application service collection. + /// + public IServiceCollection Services { get; private set; } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/HttpMeteringExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/HttpMeteringExtensions.cs new file mode 100644 index 0000000000..3378363436 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/HttpMeteringExtensions.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Telemetry.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Extension methods to register http metering and metric enrichers with the service. +/// +public static class HttpMeteringExtensions +{ + /// + /// Adds an enricher instance of to the to enrich incoming request metrics. + /// + /// Type of enricher. + /// The to add the instance of to. + /// The so that additional calls can be chained. + public static HttpMeteringBuilder AddMetricEnricher(this HttpMeteringBuilder builder) + where T : class, IIncomingRequestMetricEnricher + { + _ = Throw.IfNull(builder); + + _ = builder.Services.AddSingleton(); + + return builder; + } + + /// + /// Adds to the to enrich incoming request metrics. + /// + /// The to add to. + /// The instance of to add to . + /// The so that additional calls can be chained. + public static HttpMeteringBuilder AddMetricEnricher( + this HttpMeteringBuilder builder, + IIncomingRequestMetricEnricher enricher) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(enricher); + + _ = builder.Services.AddSingleton(enricher); + + return builder; + } + + /// + /// Adds a middleware to the specified . + /// + /// The to add the middleware to. + /// The so that additional calls can be chained. + public static IApplicationBuilder UseHttpMetering(this IApplicationBuilder builder) + { + return builder.UseMiddleware(Array.Empty()); + } + + /// + /// Adds incoming request metric auto-collection to . + /// + /// Collection of services. + /// Enriched collection of services. + public static IServiceCollection AddHttpMetering(this IServiceCollection services) => services.AddHttpMetering(null); + + /// + /// Adds incoming request metric auto-collection to . + /// + /// Collection of services. + /// Function to configure http metering options. + /// Enriched collection of services. + public static IServiceCollection AddHttpMetering(this IServiceCollection services, Action? build) + { + var builder = new HttpMeteringBuilder(services); + + // Invoke passed redact builder func if not null. + build?.Invoke(builder); + + _ = services.RegisterMetering(); + services.TryAddSingleton(); + + return services; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/IIncomingRequestMetricEnricher.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/IIncomingRequestMetricEnricher.cs new file mode 100644 index 0000000000..50bc336d66 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/IIncomingRequestMetricEnricher.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Interface for implementing enrichers for request metrics. +/// +public interface IIncomingRequestMetricEnricher : IMetricEnricher +{ + /// + /// Gets a list of dimension names to enrich incoming request metrics. + /// + IReadOnlyList DimensionNames { get; } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/Internal/HttpContextExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/Internal/HttpContextExtensions.cs new file mode 100644 index 0000000000..6fff029889 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/Internal/HttpContextExtensions.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Microsoft.AspNetCore.Telemetry.Internal; + +/// +/// Extensions for . +/// +internal static class HttpContextExtensions +{ + /// + /// Gets a route template from . + /// + /// HTTP context of a given request. + /// Raw route template string. + public static string? GetRouteTemplate(this HttpContext context) + { + var endpoint = context.GetEndpoint() as RouteEndpoint; + return endpoint?.RoutePattern.RawText; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/Internal/HttpMeteringMiddleware.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/Internal/HttpMeteringMiddleware.cs new file mode 100644 index 0000000000..19e2b849e3 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/Internal/HttpMeteringMiddleware.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Pools; +using Microsoft.Shared.Text; + +namespace Microsoft.AspNetCore.Telemetry.Internal; + +/// +/// Records the duration of all incoming requests and logs as a metric. +/// +internal sealed class HttpMeteringMiddleware : IMiddleware +{ + internal TimeProvider TimeProvider = TimeProvider.System; + + private const int StandardDimensionsCount = 4; + private const int MaxCustomDimensionsCount = 15; + + private readonly Histogram? _incomingRequestMetric; + private readonly IIncomingRequestMetricEnricher[]? _requestMetricEnrichers; + private readonly ObjectPool _propertyBagPool = PoolFactory.CreateResettingPool(); + + /// + /// Initializes a new instance of the class. + /// A middleware that records incoming request duration. + /// + /// Meter used for metric logging. + /// Enumerable of request metric enrichers. + public HttpMeteringMiddleware(Meter meter, IEnumerable requestMetricEnrichers) + { + int dimensionsCount = StandardDimensionsCount; + int enrichersCount = 0; + foreach (var enricher in requestMetricEnrichers) + { + enrichersCount++; + dimensionsCount += enricher.DimensionNames.Count; + } + + if (dimensionsCount > MaxCustomDimensionsCount + StandardDimensionsCount) + { + Throw.ArgumentOutOfRangeException( + $"Total dimensions added by all request metric enrichers should be smaller than {MaxCustomDimensionsCount}. Observed count: {dimensionsCount - StandardDimensionsCount}", + nameof(requestMetricEnrichers)); + } + + var dimensionsSet = new HashSet + { + Metric.ReqHost, + Metric.ReqName, + Metric.RspResultCode, + Metric.ExceptionType + }; + + if (enrichersCount > 0) + { + _requestMetricEnrichers = new IIncomingRequestMetricEnricher[enrichersCount]; + + int enricherIndex = 0; + foreach (var enricher in requestMetricEnrichers) + { + _requestMetricEnrichers[enricherIndex++] = enricher; + foreach (var dimensionName in enricher.DimensionNames) + { + if (!dimensionsSet.Add(dimensionName)) + { + Throw.ArgumentException(dimensionName, $"A dimension with name {dimensionName} already exists in one of the registered request metric enricher"); + } + } + } + } + + _incomingRequestMetric = meter.CreateHistogram(Metric.IncomingRequestMetricName); + } + + /// + /// Request handling method. + /// + /// The for the current request. + /// The delegate representing the remaining middleware in the request pipeline. + /// A that represents the execution of this middleware. + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + var startTimestamp = TimeProvider.GetTimestamp(); + + try + { + await next(context).ConfigureAwait(false); + OnRequestEnd(context, startTimestamp, context.Response.StatusCode, null); + } + catch (Exception ex) + { + int resultCode = context.Response.StatusCode < StatusCodes.Status400BadRequest ? StatusCodes.Status500InternalServerError : context.Response.StatusCode; + OnRequestEnd(context, startTimestamp, resultCode, ex.GetType()); + + throw; + } + } + + private void OnRequestEnd(HttpContext httpContext, long timestamp, int resultCode, Type? exceptionType) + { + string requestHost = string.IsNullOrWhiteSpace(httpContext.Request.Host.Value) ? "unknown_host_name" : httpContext.Request.Host.Value; + string requestName = $"{httpContext.Request.Method} {httpContext.GetRouteTemplate() ?? "unsupported_route"}"; + string responseResultCode = resultCode.ToInvariantString(); + string exceptionTypeName = exceptionType?.FullName ?? "no_exception"; + long duration = (long)TimeProvider.GetElapsedTime(timestamp, TimeProvider.GetTimestamp()).TotalMilliseconds; + + var tagList = new TagList + { + new(Metric.ReqHost, requestHost), + new(Metric.ReqName, requestName), + new(Metric.RspResultCode, responseResultCode), + new(Metric.ExceptionType, exceptionTypeName), + }; + + // keep default case fast by avoiding allocations + if (_requestMetricEnrichers == null) + { + _incomingRequestMetric!.Record(value: duration, tagList); + } + else + { + var requestEnrichmentPropertyBag = _propertyBagPool.Get(); + try + { + foreach (var enricher in _requestMetricEnrichers) + { + enricher.Enrich(requestEnrichmentPropertyBag); + } + + foreach (var item in requestEnrichmentPropertyBag) + { + tagList.Add(item.Key, item.Value); + } + + _incomingRequestMetric!.Record(value: duration, tagList); + } + finally + { + _propertyBagPool.Return(requestEnrichmentPropertyBag); + } + } + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/Internal/Metric.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/Internal/Metric.cs new file mode 100644 index 0000000000..ff197b668b --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Metering/Internal/Metric.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +namespace Microsoft.AspNetCore.Telemetry.Internal; + +[ExcludeFromCodeCoverage] +internal static partial class Metric +{ + internal const string IncomingRequestMetricName = @"R9\Http\IncomingRequest"; + + /// + /// The host part of the incoming request URL. + /// + internal const string ReqHost = "req_host"; + + /// + /// The name of the incoming request. + /// + internal const string ReqName = "req_name"; + + /// + /// The response status code for the request. + /// + internal const string RspResultCode = "rsp_resultCode"; + + /// + /// The type of the exception thrown during processing of the request. + /// + internal const string ExceptionType = "env_ex_type"; + + /// + /// Creates a new histogram instrument for incoming HTTP request. + /// + /// Meter object. + /// + [Histogram(ReqHost, ReqName, RspResultCode, ExceptionType, Name = IncomingRequestMetricName)] + public static partial IncomingRequestMetric CreateHistogram(Meter meter); +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Microsoft.AspNetCore.Telemetry.Middleware.csproj b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Microsoft.AspNetCore.Telemetry.Middleware.csproj new file mode 100644 index 0000000000..42486a8523 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry.Middleware/Microsoft.AspNetCore.Telemetry.Middleware.csproj @@ -0,0 +1,46 @@ + + + Microsoft.AspNetCore.Telemetry + ASP.NET Core middleware for collecting high-quality telemetry. + $(PackageTags);aspnetcore + Telemetry + + + + $(NetCoreTargetFrameworks) + true + true + true + false + false + true + false + false + true + false + + + + normal + 100 + 85 + + + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry/Enrichment.RequestHeaders/RequestHeadersEnricherExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry/Enrichment.RequestHeaders/RequestHeadersEnricherExtensions.cs new file mode 100644 index 0000000000..9e98d6dc31 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry/Enrichment.RequestHeaders/RequestHeadersEnricherExtensions.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Extension methods for setting up Request Headers Log Enricher in an . +/// +public static class RequestHeadersEnricherExtensions +{ + /// + /// Adds an instance of Request Headers Log Enricher to the . + /// + /// The to add the Request Headers Log Enricher to. + /// The so that additional calls can be chained. + /// The is . + public static IServiceCollection AddRequestHeadersLogEnricher(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + return services + .AddLogEnricherOptions(_ => { }) + .RegisterRequestHeadersEnricher(); + } + + /// + /// Adds an instance of Request Headers Log Enricher to the . + /// + /// The to add the Request Headers Log Enricher to. + /// The configuration delegate. + /// The so that additional calls can be chained. + /// The is . + public static IServiceCollection AddRequestHeadersLogEnricher(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + return services + .AddLogEnricherOptions(configure) + .RegisterRequestHeadersEnricher(); + } + + /// + /// Adds an instance of Request Headers Log Enricher to the . + /// + /// The to add the Request Headers Log Enricher to. + /// The to use for configuring + /// in the Request Headers Log Enricher. + /// The so that additional calls can be chained. + /// The is . + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(RequestHeadersLogEnricherOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed with [DynamicDependency]")] + public static IServiceCollection AddRequestHeadersLogEnricher(this IServiceCollection services, IConfigurationSection section) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(section); + + return services + .Configure(section) + .AddLogEnricherOptions(_ => { }) + .RegisterRequestHeadersEnricher(); + } + + private static IServiceCollection RegisterRequestHeadersEnricher(this IServiceCollection services) + { + return services + .AddHttpContextAccessor() + .AddLogEnricher(); + } + + private static IServiceCollection AddLogEnricherOptions( + this IServiceCollection services, + Action configure) + { + _ = services + .AddValidatedOptions() + .Configure(configure); + + return services; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry/Enrichment.RequestHeaders/RequestHeadersLogEnricher.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry/Enrichment.RequestHeaders/RequestHeadersLogEnricher.cs new file mode 100644 index 0000000000..07eba0631e --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry/Enrichment.RequestHeaders/RequestHeadersLogEnricher.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Frozen; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Enriches logs with Request headers information. +/// +internal sealed class RequestHeadersLogEnricher : ILogEnricher +{ + private readonly FrozenDictionary _headersDataClasses; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IRedactorProvider? _redactorProvider; + + /// + /// Initializes a new instance of the class. + /// + /// HttpContextAccessor responsible for obtaining properties from HTTP context. + /// Options to customize configuration of . + /// RedactorProvidor to get redactor to redact enriched data according to the data class. + public RequestHeadersLogEnricher(IHttpContextAccessor httpContextAccessor, IOptions options, + IRedactorProvider? redactorProvider = null) + { + var opt = Throw.IfMemberNull(options, options.Value); + _httpContextAccessor = httpContextAccessor; + + _headersDataClasses = opt.HeadersDataClasses.Count == 0 + ? FrozenDictionary.Empty + : opt.HeadersDataClasses.ToFrozenDictionary(StringComparer.Ordinal, optimizeForReading: true); + + if (_headersDataClasses.Count > 0) + { + _redactorProvider = Throw.IfNull(redactorProvider); + } + } + + public void Enrich(IEnrichmentPropertyBag enrichmentBag) + { + if (_httpContextAccessor.HttpContext?.Request == null) + { + return; + } + + var request = _httpContextAccessor.HttpContext.Request; + + if (_headersDataClasses.Count == 0) + { + return; + } + + if (_headersDataClasses.Count != 0) + { + foreach (var header in _headersDataClasses) + { + if (request.Headers.TryGetValue(header.Key, out var headerValue) && !string.IsNullOrEmpty(headerValue)) + { + var redactor = _redactorProvider!.GetRedactor(header.Value); + var redacted = redactor.Redact(headerValue); + enrichmentBag.Add(header.Key, redacted); + } + } + } + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry/Enrichment.RequestHeaders/RequestHeadersLogEnricherOptions.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry/Enrichment.RequestHeaders/RequestHeadersLogEnricherOptions.cs new file mode 100644 index 0000000000..1f46f1d898 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry/Enrichment.RequestHeaders/RequestHeadersLogEnricherOptions.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Compliance.Classification; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Options for the Request Headers enricher. +/// +public class RequestHeadersLogEnricherOptions +{ + /// + /// Gets or sets a dictionary of header names that logs should be enriched with and their data classification. + /// + /// + /// Default value is an empty dictionary. + /// + [Required] + [Experimental] +#pragma warning disable CA2227 // Collection properties should be read only + public IDictionary HeadersDataClasses { get; set; } = new Dictionary(); +#pragma warning restore CA2227 // Collection properties should be read only +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry/Enrichment.RequestHeaders/RequestHeadersLogEnricherOptionsValidator.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry/Enrichment.RequestHeaders/RequestHeadersLogEnricherOptionsValidator.cs new file mode 100644 index 0000000000..43213594e4 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry/Enrichment.RequestHeaders/RequestHeadersLogEnricherOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.AspNetCore.Telemetry; + +[OptionsValidator] +internal sealed partial class RequestHeadersLogEnricherOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry/Microsoft.AspNetCore.Telemetry.csproj b/src/Libraries/Microsoft.AspNetCore.Telemetry/Microsoft.AspNetCore.Telemetry.csproj new file mode 100644 index 0000000000..6f3b933d98 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry/Microsoft.AspNetCore.Telemetry.csproj @@ -0,0 +1,55 @@ + + + Microsoft.AspNetCore.Telemetry + Provides canonical implementations of telemetry abstractions which depend on the ASP.NET pipeline + $(PackageTags);aspnetcore + Telemetry + + + + true + true + false + false + false + false + false + + + + normal + 79 + 100 + 90 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry/Telemetry.Internal.Http/HttpUtilityExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry/Telemetry.Internal.Http/HttpUtilityExtensions.cs new file mode 100644 index 0000000000..ec05d97541 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry/Telemetry.Internal.Http/HttpUtilityExtensions.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !NETCOREAPP3_1_OR_GREATER +using System.Linq; +#endif +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.AspNetCore.Telemetry.Internal; + +/// +/// Telemetry utility extensions for incoming http requests. +/// +internal static class HttpUtilityExtensions +{ + /// + /// Adds an implementation instance of to the service collection. + /// + /// Instance of . + /// return object. + public static IServiceCollection AddHttpRouteUtilities(this IServiceCollection services) + { + services.TryAddActivatedSingleton(); + return services; + } + +#if NETCOREAPP3_1_OR_GREATER + /// + /// Gets the request route for the http request. + /// + /// object. + /// Returns request route. + public static string GetRoute(this HttpRequest request) + { + _ = Throw.IfNull(request); + + var endpoint = request.HttpContext.GetEndpoint(); + if (endpoint is RouteEndpoint routeEndpoint) + { + return routeEndpoint.RoutePattern.RawText ?? string.Empty; + } + + return string.Empty; + } +#else + /// + /// Gets the request route for the http request. + /// + /// object. + /// Returns request route. + public static string GetRoute(this HttpRequest request) + { + _ = Throw.IfNull(request); + + var routeData = request.HttpContext.GetRouteData(); + + var routes = routeData?.Routers.OfType().FirstOrDefault(); + var route = string.Empty; + if (routes?.Count > 0) + { + route = routes[0].ToString(); + } + + return route; + } +#endif +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry/Telemetry.Internal.Http/IIncomingHttpRouteUtility.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry/Telemetry.Internal.Http/IIncomingHttpRouteUtility.cs new file mode 100644 index 0000000000..7c53abe9f8 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry/Telemetry.Internal.Http/IIncomingHttpRouteUtility.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Compliance.Classification; + +namespace Microsoft.AspNetCore.Telemetry.Internal; + +/// +/// Telemetry utilities for incoming http requests. +/// +internal interface IIncomingHttpRouteUtility +{ + /// + /// Gets a dictionary of sensitive parameters in the route with the data class. + /// + /// Http request's route template. + /// Http request object. + /// Default sensitive parameters to be applied to all routes. + /// A dictionary of parameter name to data class containing all sensitive parameters in the given route. + IReadOnlyDictionary GetSensitiveParameters(string httpRoute, HttpRequest request, IReadOnlyDictionary defaultSensitiveParameters); +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry/Telemetry.Internal.Http/IncomingHttpRouteUtility.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry/Telemetry.Internal.Http/IncomingHttpRouteUtility.cs new file mode 100644 index 0000000000..5390e772de --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry/Telemetry.Internal.Http/IncomingHttpRouteUtility.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETCOREAPP3_1_OR_GREATER +using System; +using System.Collections.Concurrent; +using System.Collections.Frozen; +#endif +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +#if NETCOREAPP3_1_OR_GREATER +using Microsoft.AspNetCore.Mvc.Controllers; +#endif +using Microsoft.Extensions.Compliance.Classification; + +namespace Microsoft.AspNetCore.Telemetry.Internal; + +internal sealed class IncomingHttpRouteUtility : IIncomingHttpRouteUtility +{ +#if NETCOREAPP3_1_OR_GREATER + private static readonly Type _dataClassificationAttributeType = typeof(DataClassificationAttribute); + private readonly ConcurrentDictionary> _parametersToRedactCache = new(); + + public IReadOnlyDictionary GetSensitiveParameters(string httpRoute, HttpRequest request, IReadOnlyDictionary defaultSensitiveParameters) + { + if (string.IsNullOrEmpty(httpRoute)) + { + return defaultSensitiveParameters; + } + + if (_parametersToRedactCache.TryGetValue(httpRoute, out var result)) + { + return result; + } + + var parametersToRedact = new Dictionary(); + foreach (var defaultParameter in defaultSensitiveParameters) + { + parametersToRedact.Add(defaultParameter.Key, defaultParameter.Value); + } + + var endpoint = request.HttpContext.GetEndpoint(); + var parameters = endpoint?.Metadata.GetMetadata()?.Parameters; + if (parameters != null) + { + foreach (var parameter in parameters) + { + var p = parameter as ControllerParameterDescriptor; + + if (p != null) + { + var dataClassificationAttributes = p.ParameterInfo.GetCustomAttributes(_dataClassificationAttributeType, true); + + if (dataClassificationAttributes.Length > 0) + { + var classification = ((DataClassificationAttribute)dataClassificationAttributes[0]).Classification; + parametersToRedact[p.ParameterInfo.Name!] = classification; + } + } + } + } + + return _parametersToRedactCache.GetOrAdd(httpRoute, static (_, paramsToRedact) => paramsToRedact.ToFrozenDictionary(StringComparer.Ordinal, optimizeForReading: true), parametersToRedact); + } +#else + public IReadOnlyDictionary GetSensitiveParameters(string httpRoute, HttpRequest request, IReadOnlyDictionary defaultSensitiveParameters) + { + return defaultSensitiveParameters; + } +#endif +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/Constants.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/Constants.cs new file mode 100644 index 0000000000..1a93ab3b3d --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/Constants.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Telemetry; + +internal static class Constants +{ + public const string AttributeHttpPath = "http.path"; + public const string AttributeHttpRoute = "http.route"; + public const string AttributeHttpTarget = "http.target"; + public const string AttributeHttpUrl = "http.url"; + public const string AttributeHttpHost = "http.host"; + public const string AttributeHttpScheme = "http.scheme"; + public const string AttributeHttpFlavor = "http.flavor"; + public const string AttributeNetHostName = "net.host.name"; + public const string AttributeNetHostPort = "net.host.port"; + public const string AttributeUserAgent = "http.user_agent"; + public const string CustomPropertyHttpRequest = "Tracing.CustomProperty.HttpRequest"; + public const string ActivityStartEvent = "OnStartActivity"; + public const string RequestNameUnknown = "UnknownRequest"; + public const string OtelStatusCode = "otel.status_code"; +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpTraceEnrichmentProcessor.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpTraceEnrichmentProcessor.cs new file mode 100644 index 0000000000..e7a1e156cf --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpTraceEnrichmentProcessor.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETCOREAPP3_1_OR_GREATER + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Telemetry; + +internal sealed class HttpTraceEnrichmentProcessor +{ + private readonly IHttpTraceEnricher[] _traceEnrichers; + + public HttpTraceEnrichmentProcessor(IEnumerable traceEnrichers) + { + _traceEnrichers = traceEnrichers.ToArray(); + } + + public void Enrich(Activity activity, HttpRequest request) + { + foreach (var enricher in _traceEnrichers) + { + enricher.Enrich(activity, request); + } + } +} + +#endif diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpTraceEnrichmentProcessor.netfx.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpTraceEnrichmentProcessor.netfx.cs new file mode 100644 index 0000000000..d33b584e1e --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpTraceEnrichmentProcessor.netfx.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !NETCOREAPP3_1_OR_GREATER + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNetCore.Http; +using OpenTelemetry; + +namespace Microsoft.AspNetCore.Telemetry; + +internal sealed class HttpTraceEnrichmentProcessor : BaseProcessor +{ + private readonly IHttpTraceEnricher[] _traceEnrichers; + private readonly HttpUrlRedactionProcessor _redactionProcessor; + + public HttpTraceEnrichmentProcessor(HttpUrlRedactionProcessor redactionProcessor, IEnumerable traceEnrichers) + { + _traceEnrichers = traceEnrichers.ToArray(); + _redactionProcessor = redactionProcessor; + } + + public void EnrichAndRedact(Activity activity, HttpRequest request) + { + foreach (var enricher in _traceEnrichers) + { + enricher.Enrich(activity, request); + } + + _redactionProcessor.Process(activity, request); + } + + public override void OnEnd(Activity activity) + { + HttpRequest? request = (HttpRequest?)activity.GetCustomProperty(Constants.CustomPropertyHttpRequest); + + if (request != null) + { + activity.SetCustomProperty(Constants.CustomPropertyHttpRequest, null); + + EnrichAndRedact(activity, request); + } + } +} + +#endif diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpTracingExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpTracingExtensions.cs new file mode 100644 index 0000000000..02aa2d705a --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpTracingExtensions.cs @@ -0,0 +1,183 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +#if !NETCOREAPP3_1_OR_GREATER +using Microsoft.AspNetCore.Http; +#endif +using Microsoft.AspNetCore.Telemetry.Internal; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Shared.Diagnostics; +using OpenTelemetry.Instrumentation.AspNetCore; +using OpenTelemetry.Trace; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Extensions for adding and configuring trace auto collectors for incoming HTTP requests. +/// +public static class HttpTracingExtensions +{ + /// + /// Adds trace auto collector for incoming HTTP requests. + /// + /// The to add the tracing auto collector. + /// The so that additional calls can be chained. + /// One of the arguments is . + public static TracerProviderBuilder AddHttpTracing(this TracerProviderBuilder builder) + { + _ = Throw.IfNull(builder); + + return builder + .ConfigureServices(services => services + .AddValidatedOptions()) + .AddHttpTracingInternal(); + } + + /// + /// Adds trace auto collector for incoming HTTP requests. + /// + /// The to add the tracing auto collector. + /// The configuration delegate. + /// The so that additional calls can be chained. + /// One of the arguments is . + public static TracerProviderBuilder AddHttpTracing(this TracerProviderBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + return builder + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(configure)) + .AddHttpTracingInternal(); + } + + /// + /// Adds trace auto collector for incoming HTTP requests. + /// + /// The to add the tracing auto collector. + /// Configuration section that contains . + /// The so that additional calls can be chained. + /// One of the arguments is . + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(HttpTracingOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed with [DynamicDependency]")] + public static TracerProviderBuilder AddHttpTracing(this TracerProviderBuilder builder, IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + + return builder + .ConfigureServices(services => services + .AddValidatedOptions() + .Bind(section)) + .AddHttpTracingInternal(); + } + + /// + /// Adds an enricher that enriches only incoming HTTP requests traces. + /// + /// Enricher object type. + /// The to add this enricher. + /// for chaining. + public static TracerProviderBuilder AddHttpTraceEnricher(this TracerProviderBuilder builder) + where T : class, IHttpTraceEnricher + { + _ = Throw.IfNull(builder); + + return builder.ConfigureServices(services => services + .AddHttpTraceEnricher()); + } + + /// + /// Adds an enricher that enriches only incoming HTTP requests traces. + /// + /// The to add this enricher. + /// Enricher to be added. + /// for chaining. + public static TracerProviderBuilder AddHttpTraceEnricher(this TracerProviderBuilder builder, IHttpTraceEnricher enricher) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(enricher); + + return builder.ConfigureServices(services => services + .AddHttpTraceEnricher(enricher)); + } + + /// + /// Adds an enricher that enriches only incoming HTTP requests traces. + /// + /// Enricher object type. + /// The to add this enricher. + /// for chaining. + /// The argument is . + [Experimental] + public static IServiceCollection AddHttpTraceEnricher(this IServiceCollection services) + where T : class, IHttpTraceEnricher + { + _ = Throw.IfNull(services); + + return services.AddSingleton(); + } + + /// + /// Adds an enricher that enriches only incoming HTTP requests traces. + /// + /// The to add this enricher. + /// Enricher to be added. + /// for chaining. + /// The argument or is . + [Experimental] + public static IServiceCollection AddHttpTraceEnricher(this IServiceCollection services, IHttpTraceEnricher enricher) + { + _ = Throw.IfNull(services); + + return services.AddSingleton(enricher); + } + + private static TracerProviderBuilder AddHttpTracingInternal(this TracerProviderBuilder builder) + { + return builder +#if NETCOREAPP3_1_OR_GREATER + .ConfigureServices(services => + { + _ = services.AddHttpRouteProcessor(); + _ = services.AddHttpRouteUtilities(); + _ = services.AddSingleton, ConfigureAspNetCoreInstrumentationOptions>(); + + services.TryAddActivatedSingleton(); + services.TryAddActivatedSingleton(); + }) + .AddAspNetCoreInstrumentation(); +#else + .ConfigureServices(services => + { + _ = services.AddHttpRouteProcessor(); + _ = services.AddHttpRouteUtilities(); + + services.TryAddActivatedSingleton(); + services.TryAddActivatedSingleton(); + }) + .AddAspNetCoreInstrumentation(options => options.Enrich + = (activity, eventName, rawObject) => + { + if (eventName.Equals(Constants.ActivityStartEvent, StringComparison.Ordinal)) + { + if (rawObject is HttpRequest request) + { + activity.SetCustomProperty(Constants.CustomPropertyHttpRequest, request); + } + } + }) + .AddProcessor(); +#endif + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpTracingOptions.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpTracingOptions.cs new file mode 100644 index 0000000000..8f2157b042 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpTracingOptions.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Http.Telemetry; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Options class for providing configuration parameters to configure incoming HTTP trace auto collection. +/// +public class HttpTracingOptions +{ + private const HttpRouteParameterRedactionMode DefaultPathParameterRedactionMode = HttpRouteParameterRedactionMode.Strict; + + /// + /// Gets or sets a map between HTTP request parameters and their data classification. + /// + /// + /// Default set to empty . + /// If a parameter in requestUrl is not found in this map, it will be redacted as if it was . + /// If the parameter will not contain sensitive information and shouldn't be redacted, mark it as . + /// + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Options pattern.")] + [Required] + public IDictionary RouteParameterDataClasses { get; set; } + = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets or sets a value indicating whether to include path with redacted parameters. + /// + /// + /// When false the exported traces will contain the route template. + /// When true, the request path will be recreated using the redacted parameter and included in the exported traces. + /// Default value is false. + /// + public bool IncludePath { get; set; } + + /// + /// Gets or sets a value indicating how HTTP path parameter should be redacted. + /// + /// + /// Default set to . + /// It is applicable when option is enabled. + /// + [Experimental] + public HttpRouteParameterRedactionMode RequestPathParameterRedactionMode { get; set; } = DefaultPathParameterRedactionMode; + + /// + /// Gets or sets a list of paths to exclude when auto collecting traces. + /// + /// + /// Traces for requests matching the exclusion list will not be exported. + /// + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Options pattern.")] + [Required] + public ISet ExcludePathStartsWith { get; set; } = new HashSet(); +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpUrlRedactionProcessor.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpUrlRedactionProcessor.cs new file mode 100644 index 0000000000..d9dcab779f --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/HttpUrlRedactionProcessor.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Collections.Frozen; +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Telemetry.Internal; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.AspNetCore.Telemetry; + +internal sealed class HttpUrlRedactionProcessor +{ + private readonly ILogger _logger; + private readonly HttpTracingOptions _options; + private readonly IHttpRouteFormatter _routeFormatter; + private readonly IHttpRouteParser _routeParser; + private readonly IIncomingHttpRouteUtility _routeUtility; + private readonly FrozenDictionary _defaultParamsToRedact; + private readonly string[] _excludePathStartsWith; + + public HttpUrlRedactionProcessor( + IOptions options, + IHttpRouteFormatter routeFormatter, + IHttpRouteParser routeParser, + IIncomingHttpRouteUtility routeUtility, + ILogger logger) + { + _options = Throw.IfMemberNull(options, options.Value); + _logger = logger; + + _routeFormatter = routeFormatter; + _routeParser = routeParser; + _routeUtility = routeUtility; + + _defaultParamsToRedact = _options.RouteParameterDataClasses.ToFrozenDictionary(StringComparer.Ordinal, optimizeForReading: true); + _excludePathStartsWith = _options.ExcludePathStartsWith.ToArray(); + + _logger.ConfiguredHttpTracingOptions(_options); + } + + public void Process(Activity activity, HttpRequest request) + { + // remove attributes that might contain sensitive information. + _ = activity.SetTag(Constants.AttributeUserAgent, null); + _ = activity.SetTag(Constants.AttributeHttpUrl, null); + _ = activity.SetTag(Constants.AttributeHttpPath, null); + _ = activity.SetTag(Constants.AttributeHttpTarget, null); + _ = activity.SetTag(Constants.AttributeNetHostName, null); + _ = activity.SetTag(Constants.AttributeNetHostPort, null); + _ = activity.SetTag(Constants.AttributeHttpScheme, null); + _ = activity.SetTag(Constants.AttributeHttpFlavor, null); + _ = activity.SetTag(Constants.OtelStatusCode, null); + + if (_options.RequestPathParameterRedactionMode == HttpRouteParameterRedactionMode.None) + { + _ = activity.DisplayName = request.Path.Value!; + _ = activity.AddTag(Constants.AttributeHttpPath, request.Path.Value); + return; + } + + var httpRoute = (string?)activity.GetTagItem(Constants.AttributeHttpRoute) ?? string.Empty; + if (string.IsNullOrEmpty(httpRoute)) + { + httpRoute = request.GetRoute(); + _ = activity.AddTag(Constants.AttributeHttpRoute, httpRoute); + } + + _ = activity.SetTag(Constants.AttributeHttpHost, request.Host.Host); + + if (string.IsNullOrEmpty(httpRoute)) + { + _logger.HttpRouteNotFound(activity.OperationName); + activity.DisplayName = Constants.RequestNameUnknown; + return; + } + + if (ShouldExcludePath(httpRoute)) + { + activity.ActivityTraceFlags = ~ActivityTraceFlags.Recorded; + return; + } + + activity.DisplayName = httpRoute; + + var routeSegments = _routeParser.ParseRoute(httpRoute); + var parametersToRedact = _routeUtility.GetSensitiveParameters(httpRoute, request, _defaultParamsToRedact); + + if (!_options.IncludePath) + { + var routeParameters = ArrayPool.Shared.Rent(routeSegments.ParameterCount); + try + { + if (_routeParser.TryExtractParameters(request.Path, routeSegments, _options.RequestPathParameterRedactionMode, parametersToRedact, ref routeParameters)) + { + for (int i = 0; i < routeSegments.ParameterCount; i++) + { + _ = activity.AddTag(routeParameters[i].Name, routeParameters[i].Value); + } + } + } + finally + { + ArrayPool.Shared.Return(routeParameters); + } + + _ = activity.SetTag(Constants.AttributeHttpPath, httpRoute); + } + else + { + var redactedPath = _routeFormatter.Format(routeSegments, request.Path, _options.RequestPathParameterRedactionMode, parametersToRedact); + _ = activity.SetTag(Constants.AttributeHttpPath, redactedPath); + } + } + + private bool ShouldExcludePath(string path) + { + foreach (var excludedPath in _excludePathStartsWith) + { + if (path.StartsWith(excludedPath, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/IHttpTraceEnricher.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/IHttpTraceEnricher.cs new file mode 100644 index 0000000000..ef2f23d463 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/IHttpTraceEnricher.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Telemetry; + +/// +/// Interface for implementing enricher for enriching only traces for incoming HTTP requests. +/// +public interface IHttpTraceEnricher +{ + /// + /// Enrich trace with desired tags. + /// + /// object to be used to add the required tags to enrich the traces. + /// object associated with the incoming request for the trace. + /// If your enricher fetches some information from request object to enrich HTTP traces, then make sure to check for . + void Enrich(Activity activity, HttpRequest request); +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/Internal/ConfigureAspNetCoreInstrumentationOptions.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/Internal/ConfigureAspNetCoreInstrumentationOptions.cs new file mode 100644 index 0000000000..ec4a15e265 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/Internal/ConfigureAspNetCoreInstrumentationOptions.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETCOREAPP3_1_OR_GREATER + +using System.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Telemetry; +using Microsoft.Extensions.Options; +using OpenTelemetry.Instrumentation.AspNetCore; + +namespace Microsoft.AspNetCore.Telemetry.Internal; + +internal sealed class ConfigureAspNetCoreInstrumentationOptions : IConfigureOptions +{ + private readonly HttpTraceEnrichmentProcessor _enrichmentProcessor; + private readonly HttpUrlRedactionProcessor _redactionProcessor; + + public ConfigureAspNetCoreInstrumentationOptions(HttpTraceEnrichmentProcessor enrichmentProcessor, HttpUrlRedactionProcessor redactionProcessor) + { + _enrichmentProcessor = enrichmentProcessor; + _redactionProcessor = redactionProcessor; + } + + public void Configure(AspNetCoreInstrumentationOptions options) + { + options.EnrichWithHttpRequest = (activity, request) + => activity.SetCustomProperty(Constants.CustomPropertyHttpRequest, request); + + options.EnrichWithHttpResponse = (activity, response) + => EnrichAndRedact(activity, response.HttpContext.Request); + + options.EnrichWithException = (activity, _) + => EnrichAndRedact(activity, (HttpRequest?)activity.GetCustomProperty(Constants.CustomPropertyHttpRequest)); + } + + private void EnrichAndRedact(Activity activity, HttpRequest? request) + { + if (request != null) + { + activity.SetCustomProperty(Constants.CustomPropertyHttpRequest, null); + + _enrichmentProcessor.Enrich(activity, request); + _redactionProcessor.Process(activity, request); + } + } +} + +#endif diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/Internal/HttpTracingOptionsValidator.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/Internal/HttpTracingOptionsValidator.cs new file mode 100644 index 0000000000..5a7146850a --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/Internal/HttpTracingOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.AspNetCore.Telemetry.Internal; + +[OptionsValidator] +internal sealed partial class HttpTracingOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/Internal/Log.cs b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/Internal/Log.cs new file mode 100644 index 0000000000..4edc2df12b --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Telemetry/Tracing/Internal/Log.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.AspNetCore.Telemetry.Internal; + +#pragma warning disable S109 // Magic numbers should not be used +internal static partial class Log +{ + /// + /// Logs `Http Route not found for Activity {activityName}.` at `Debug` level. + /// + [LogMethod(2, LogLevel.Debug, "Http Route not found for Activity {activityName}.")] + public static partial void HttpRouteNotFound(this ILogger logger, string activityName); + + /// + /// Logs `Configured HttpTracingOptions: {options}` at `Information` level. + /// + [LogMethod(3, LogLevel.Information, "Configured HttpTracingOptions: {options}")] + internal static partial void ConfiguredHttpTracingOptions(this ILogger logger, HttpTracingOptions options); +} +#pragma warning restore S109 // Magic numbers should not be used diff --git a/src/Libraries/Microsoft.AspNetCore.Testing/Internal/FakeCertificateHttpClientHandler.cs b/src/Libraries/Microsoft.AspNetCore.Testing/Internal/FakeCertificateHttpClientHandler.cs new file mode 100644 index 0000000000..27f198e822 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Testing/Internal/FakeCertificateHttpClientHandler.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace Microsoft.AspNetCore.Testing.Internal; + +internal sealed class FakeCertificateHttpClientHandler : HttpClientHandler +{ + public FakeCertificateHttpClientHandler(X509Certificate2 certificate) + { + ServerCertificateCustomValidationCallback = (_, serverCertificate, _, errors) => + { + if (serverCertificate is null || !serverCertificate.Equals(certificate)) + { + return errors == SslPolicyErrors.None; + } + + return true; + }; + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Testing/Internal/FakeCertificateOptions.cs b/src/Libraries/Microsoft.AspNetCore.Testing/Internal/FakeCertificateOptions.cs new file mode 100644 index 0000000000..6bd4b24e98 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Testing/Internal/FakeCertificateOptions.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Security.Cryptography.X509Certificates; + +namespace Microsoft.AspNetCore.Testing.Internal; + +internal sealed class FakeCertificateOptions +{ + public X509Certificate2? Certificate { get; set; } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Testing/Internal/FakeSslCertificateFactory.cs b/src/Libraries/Microsoft.AspNetCore.Testing/Internal/FakeSslCertificateFactory.cs new file mode 100644 index 0000000000..8c1859050d --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Testing/Internal/FakeSslCertificateFactory.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace Microsoft.AspNetCore.Testing.Internal; + +/// Copied from Microsoft.Extensions.Secrets.Test.TestCertificateFactory. +/// Could be exposed in a new package called Secrets.Fakes to facilitate reusability. +internal static class FakeSslCertificateFactory +{ + private static readonly RSA _rsa = GenerateRsa(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + + /// + /// Creates a self-signed instance for testing. + /// + /// An instance for testing. + [SuppressMessage("Reliability", "R9A022:Use System.TimeProvider when dealing with time in your code.", Justification = "declarations")] + public static X509Certificate2 CreateSslCertificate() + { + var request = new CertificateRequest( + new X500DistinguishedName("CN=r9-self-signed-unit-test-certificate"), + _rsa, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + var sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddDnsName("localhost"); + request.CertificateExtensions.Add(sanBuilder.Build()); + + request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension( + new OidCollection + { + new("1.3.6.1.5.5.7.3.1"), // serverAuth Object ID - indicates that the certificate is an SSL server certificate + new("1.3.6.1.5.5.7.3.2") // clientAuth Object ID - indicates that the certificate is an SSL client certificate + }, + false)); + + return request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(1)); + } + + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", + Justification = "Must use RSACryptoServiceProvider on windows, otherwise X509Certificate2.GetRSAPrivateKey().ExportPkcs8PrivateKey() will throw.")] + internal static RSA GenerateRsa(bool runsOnWindows) + { + // Stryker disable all + return runsOnWindows + ? new RSACryptoServiceProvider( + 2048, + new CspParameters(24, "Microsoft Enhanced RSA and AES Cryptographic Provider", Guid.NewGuid().ToString())) + : RSA.Create(); + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Testing/Internal/FakeStartup.cs b/src/Libraries/Microsoft.AspNetCore.Testing/Internal/FakeStartup.cs new file mode 100644 index 0000000000..48c0898081 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Testing/Internal/FakeStartup.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Testing.Internal; + +[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "convention")] +internal sealed class FakeStartup +{ + public void Configure(IApplicationBuilder _) + { + // intentionally empty + } + + public void ConfigureServices(IServiceCollection _) + { + // intentionally empty + } +} diff --git a/src/Libraries/Microsoft.AspNetCore.Testing/Microsoft.AspNetCore.Testing.csproj b/src/Libraries/Microsoft.AspNetCore.Testing/Microsoft.AspNetCore.Testing.csproj new file mode 100644 index 0000000000..6f2c723416 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Testing/Microsoft.AspNetCore.Testing.csproj @@ -0,0 +1,27 @@ + + + Microsoft.AspNetCore.Testing + Test fakes for integration testing + Fundamentals + $(PackageTags);Testing + + + + $(NetCoreTargetFrameworks) + true + + + + dev + 100 + 100 + + + + + + + + + + diff --git a/src/Libraries/Microsoft.AspNetCore.Testing/ServiceFakesExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Testing/ServiceFakesExtensions.cs new file mode 100644 index 0000000000..623b753b96 --- /dev/null +++ b/src/Libraries/Microsoft.AspNetCore.Testing/ServiceFakesExtensions.cs @@ -0,0 +1,131 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Testing.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.AspNetCore.Testing; + +/// +/// Extension methods supporting Kestrel server unit testing scenarios. +/// +public static class ServiceFakesExtensions +{ + private static readonly Func _defaultAddressFilter = static _ => true; + + /// + /// Adds an empty Startup class to satisfy ASP.NET check. + /// + /// An instance. + /// The same instance to allow method chaining. + public static IWebHostBuilder UseTestStartup(this IWebHostBuilder builder) + { + return builder.UseStartup(); + } + + /// + /// Adds Kestrel server instance listening on the given HTTP port. + /// + /// An instance. + /// The same instance to allow method chaining. + /// When a concrete port is set by caller, it's not further validated if the port is really free. + public static IWebHostBuilder ListenHttpOnAnyPort(this IWebHostBuilder builder) + { + _ = Throw.IfNull(builder); + return builder.UseKestrel(options => options.Listen(new IPEndPoint(IPAddress.Loopback, 0))); + } + + /// + /// Adds Kestrel server instance listening on a random HTTPS port. + /// + /// An instance. + /// An SSL certificate for the port. If null, a self-signed certificate is created and used. + /// The same instance to allow method chaining. + /// When a concrete port is set by caller, it's not further validated if the port is really free. + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "Dispose objects before losing scope")] + public static IWebHostBuilder ListenHttpsOnAnyPort(this IWebHostBuilder builder, X509Certificate2? sslCertificate = null) + { + sslCertificate ??= FakeSslCertificateFactory.CreateSslCertificate(); + + return builder + .UseKestrel(options => + { + options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions => + { + _ = listenOptions.UseHttps(sslCertificate); + }); + }) + .ConfigureServices(services => + services.Configure(options => + options.Certificate = sslCertificate)); + } + + /// + /// Creates an to call the hosted application. + /// + /// An instance. + /// The inner . + /// Selects what address should be used. If null, takes the first available address. + /// An configured to call the hosted application. + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "not applicable")] + [SuppressMessage("Reliability", "CA5399:HttpClient is created without enabling CheckCertificateRevocationList", Justification = "local calls")] + public static HttpClient CreateClient(this IHost host, HttpMessageHandler? handler = null, Func? addressFilter = null) + { + _ = Throw.IfNull(host); + addressFilter ??= _defaultAddressFilter; + + var uri = GetListenUris(host.Services.GetRequiredService()).FirstOrDefault(addressFilter) + ?? throw new InvalidOperationException("No suitable address found to call the server."); + + if (handler is null) + { + var certificate = host.Services.GetService>()?.Value.Certificate; + if (certificate is not null) + { + var httpHandler = new FakeCertificateHttpClientHandler(certificate); + return new HttpClient(httpHandler) { BaseAddress = uri }; + } + + return new HttpClient { BaseAddress = uri }; + } + + return new HttpClient(handler) { BaseAddress = uri }; + } + + /// + /// Gets the first available URI the server listens to that passes the filter. + /// + /// An instance. + /// A instance. + public static IEnumerable GetListenUris(this IHost host) + { + return GetListenUris(Throw.IfNull(host).Services.GetRequiredService()); + } + + private static IEnumerable GetListenUris(IServer server) + { + var feature = server.Features.Get(); + + // Stryker disable logical: we use the latter check to return static object instead of allocating a new one. + if (feature is null || feature.Addresses.Count == 0) + { + return ArraySegment.Empty; + } + + return feature.Addresses + .Select(x => new Uri(x.Replace("[::]", "localhost", StringComparison.OrdinalIgnoreCase))); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadata.cs b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadata.cs new file mode 100644 index 0000000000..b5795fb34e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadata.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Extensions.AmbientMetadata; + +/// +/// Application-level metadata model. +/// +public class ApplicationMetadata +{ + /// + /// Gets or sets a value that represents the deployment ring from where the application is running. + /// + public string? DeploymentRing { get; set; } + + /// + /// Gets or sets a value that represents the application's build version. + /// + public string? BuildVersion { get; set; } + + /// + /// Gets or sets a value that represents the application's name. + /// + [Required] + public string ApplicationName { get; set; } = string.Empty; + + /// + /// Gets or sets a value that represents the application's environment name, such as Development, Staging, Production. + /// + [Required] + public string EnvironmentName { get; set; } = string.Empty; +} diff --git a/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataExtensions.cs b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataExtensions.cs new file mode 100644 index 0000000000..0b580e8222 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataExtensions.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AmbientMetadata; + +/// +/// Extensions for application metadata. +/// +public static class ApplicationMetadataExtensions +{ + private const string DefaultSectionName = "ambientmetadata:application"; + + /// + /// Registers a configuration provider for application metadata and binds a model object onto the configuration. + /// + /// The host builder. + /// Section name to bind configuration from. Default set to "ambientmetadata:application". + /// The value of >. + /// If is . + /// If is either , empty or whitespace. + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(ApplicationMetadata))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed with [DynamicDependency]")] + public static IHostBuilder UseApplicationMetadata(this IHostBuilder builder, string sectionName = DefaultSectionName) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrWhitespace(sectionName); + + _ = builder + .ConfigureAppConfiguration((hostBuilderContext, configurationBuilder) => + configurationBuilder.AddApplicationMetadata(hostBuilderContext.HostingEnvironment, sectionName)) + .ConfigureServices((hostBuilderContext, serviceCollection) => + serviceCollection.AddApplicationMetadata(hostBuilderContext.Configuration.GetSection(sectionName))); + + return builder; + } + + /// + /// Registers a configuration provider for application metadata. + /// + /// The configuration builder. + /// An instance of . + /// Section name to save configuration into. Default set to "ambientmetadata:application". + /// The value of >. + /// If or is . + /// If is either , empty or whitespace. + public static IConfigurationBuilder AddApplicationMetadata(this IConfigurationBuilder builder, IHostEnvironment hostEnvironment, string sectionName = DefaultSectionName) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(hostEnvironment); + _ = Throw.IfNullOrWhitespace(sectionName); + + return builder.Add(new ApplicationMetadataSource(hostEnvironment, sectionName)); + } + + /// + /// Adds an instance of to a dependency injection container. + /// + /// The dependency injection container to add the instance to. + /// The configuration section to bind. + /// The value of >. + /// If or are . + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(ApplicationMetadata))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed with [DynamicDependency]")] + public static IServiceCollection AddApplicationMetadata(this IServiceCollection services, IConfigurationSection section) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(section); + + _ = services.AddValidatedOptions().Bind(section); + + return services; + } + + /// + /// Adds an instance of to a dependency injection container. + /// + /// The dependency injection container to add the instance to. + /// The delegate to configure with. + /// The value of >. + /// If or are . + public static IServiceCollection AddApplicationMetadata(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + _ = services.AddValidatedOptions().Configure(configure); + + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataSource.cs b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataSource.cs new file mode 100644 index 0000000000..ae6c35b133 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataSource.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Microsoft.Extensions.Hosting; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AmbientMetadata; + +/// +/// Provides virtual configuration source for service metadata information. +/// +internal sealed class ApplicationMetadataSource : IConfigurationSource +{ + private readonly IHostEnvironment _hostEnvironment; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// Section name to be used in configuration. + /// If is . + /// If is either , empty or whitespace. + public ApplicationMetadataSource(IHostEnvironment hostEnvironment, string sectionName) + { + _hostEnvironment = Throw.IfNull(hostEnvironment); + SectionName = Throw.IfNullOrWhitespace(sectionName); + } + + /// + /// Gets configuration section name. + /// + public string SectionName { get; } + + /// + /// Builds an for the source. + /// + /// The to add to. + /// The configuration provider. + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + var provider = new MemoryConfigurationProvider(new()) + { + { $"{SectionName}:{nameof(ApplicationMetadata.EnvironmentName)}", _hostEnvironment.EnvironmentName }, + { $"{SectionName}:{nameof(ApplicationMetadata.ApplicationName)}", _hostEnvironment.ApplicationName }, + }; + + return provider; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataValidator.cs b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataValidator.cs new file mode 100644 index 0000000000..cf8e27ded8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/ApplicationMetadataValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.AmbientMetadata; + +[OptionsValidator] +internal sealed partial class ApplicationMetadataValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/Microsoft.Extensions.AmbientMetadata.Application.csproj b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/Microsoft.Extensions.AmbientMetadata.Application.csproj new file mode 100644 index 0000000000..0aaa9620fb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AmbientMetadata.Application/Microsoft.Extensions.AmbientMetadata.Application.csproj @@ -0,0 +1,32 @@ + + + Microsoft.Extensions.AmbientMetadata + Runtime information provider for application-level ambient metadata. + Fundamentals + + + + true + + + + normal + 100 + 100 + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/AsyncContext.cs b/src/Libraries/Microsoft.Extensions.AsyncState/AsyncContext.cs new file mode 100644 index 0000000000..09783d52e9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AsyncState/AsyncContext.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.AsyncState; + +/// +/// Represents an implementation of the interface. +/// +internal sealed class AsyncContext : IAsyncLocalContext + where T : class +{ + private readonly AsyncStateToken _token; + private readonly IAsyncState _state; + + public AsyncContext(IAsyncState state) + { + _state = state; + _token = state.RegisterAsyncContext(); + } + + public T? Get() => (T?)_state.Get(_token); + public void Set(T? context) => _state.Set(_token, context); + + public bool TryGet(out T? context) + { + var result = _state.TryGet(_token, out object? value); + context = (T?)value; + + return result; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/AsyncState.cs b/src/Libraries/Microsoft.Extensions.AsyncState/AsyncState.cs new file mode 100644 index 0000000000..3c515907b4 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AsyncState/AsyncState.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.AsyncState; + +internal sealed class AsyncState : IAsyncState +{ + private static readonly AsyncLocal _asyncContextCurrent = new(); + private static readonly ObjectPool> _featuresPool = PoolFactory.CreatePool(new FeaturesPooledPolicy()); + private int _contextCount; + + public void Initialize() + { + Reset(); + + // Use an object indirection to hold the AsyncContext in the AsyncLocal, + // so it can be cleared in all ExecutionContexts when its cleared. + var features = new AsyncStateHolder + { + Features = _featuresPool.Get() + }; + + _asyncContextCurrent.Value = features; + } + + public void Reset() + { + var holder = _asyncContextCurrent.Value; + if (holder != null) + { + // Clear current AsyncContext trapped in the AsyncLocals, as its done. + if (holder.Features != null) + { + _featuresPool.Return(holder.Features); + holder.Features = null; + } + } + } + + public AsyncStateToken RegisterAsyncContext() + { + return new AsyncStateToken(Interlocked.Increment(ref _contextCount) - 1); + } + + public bool TryGet(AsyncStateToken token, out object? value) + { + // Context is not initialized + if (_asyncContextCurrent.Value?.Features == null) + { + value = null; + return false; + } + + EnsureCount(_asyncContextCurrent.Value.Features, token.Index + 1); + + value = _asyncContextCurrent.Value.Features[token.Index]; + return true; + } + + public object? Get(AsyncStateToken token) + { + if (TryGet(token, out object? value)) + { + return value; + } + + throw new InvalidOperationException("Context is not initialized"); + } + + public void Set(AsyncStateToken token, object? value) + { + // Context is not initialized + if (_asyncContextCurrent.Value?.Features == null) + { + Throw.InvalidOperationException("Context is not initialized"); + } + + EnsureCount(_asyncContextCurrent.Value.Features, token.Index + 1); + + _asyncContextCurrent.Value.Features[token.Index] = value; + } + + internal static void EnsureCount(List features, int count) + { +#if NET6_0_OR_GREATER + features.EnsureCapacity(count); +#endif + var difference = count - features.Count; + + for (int i = 0; i < difference; i++) + { + features.Add(null); + } + } + + internal int ContextCount => Volatile.Read(ref _contextCount); + + private sealed class AsyncStateHolder + { + public List? Features { get; set; } + } +} diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/AsyncStateExtensions.cs b/src/Libraries/Microsoft.Extensions.AsyncState/AsyncStateExtensions.cs new file mode 100644 index 0000000000..0780ce1d51 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AsyncState/AsyncStateExtensions.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AsyncState; + +/// +/// Extension methods to manipulate async state. +/// +public static class AsyncStateExtensions +{ + /// + /// Adds default implementations for , , and services. + /// + /// The dependency injection container to add the implementations to. + /// The value of . + /// is . + public static IServiceCollection AddAsyncStateCore(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + services.TryAddSingleton(typeof(IAsyncContext<>), typeof(AsyncContext<>)); + services.TryAddActivatedSingleton(); + services.TryAddSingleton(typeof(IAsyncLocalContext<>), typeof(AsyncContext<>)); + + return services; + } + + /// + /// Tries to remove the default implementation for , , and services. + /// + /// The dependency injection container to remove the implementations from. + /// The value of . + /// If is . + [EditorBrowsable(EditorBrowsableState.Never)] + public static IServiceCollection TryRemoveAsyncStateCore(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + services.TryRemoveSingleton(typeof(IAsyncContext<>), typeof(AsyncContext<>)); + + return services; + } + + internal static void TryRemoveSingleton( + this IServiceCollection services, + Type serviceType, + Type implementationType) + { + var descriptor = services.FirstOrDefault( + x => (x.ServiceType == serviceType) && (x.ImplementationType == implementationType)); + + if (descriptor != null) + { + _ = services.Remove(descriptor); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/AsyncStateToken.cs b/src/Libraries/Microsoft.Extensions.AsyncState/AsyncStateToken.cs new file mode 100644 index 0000000000..526bb70321 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AsyncState/AsyncStateToken.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.AsyncState; + +/// +/// Async state token representing a registered context wihtin the asynchornous state. +/// +public readonly struct AsyncStateToken : IEquatable +{ + internal AsyncStateToken(int index) + { + Index = index; + } + + internal readonly int Index { get; } + + /// + /// Determines whether the specified object is equal to the current async state token. + /// + /// The object to compare. + /// If the specified object is identical to the current async state token; otherwise, . + public override bool Equals(object? obj) + { + return obj is AsyncStateToken token && Equals(token); + } + + /// + /// Determines whether this async state token and a specified async state token are identical. + /// + /// The other async state token. + /// If the two async state tokens are identical; otherwise, . + public bool Equals(AsyncStateToken other) + { + return Index == other.Index; + } + + /// + /// Returns the hash code for this instance. + /// + /// A 32-bit signed integer hash code. + public override int GetHashCode() + { + return Index.GetHashCode(); + } + + /// + /// Compares two instances. + /// + /// Left argument of the comparison. + /// Right argument of the comparison. + /// when equal, otherwise. + public static bool operator ==(AsyncStateToken left, AsyncStateToken right) + { + return left.Equals(right); + } + + /// + /// Compares two instances. + /// + /// Left argument of the comparison. + /// Right argument of the comparison. + /// when not equal, otherwise. + public static bool operator !=(AsyncStateToken left, AsyncStateToken right) + { + return !(left == right); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/FeaturesPooledPolicy.cs b/src/Libraries/Microsoft.Extensions.AsyncState/FeaturesPooledPolicy.cs new file mode 100644 index 0000000000..c83245405e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AsyncState/FeaturesPooledPolicy.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.Extensions.AsyncState; + +internal sealed class FeaturesPooledPolicy : IPooledObjectPolicy> +{ + /// + public List Create() + { + return new List(); + } + + /// + public bool Return(List obj) + { + for (int i = 0; i < obj.Count; i++) + { + obj[i] = null; + } + + return true; + } +} diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncContext.cs b/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncContext.cs new file mode 100644 index 0000000000..f1c07ee94f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncContext.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.AsyncState; + +/// +/// Provides access to the current async context. +/// +/// The type of the asynchronous state. +[SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Getter and setter throw exceptions.")] +public interface IAsyncContext + where T : notnull +{ + /// + /// Gets current async context. + /// + /// Current async context. + /// Context is not initialized. + /// + /// If you are getting an exception that context is not initialized, make sure that you initialized it before usage in your framework. + /// Also check if you are accessing the context from the current asynchronous flow, starting with context initialization. + /// + T? Get(); + + /// + /// Sets async context. + /// + /// Context to be set. + /// Context is not initialized. + /// + /// If you are getting an exception that context is not initialized, make sure that you initialized it before usage in your framework. + /// Also check if you are accessing the context from the current asynchronous flow, starting with context initialization. + /// + void Set(T? context); + + /// + /// Tries to get the current async context. + /// + /// Receives the context. + /// if the context is initialized; otherwise, . + bool TryGet([MaybeNullWhen(false)] out T? context); +} + diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncLocalContext.cs b/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncLocalContext.cs new file mode 100644 index 0000000000..a180eb275f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncLocalContext.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.AsyncState; + +/// +/// Provides access to the current async context stored outside of the HTTP pipeline. +/// +/// The type of the asynchronous state. +/// This type is intended for internal use. Use instead. +[Experimental] +[EditorBrowsable(EditorBrowsableState.Never)] +#pragma warning disable S4023 // Interfaces should not be empty +public interface IAsyncLocalContext : IAsyncContext +#pragma warning restore S4023 // Interfaces should not be empty + where T : class +{ +} diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncState.cs b/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncState.cs new file mode 100644 index 0000000000..228320582a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AsyncState/IAsyncState.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; + +namespace Microsoft.Extensions.AsyncState; + +/// +/// Encapsulates all information within the asynchronous flow in an variable. +/// +[SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Getter and setter throw exceptions.")] +public interface IAsyncState +{ + /// + /// Initializes async state in current asynchronous flow. + /// + void Initialize(); + + /// + /// Resets async state after usage. + /// + void Reset(); + + /// + /// Tries to get the stored async context from the state. + /// + /// The token representing the state to extract. + /// + /// Receives the value associated with the specified token, if the context is initialized; + /// otherwise, the default value for the type of the parameter. + /// + /// if the context is initialized; otherwise, . + bool TryGet(AsyncStateToken token, [MaybeNullWhen(false)] out object? value); + + /// + /// Gets the stored async context from the state. + /// + /// The token representing the state to extract. + /// If the context is not initialized. + /// The asynchronous state corresponding to the token. + object? Get(AsyncStateToken token); + + /// + /// Stores async context. + /// + /// The token representing the state to store. + /// New state value. + /// Context is not initialized. + void Set(AsyncStateToken token, object? value); + + /// + /// Registers new async context with the state. + /// + /// Token that gives access to the reserved context. + public AsyncStateToken RegisterAsyncContext(); +} diff --git a/src/Libraries/Microsoft.Extensions.AsyncState/Microsoft.Extensions.AsyncState.csproj b/src/Libraries/Microsoft.Extensions.AsyncState/Microsoft.Extensions.AsyncState.csproj new file mode 100644 index 0000000000..95433aa420 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AsyncState/Microsoft.Extensions.AsyncState.csproj @@ -0,0 +1,30 @@ + + + Microsoft.Extensions.AsyncState + Asynchronous feature store. + Fundamentals + + + + true + true + + + + normal + 100 + 100 + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Classification/DataClassification.cs b/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Classification/DataClassification.cs new file mode 100644 index 0000000000..a484bcd157 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Classification/DataClassification.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Compliance.Classification; + +/// +/// Represents a set of data classes as a part of a data taxonomy. +/// +public readonly struct DataClassification : IEquatable +{ + /// + /// Represents unclassified data. + /// + public const ulong NoneTaxonomyValue = 0UL; + + /// + /// Represents the unknown classification. + /// + public const ulong UnknownTaxonomyValue = 1UL << 63; + + /// + /// Gets the value to represent data with no defined classification. + /// + public static DataClassification None => new(NoneTaxonomyValue); + + /// + /// Gets the value to represent data with an unknown classification. + /// + public static DataClassification Unknown => new(UnknownTaxonomyValue); + + /// + /// Gets the name of the taxonomy that recognizes this classification. + /// + public string TaxonomyName { get; } + + /// + /// Gets the bit mask representing the data classes. + /// + public ulong Value { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// Name of the taxonomy this classification belongs to. + /// The taxonomy-specific bit vector representing the data classes. + /// If bit 63, corresponding to is set in the value. + public DataClassification(string taxonomyName, ulong value) + { + TaxonomyName = Throw.IfNullOrEmpty(taxonomyName); + Value = value; + + if (((value & UnknownTaxonomyValue) != 0) || (value == NoneTaxonomyValue)) + { + Throw.ArgumentException(nameof(value), $"Cannot create a classification with a value of 0x{value:x}."); + } + } + + private DataClassification(ulong taxonomyValue) + { + TaxonomyName = string.Empty; + Value = taxonomyValue; + } + + /// + /// Checks if object is equal to this instance of . + /// + /// Object to check for equality. + /// if object instances are equal otherwise. + public override bool Equals(object? obj) => (obj is DataClassification dc) && Equals(dc); + + /// + /// Checks if object is equal to this instance of . + /// + /// Instance of to check for equality. + /// if object instances are equal otherwise. + public bool Equals(DataClassification other) => other.TaxonomyName == TaxonomyName && other.Value == Value; + + /// + /// Get the hash code the current instance. + /// + /// Hash code. + public override int GetHashCode() + { + return HashCode.Combine(TaxonomyName, Value); + } + + /// + /// Check if two instances are equal. + /// + /// Left argument of the comparison. + /// Right argument of the comparison. + /// if object instances are equal, or otherwise. + public static bool operator ==(DataClassification left, DataClassification right) + { + return left.Equals(right); + } + + /// + /// Check if two instances are not equal. + /// + /// Left argument of the comparison. + /// Right argument of the comparison. + /// if object instances are equal, or otherwise. + public static bool operator !=(DataClassification left, DataClassification right) + { + return !left.Equals(right); + } + + /// + /// Combines together two data classifications. + /// + /// The first classification to combine. + /// The second classification to combine. + /// A new classification object representing the combination of the two input classifications. + /// if the two classifications aren't part of the same taxonomy. + public static DataClassification Combine(DataClassification left, DataClassification right) + { + if (string.IsNullOrEmpty(left.TaxonomyName)) + { + return (left.Value == NoneTaxonomyValue) ? right : Unknown; + } + else if (string.IsNullOrEmpty(right.TaxonomyName)) + { + return (right.Value == NoneTaxonomyValue) ? left : Unknown; + } + + if (left.TaxonomyName != right.TaxonomyName) + { + Throw.ArgumentException(nameof(right), $"Mismatched data taxonomies: {left.TaxonomyName} and {right.TaxonomyName} cannot be combined"); + } + + return new(left.TaxonomyName, left.Value | right.Value); + } + + /// + /// Combines together two data classifications. + /// + /// The first classification to combine. + /// The second classification to combine. + /// A new classification object representing the combination of the two input classifications. + /// if the two classifications aren't part of the same taxonomy. + [SuppressMessage("Usage", "CA2225:Operator overloads have named alternates", Justification = "It's called Combine")] + public static DataClassification operator |(DataClassification left, DataClassification right) + { + return Combine(left, right); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Classification/DataClassificationAttribute.cs b/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Classification/DataClassificationAttribute.cs new file mode 100644 index 0000000000..117b793e18 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Classification/DataClassificationAttribute.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Compliance.Classification; + +/// +/// Base attribute for data classification. +/// +[AttributeUsage( + AttributeTargets.Field + | AttributeTargets.Property + | AttributeTargets.Parameter + | AttributeTargets.Class + | AttributeTargets.Struct + | AttributeTargets.Interface + | AttributeTargets.ReturnValue + | AttributeTargets.GenericParameter, + AllowMultiple = true)] +#pragma warning disable CA1813 // Avoid unsealed attributes +public class DataClassificationAttribute : Attribute +#pragma warning restore CA1813 // Avoid unsealed attributes +{ + /// + /// Gets or sets the notes. + /// + /// Optional free-form text to provide context during a privacy audit. + public string Notes { get; set; } = string.Empty; + + /// + /// Gets the data class represented by this attribute. + /// + public DataClassification Classification { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The data classification to apply. + protected DataClassificationAttribute(DataClassification classification) + { + Classification = classification; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Classification/NoDataClassificationAttribute.cs b/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Classification/NoDataClassificationAttribute.cs new file mode 100644 index 0000000000..727cf34394 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Classification/NoDataClassificationAttribute.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Compliance.Classification; + +/// +/// Indicates data which is specifically not classified. +/// +public sealed class NoDataClassificationAttribute : DataClassificationAttribute +{ + /// + /// Initializes a new instance of the class. + /// + public NoDataClassificationAttribute() + : base(DataClassification.None) + { + } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Classification/UnknownDataClassificationAttribute.cs b/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Classification/UnknownDataClassificationAttribute.cs new file mode 100644 index 0000000000..70bf0d72ac --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Classification/UnknownDataClassificationAttribute.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Compliance.Classification; + +/// +/// Indicates data whose classification is unknown. +/// +public sealed class UnknownDataClassificationAttribute : DataClassificationAttribute +{ + /// + /// Initializes a new instance of the class. + /// + public UnknownDataClassificationAttribute() + : base(DataClassification.Unknown) + { + } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Microsoft.Extensions.Compliance.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Microsoft.Extensions.Compliance.Abstractions.csproj new file mode 100644 index 0000000000..954caee2b3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Microsoft.Extensions.Compliance.Abstractions.csproj @@ -0,0 +1,31 @@ + + + Microsoft.Extensions.Compliance + Abstractions to help ensure compliant data management. + Fundamentals + + + + true + true + true + true + + + + normal + 100 + 22 + 100 + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Redaction/IRedactionBuilder.cs b/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Redaction/IRedactionBuilder.cs new file mode 100644 index 0000000000..59cce31c23 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Redaction/IRedactionBuilder.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Compliance.Redaction; + +/// +/// Adds redactors to the application. +/// +public interface IRedactionBuilder +{ + /// + /// Gets the service collection into which the redactor instances are registered. + /// + IServiceCollection Services { get; } + + /// + /// Sets the redactor to use for a set of data classes. + /// + /// Redactor type. + /// The data classes for which the redactor type should be used. + /// The value of this instance. + /// If is . + IRedactionBuilder SetRedactor(params DataClassification[] classifications) + where T : Redactor; + + /// + /// Sets the redactor to use when processing classified data for which no specific redactor has been registered. + /// + /// Redactor type. + /// The value of this instance. + IRedactionBuilder SetFallbackRedactor() + where T : Redactor; +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Redaction/IRedactorProvider.cs b/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Redaction/IRedactorProvider.cs new file mode 100644 index 0000000000..fc6e6f14d5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Redaction/IRedactorProvider.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Compliance.Classification; + +namespace Microsoft.Extensions.Compliance.Redaction; + +/// +/// Provides redactors for different data classes. +/// +public interface IRedactorProvider +{ + /// + /// Gets the redactor configured to handle the specified data class. + /// + /// Data classification of the data to redact. + /// A redactor suitable to redact data of the given class. + Redactor GetRedactor(DataClassification classification); +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Redaction/RedactionAbstractionsExtensions.cs b/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Redaction/RedactionAbstractionsExtensions.cs new file mode 100644 index 0000000000..f175b26fc1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Redaction/RedactionAbstractionsExtensions.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using System.Text; + +#if NETCOREAPP3_1_OR_GREATER +using Microsoft.Shared.Pools; +#endif + +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Compliance.Redaction; + +/// +/// Redaction utility methods. +/// +public static class RedactionAbstractionsExtensions +{ + /// + /// Redacts potentially sensitive data and appends it to a instance. + /// + /// Instance of to append the redacted value. + /// The redactor that will redact the input value. + /// Value to redact. + /// Returns the value of . + /// + /// When the is nothing will be appended to the string builder. + /// + /// When is . + /// When is . + /// When is . + public static StringBuilder AppendRedacted(this StringBuilder stringBuilder, Redactor redactor, string? value) + => AppendRedacted(stringBuilder, redactor, value.AsSpan()); + + /// + /// Redacts potentially sensitive data and appends it to a instance. + /// + /// Instance of to append the redacted value. + /// The redactor that will redact the input value. + /// Value to redact. + /// Returns the value of . + /// When is . + /// When is . + [SkipLocalsInit] + public static StringBuilder AppendRedacted(this StringBuilder stringBuilder, Redactor redactor, ReadOnlySpan value) + { + _ = Throw.IfNull(stringBuilder); + _ = Throw.IfNull(redactor); + + if (value.IsEmpty) + { + return stringBuilder; + } + +#if NETCOREAPP3_1_OR_GREATER + var length = redactor.GetRedactedLength(value); + using var rental = new RentedSpan(length); + var destination = rental.Rented ? rental.Span : stackalloc char[length]; + + var written = redactor.Redact(value, destination); + return stringBuilder.Append(destination.Slice(0, written)); +#else + return stringBuilder.Append(redactor.Redact(value)); +#endif + } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Redaction/Redactor.cs b/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Redaction/Redactor.cs new file mode 100644 index 0000000000..d02a8538f1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Abstractions/Redaction/Redactor.cs @@ -0,0 +1,201 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +#if !NETCOREAPP3_1_OR_GREATER +using System.Buffers; +#endif + +namespace Microsoft.Extensions.Compliance.Redaction; + +/// +/// Enables the redaction of potentially sensitive data. +/// +public abstract class Redactor +{ +#if NET6_0_OR_GREATER + private const int MaximumStackAllocation = 256; +#endif + + /// + /// Redacts potentially sensitive data. + /// + /// Value to redact. + /// Redacted value. + public string Redact(ReadOnlySpan source) + { + if (source.IsEmpty) + { + return string.Empty; + } + + var length = GetRedactedLength(source); + +#if NETCOREAPP3_1_OR_GREATER + unsafe + { +#pragma warning disable 8500 + return string.Create( + length, + (this, (IntPtr)(&source)), + (destination, state) => state.Item1.Redact(*(ReadOnlySpan*)state.Item2, destination)); +#pragma warning restore 8500 + } +#else + var buffer = ArrayPool.Shared.Rent(length); + + try + { + var charsWritten = Redact(source, buffer); + var redactedString = new string(buffer, 0, charsWritten); + + return redactedString; + } + finally + { + ArrayPool.Shared.Return(buffer); + } +#endif + } + + /// + /// Redacts potentially sensitive data. + /// + /// Value to redact. + /// Buffer to store redacted value. + /// Number of characters produced when redacting the given source input. + /// When is too small. + public abstract int Redact(ReadOnlySpan source, Span destination); + + /// + /// Redacts potentially sensitive data. + /// + /// Value to redact. + /// Buffer to redact into. + /// + /// Returns 0 when is . + /// + /// Number of characters written to the buffer. + /// When is too small. + public int Redact(string? source, Span destination) => Redact(source.AsSpan(), destination); + + /// + /// Redacts potentially sensitive data. + /// + /// Value to redact. + /// Redacted value. + /// + /// Returns an empty string when is . + /// + /// When is . + public virtual string Redact(string? source) => Redact(source.AsSpan()); + + /// + /// Redacts potentially sensitive data. + /// + /// Type of value to redact. + /// Value to redact. + /// + /// The optional format that selects the specific formatting operation performed. Refer to the + /// documentation of the type being formatted to understand the values you can supply here. + /// + /// Format provider to retrieve format for span formattable. + /// Redacted value. + /// When is . + [SkipLocalsInit] + [SuppressMessage("Minor Code Smell", "S3247:Duplicate casts should not be made", Justification = "Avoid pattern matching to improve jitted code")] + public string Redact(T value, string? format = null, IFormatProvider? provider = null) + { +#if NET6_0_OR_GREATER + if (value is ISpanFormattable) + { + Span buffer = stackalloc char[MaximumStackAllocation]; + + // Stryker disable all : Cannot kill the mutant because the only difference is allocating buffer on stack or renting it. + // Null forgiving operator: The null case is checked with default equality comparer, but compiler doesn't understand it. + if (((ISpanFormattable)value).TryFormat(buffer, out var written, format.AsSpan(), provider)) + { + // Stryker enable all : Cannot kill the mutant because the only difference is allocating buffer on stack or renting it. + + var formatted = buffer.Slice(0, written); + var length = GetRedactedLength(formatted); + + unsafe + { +#pragma warning disable 8500 + return string.Create( + length, + (this, (IntPtr)(&formatted)), + (destination, state) => state.Item1.Redact(*(ReadOnlySpan*)state.Item2, destination)); +#pragma warning restore 8500 + } + } + } +#endif + + if (value is IFormattable) + { + return Redact(((IFormattable)value).ToString(format, provider)); + } + + return Redact(value?.ToString()); + } + + /// + /// Redacts potentially sensitive data. + /// + /// Type of value to redact. + /// Value to redact. + /// Buffer to redact into. + /// + /// The optional format string that selects the specific formatting operation performed. Refer to the + /// documentation of the type being formatted to understand the values you can supply here. + /// + /// Format provider to retrieve format for span formattable. + /// Number of characters written to the buffer. + /// When is . + [SkipLocalsInit] + [SuppressMessage("Minor Code Smell", "S3247:Duplicate casts should not be made", Justification = "Avoid pattern matching to improve jitted code")] + public int Redact(T value, Span destination, string? format = null, IFormatProvider? provider = null) + { +#if NET6_0_OR_GREATER + if (value is ISpanFormattable) + { + Span buffer = stackalloc char[MaximumStackAllocation]; + + // Stryker disable all : Cannot kill the mutant because the only difference is allocating buffer on stack or renting it. + if (((ISpanFormattable)value).TryFormat(buffer, out var written, format.AsSpan(), provider)) + { + // Stryker enable all : Cannot kill the mutant because the only difference is allocating buffer on stack or renting it. + var formatted = buffer.Slice(0, written); + + return Redact(formatted, destination); + } + } +#endif + + if (value is IFormattable) + { + return Redact(((IFormattable)value).ToString(format, provider), destination); + } + + return Redact(value?.ToString(), destination); + } + + /// + /// Gets the number of characters produced by redacting the input. + /// + /// Value to be redacted. + /// Minimum buffer size. + public abstract int GetRedactedLength(ReadOnlySpan input); + + /// + /// Gets the number of characters produced by redacting the input. + /// + /// Value to be redacted. + /// Minimum buffer size. + public int GetRedactedLength(string? input) => GetRedactedLength(input.AsSpan()); +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Redaction/ErasingRedactor.cs b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/ErasingRedactor.cs new file mode 100644 index 0000000000..92561a1d6b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/ErasingRedactor.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Compliance.Redaction; + +/// +/// Redactor that replaces anything with an empty string. +/// +public sealed class ErasingRedactor : Redactor +{ + /// + /// Gets the singleton instance of . + /// + public static ErasingRedactor Instance { get; } = new(); + + /// + public override int Redact(ReadOnlySpan source, Span destination) => 0; + + /// + public override int GetRedactedLength(ReadOnlySpan input) => 0; +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Redaction/Microsoft.Extensions.Compliance.Redaction.csproj b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/Microsoft.Extensions.Compliance.Redaction.csproj new file mode 100644 index 0000000000..9cea2e0577 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/Microsoft.Extensions.Compliance.Redaction.csproj @@ -0,0 +1,34 @@ + + + Microsoft.Extensions.Compliance.Redaction + Redaction engine and canonical redactors. + Fundamentals + + + + true + true + + + + normal + 100 + 100 + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Redaction/NullRedactor.cs b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/NullRedactor.cs new file mode 100644 index 0000000000..53a4a820be --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/NullRedactor.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Compliance.Redaction; + +/// +/// Redactor that does nothing to its input and returns it as-is. +/// +public sealed class NullRedactor : Redactor +{ + /// + /// Gets the singleton instance of this class. + /// + public static NullRedactor Instance { get; } = new(); + + /// + public override int GetRedactedLength(ReadOnlySpan input) => input.Length; + + /// + public override int Redact(ReadOnlySpan source, Span destination) + { + if (!source.TryCopyTo(destination)) + { + // will throw unconditionally, with a nice error message + Throw.IfBufferTooSmall(destination.Length, source.Length, nameof(destination)); + } + + return source.Length; + } + + /// + public override string Redact(string? source) => source ?? string.Empty; +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Redaction/NullRedactorProvider.cs b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/NullRedactorProvider.cs new file mode 100644 index 0000000000..e433b95c2d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/NullRedactorProvider.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Compliance.Classification; + +namespace Microsoft.Extensions.Compliance.Redaction; + +/// +/// A provider that only returns the null redactor implementation used for situations that don't require redaction. +/// +public sealed class NullRedactorProvider : IRedactorProvider +{ + /// + /// Gets the singleton instance of this class. + /// + public static NullRedactorProvider Instance { get; } = new(); + + /// + public Redactor GetRedactor(DataClassification classification) => NullRedactor.Instance; +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactionBuilder.cs b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactionBuilder.cs new file mode 100644 index 0000000000..46a88fd6c5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactionBuilder.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Compliance.Redaction; + +/// +/// Configures redaction library. +/// +internal sealed class RedactionBuilder : IRedactionBuilder +{ + /// + public IServiceCollection Services { get; } + + /// + /// Initializes a new instance of the class. + /// + /// Host services that will be used to register redactors. + public RedactionBuilder(IServiceCollection services) + { + Services = Throw.IfNull(services); + + Services.TryAddEnumerable(ServiceDescriptor.Singleton(ErasingRedactor.Instance)); + Services.TryAddEnumerable(ServiceDescriptor.Singleton(NullRedactor.Instance)); + } + + public IRedactionBuilder SetRedactor(params DataClassification[] classifications) + where T : Redactor + { + _ = Throw.IfNull(classifications); + + foreach (var c in classifications) + { + var redactorType = typeof(T); + Services.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(Redactor), redactorType)); + _ = Services.Configure(options => options.Redactors[c] = redactorType); + } + + return this; + } + + public IRedactionBuilder SetFallbackRedactor() + where T : Redactor + { + Services.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(Redactor), typeof(T))); + _ = Services.Configure(options => options.FallbackRedactor = typeof(T)); + return this; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactionExtensions.cs b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactionExtensions.cs new file mode 100644 index 0000000000..9d4cd97bd4 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactionExtensions.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Compliance.Redaction; + +/// +/// Add redaction to the application. +/// +public static partial class RedactionExtensions +{ + /// + /// Registers redaction in the application. + /// + /// instance. + /// The value of . + /// When is . + public static IHostBuilder ConfigureRedaction(this IHostBuilder builder) + { + _ = Throw.IfNull(builder); + + return builder.ConfigureServices((_, services) => services.AddRedaction()); + } + + /// + /// Registers redaction in the application. + /// + /// instance. + /// Configuration for . + /// The value of . + /// When is . + /// When is . + public static IHostBuilder ConfigureRedaction( + this IHostBuilder builder, + Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + return builder.ConfigureServices((context, services) => services.AddRedaction(builder => configure(context, builder))); + } + + /// + /// Registers redaction in the application. + /// + /// instance. + /// Configuration for . + /// The value of . + /// When is . + /// When is . + public static IHostBuilder ConfigureRedaction(this IHostBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + return builder.ConfigureServices((_, services) => services.AddRedaction(builder => configure(builder))); + } + + /// + /// Registers an implementation of in the . + /// + /// Instance of used to configure redaction. + /// The value of . + /// When is . + public static IServiceCollection AddRedaction(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + return services.AddRedaction(_ => { }); + } + + /// + /// Registers an implementation of in the and configures available redactors. + /// + /// Instance of used to configure redaction. + /// Configuration function for . + /// The value of . + /// When is . + /// When is . + public static IServiceCollection AddRedaction(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + services + .AddOptions() + .Services + .TryAddSingleton(); + + configure(new RedactionBuilder(services)); + + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactionExtensions.xxHash.cs b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactionExtensions.xxHash.cs new file mode 100644 index 0000000000..7a90578b4f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactionExtensions.xxHash.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Compliance.Redaction; + +public static partial class RedactionExtensions +{ + /// + /// Sets the xxHash3 redactor to use for a set of data classes. + /// + /// The builder to attach the redactor to. + /// Configuration function. + /// The data classes for which the redactor type should be used. + /// The value of . + /// If , or are . + public static IRedactionBuilder SetXXHash3Redactor(this IRedactionBuilder builder, Action configure, params DataClassification[] classifications) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + _ = Throw.IfNull(classifications); + + _ = builder + .Services + .AddOptions() + .Configure(configure); + + return builder.SetRedactor(classifications); + } + + /// + /// Sets the xxHash3 redactor to use for a set of data classes. + /// + /// The builder to attach the redactor to. + /// Configuration section. + /// The data classes for which the redactor type should be used. + /// The value of . + /// If , or are . + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(XXHash3RedactorOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed with [DynamicDependency]")] + public static IRedactionBuilder SetXXHash3Redactor(this IRedactionBuilder builder, IConfigurationSection section, params DataClassification[] classifications) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + _ = Throw.IfNull(classifications); + + _ = builder + .Services.AddOptions() + .Services.Configure(section); + + return builder.SetRedactor(classifications); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactorProvider.cs b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactorProvider.cs new file mode 100644 index 0000000000..5331d5bd6a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactorProvider.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Compliance.Redaction; + +[SuppressMessage("Performance", "CA1812: Avoid uninstantiated internal classes", Justification = "Instantiated via reflection.")] +internal sealed class RedactorProvider : IRedactorProvider +{ + private readonly FrozenDictionary _classRedactors; + private readonly Redactor _fallbackRedactor; + + public RedactorProvider(IEnumerable redactors, IOptions options) + { + var value = Throw.IfMemberNull(options, options.Value); + + _classRedactors = GetClassRedactorMap(redactors, value.Redactors); + _fallbackRedactor = GetFallbackRedactor(redactors, options.Value.FallbackRedactor); + } + + public Redactor GetRedactor(DataClassification classification) + { + if (_classRedactors.TryGetValue(classification, out var result)) + { + return result; + } + + return _fallbackRedactor; + } + + private static FrozenDictionary GetClassRedactorMap(IEnumerable redactors, Dictionary map) + { + var dict = new Dictionary(map.Count); + foreach (var m in map) + { + foreach (var r in redactors) + { + if (r.GetType() == m.Value) + { + dict[m.Key] = r; + } + } + } + + return dict.ToFrozenDictionary(optimizeForReading: true); + } + + private static Redactor GetFallbackRedactor(IEnumerable redactors, Type defaultRedactorType) + { + foreach (var r in redactors) + { + if (r.GetType() == defaultRedactorType) + { + return r; + } + } + + // can't use exception helper here since it confuses the compiler's control flow analysis + throw new InvalidOperationException($"Couldn't find redactor of type {defaultRedactorType} in the dependency injection container."); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactorProviderOptions.cs b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactorProviderOptions.cs new file mode 100644 index 0000000000..04efeda7ed --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/RedactorProviderOptions.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Compliance.Classification; + +namespace Microsoft.Extensions.Compliance.Redaction; + +/// +/// Redactor provider options. +/// +internal sealed class RedactorProviderOptions +{ + /// + /// Gets or sets the fallback redactor to use when no classification-specific redactor exists. + /// + public Type FallbackRedactor { get; set; } = typeof(ErasingRedactor); + + /// + /// Gets a dictionary of classification-specific redactors. + /// + public Dictionary Redactors { get; } = new(); +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Redaction/XXHash3Redactor.cs b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/XXHash3Redactor.cs new file mode 100644 index 0000000000..949893e102 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/XXHash3Redactor.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using System.IO.Hashing; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Compliance.Redaction; + +/// +/// Redactor that uses xxHash3 hashing to redact data. +/// +public sealed class XXHash3Redactor : Redactor +{ + internal const int HashSize = 16; + internal const string Prefix = " + /// Initializes a new instance of the class. + /// + /// The options to control the redactor. + public XXHash3Redactor(IOptions options) + { + _seed = Throw.IfMemberNull(options, options?.Value).HashSeed; + } + + /// + public override int GetRedactedLength(ReadOnlySpan input) + { + if (input.IsEmpty) + { + return 0; + } + + return RedactedSize; + } + + /// + public override int Redact(ReadOnlySpan source, Span destination) + { + var length = GetRedactedLength(source); + + if (length == 0) + { + return 0; + } + + Throw.IfBufferTooSmall(destination.Length, length, nameof(destination)); + + var s = MemoryMarshal.AsBytes(source); + var hash = XxHash3.HashToUInt64(s, (long)_seed); + +#pragma warning disable S109 // Magic numbers should not be used + destination[24] = '>'; // do this first to avoid redundant bounds checking + destination[0] = '<'; + destination[1] = 'x'; + destination[2] = 'x'; + destination[3] = 'h'; + destination[4] = 'a'; + destination[5] = 's'; + destination[6] = 'h'; + destination[7] = ':'; +#pragma warning restore S109 // Magic numbers should not be used + +#if NETCOREAPP3_1_OR_GREATER + _ = hash.TryFormat(destination.Slice(Prefix.Length), out var _, "x", CultureInfo.InvariantCulture); +#else + var str = hash.ToString("x", CultureInfo.InvariantCulture); + for (int i = 0; i < str.Length; i++) + { + destination[Prefix.Length + i] = str[i]; + } +#endif + + return RedactedSize; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Redaction/XXHash3RedactorOptions.cs b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/XXHash3RedactorOptions.cs new file mode 100644 index 0000000000..adfb30de21 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Redaction/XXHash3RedactorOptions.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Compliance.Redaction; + +/// +/// Options for the xxHash redactor. +/// +public class XXHash3RedactorOptions +{ + /// + /// Gets or sets a hash seed used when computing hashes during redaction. + /// + /// + /// You typically pick a unique value for your application and don't change it afterwards. You'll want a different value for + /// different deployment environments in order to prevent identifiers from one environment being redacted to the same + /// value across environments. + /// + /// Default set to 0. + /// + public ulong HashSeed { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Testing/Attributes/PrivateDataAttribute.cs b/src/Libraries/Microsoft.Extensions.Compliance.Testing/Attributes/PrivateDataAttribute.cs new file mode 100644 index 0000000000..843b980a00 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Testing/Attributes/PrivateDataAttribute.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Compliance.Classification; + +namespace Microsoft.Extensions.Compliance.Testing; + +/// +/// Private data. +/// +public sealed class PrivateDataAttribute : DataClassificationAttribute +{ + /// + /// Initializes a new instance of the class. + /// + public PrivateDataAttribute() + : base(SimpleClassifications.PrivateData) + { + } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Testing/Attributes/PublicDataAttribute.cs b/src/Libraries/Microsoft.Extensions.Compliance.Testing/Attributes/PublicDataAttribute.cs new file mode 100644 index 0000000000..8d1b0aef53 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Testing/Attributes/PublicDataAttribute.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Compliance.Classification; + +namespace Microsoft.Extensions.Compliance.Testing; + +/// +/// Public data. +/// +public sealed class PublicDataAttribute : DataClassificationAttribute +{ + /// + /// Initializes a new instance of the class. + /// + public PublicDataAttribute() + : base(SimpleClassifications.PublicData) + { + } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactionCollector.cs b/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactionCollector.cs new file mode 100644 index 0000000000..2b68626838 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactionCollector.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Compliance.Testing; + +/// +/// Usage history of fake redaction types. +/// +public class FakeRedactionCollector +{ + private readonly List _redactorRequestedLog = new(); + private readonly List _dataRedactedLog = new(); + + /// + /// Gets the last redactor request "event". + /// + /// When there has been no previous redactor request event. + public RedactorRequested LastRedactorRequested + { + get + { + lock (_redactorRequestedLog) + { + if (_redactorRequestedLog.Count == 0) + { + Throw.InvalidOperationException("No redactor requested."); + } + + return _redactorRequestedLog[_redactorRequestedLog.Count - 1]; + } + } + } + + /// + /// Gets the full log of all redactor request events that happened. + /// + public IReadOnlyList AllRedactorRequests + { + get + { + lock (_redactorRequestedLog) + { + return _redactorRequestedLog.ToArray(); + } + } + } + + /// + /// Gets the last redaction "event". + /// + /// When there has been no previous redaction event. + public RedactedData LastRedactedData + { + get + { + lock (_dataRedactedLog) + { + if (_dataRedactedLog.Count == 0) + { + Throw.InvalidOperationException("No data redacted."); + } + + return _dataRedactedLog[_dataRedactedLog.Count - 1]; + } + } + } + + /// + /// Gets the full log of all redaction events that happened. + /// + public IReadOnlyList AllRedactedData + { + get + { + lock (_dataRedactedLog) + { + return _dataRedactedLog.ToArray(); + } + } + } + + internal void Append(RedactedData redactedData) + { + lock (_dataRedactedLog) + { + _dataRedactedLog.Add(redactedData); + } + } + + internal void Append(RedactorRequested redactorRequested) + { + lock (_redactorRequestedLog) + { + _redactorRequestedLog.Add(redactorRequested); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactionExtensions.cs b/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactionExtensions.cs new file mode 100644 index 0000000000..2b0919860f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactionExtensions.cs @@ -0,0 +1,152 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Compliance.Testing; + +/// +/// Extensions that allow registering a fake redactor in the application. +/// +public static class FakeRedactionExtensions +{ + /// + /// Sets the fake redactor to use for a set of data classes. + /// + /// The builder to attach the redactorr to. + /// The data classes for which the redactor type should be used. + /// The value of . + /// When is . + public static IRedactionBuilder SetFakeRedactor(this IRedactionBuilder builder, params DataClassification[] classifications) + { + _ = Throw.IfNull(builder); + + builder.Services.TryAddSingleton(); + + return builder.SetRedactor(classifications); + } + + /// + /// Sets the fake redactor to use for a set of data classes. + /// + /// The builder to attach the redactorr to. + /// Configuration function. + /// The data classes for which the redactor type should be used. + /// The value of . + /// When or are . + public static IRedactionBuilder SetFakeRedactor(this IRedactionBuilder builder, Action configure, params DataClassification[] classifications) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + builder + .Services.AddValidatedOptions() + .Services.AddValidatedOptions() + .Configure(configure) + .Services.TryAddSingleton(); + + return builder.SetRedactor(classifications); + } + + /// + /// Sets the fake redactor to use for a set of data classes. + /// + /// The builder to attach the redactorr to. + /// Configuration section. + /// The data classes for which the redactor type should be used. + /// The value of . + /// When or are . + [UnconditionalSuppressMessage("Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "The type is FakeRedactorOptions and we know it.")] + public static IRedactionBuilder SetFakeRedactor(this IRedactionBuilder builder, IConfigurationSection section, params DataClassification[] classifications) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + + builder + .Services.AddValidatedOptions() + .Services.AddValidatedOptions() + .Services.Configure(section) + .TryAddSingleton(); + + return builder.SetRedactor(classifications); + } + + /// + /// Registers the fake redactor provider that always returns fake redactor instances. + /// + /// Container used to register fake redaction classes. + /// The value of . + /// When is . + public static IServiceCollection AddFakeRedaction(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + services.TryAddSingleton(); + services.TryAddSingleton(serviceProvider => + { + var collector = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>().Value; + return new FakeRedactorProvider(options, collector); + }); + + return services + .AddValidatedOptions() + .Services.AddValidatedOptions() + .Services; + } + + /// + /// Registers the fake redactor provider that always returns fake redactor instances. + /// + /// Container used to register fake redaction classes. + /// Configures fake redactor. + /// The value of . + /// When or > are . + public static IServiceCollection AddFakeRedaction(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + services.TryAddSingleton(); + services.TryAddSingleton(serviceProvider => + { + var collector = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>().Value; + + return new FakeRedactorProvider(options, collector); + }); + + return services + .AddValidatedOptions() + .Services.AddValidatedOptions() + .Configure(configure) + .Services; + } + + /// + /// Gets the fake redacton collector instance from the dependency injection container. + /// + /// Container used to obtain collector instance. + /// Obtained collector. + /// When collector is not in the container. + /// When is . + /// + /// should be registered and used only with fake redaction implementation. + /// + public static FakeRedactionCollector GetFakeRedactionCollector(this IServiceProvider serviceProvider) + { + _ = Throw.IfNull(serviceProvider); + return serviceProvider.GetRequiredService(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactor.cs b/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactor.cs new file mode 100644 index 0000000000..ce1491d50f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactor.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using System.Threading; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Text; + +namespace Microsoft.Extensions.Compliance.Testing; + +/// +/// Redactor designed for use in tests. +/// +public class FakeRedactor : Redactor +{ + private readonly CompositeFormat _format; + private int _redactedSoFar; + + /// + /// Gets the collector of redaction events. + /// + public FakeRedactionCollector EventCollector { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The options to control behavior of redactor. + /// Collects info about redacted values. + public FakeRedactor(IOptions? options = null, FakeRedactionCollector? collector = null) + { + var opt = options ?? Microsoft.Extensions.Options.Options.Create(new FakeRedactorOptions()); + EventCollector = collector ?? new FakeRedactionCollector(); + + _format = GetRedactionFormat(opt.Value.RedactionFormat); + } + + /// + /// Initializes a new instance of the class. + /// + /// The options to control behavior of redactor. + /// Collects info about redacted values. + /// New instance of . + public static FakeRedactor Create(FakeRedactorOptions? options = null, FakeRedactionCollector? collector = null) => new(Options.Options.Create(options ?? new FakeRedactorOptions()), collector); + + /// + public override int Redact(ReadOnlySpan source, Span destination) + { + Throw.IfBufferTooSmall(destination.Length, GetRedactedLength(source), nameof(destination)); + + int charsWritten; + + var sourceString = source.ToString(); + + if (_format.NumArgumentsNeeded == 0) + { + _ = _format.TryFormat(destination, out charsWritten, CultureInfo.InvariantCulture, Array.Empty()); + } + else + { + _ = _format.TryFormat(destination, out charsWritten, CultureInfo.InvariantCulture, sourceString); + } + + var order = Interlocked.Increment(ref _redactedSoFar); + + EventCollector.Append(new RedactedData(sourceString, destination.Slice(0, charsWritten).ToString(), order)); + + return charsWritten; + } + + /// + public override int GetRedactedLength(ReadOnlySpan input) + { + if (_format.NumArgumentsNeeded == 0) + { + return _format.Format(CultureInfo.InvariantCulture, Array.Empty()).Length; + } + + return _format.Format(CultureInfo.InvariantCulture, input.ToString()).Length; + } + + private static CompositeFormat GetRedactionFormat(string redactionFormat) + { + if (!CompositeFormat.TryParse(redactionFormat, out var parsed, out var error)) + { + Throw.ArgumentException(nameof(FakeRedactorOptions.RedactionFormat), error); + } + + return parsed; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactorOptions.cs b/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactorOptions.cs new file mode 100644 index 0000000000..6cab2be994 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactorOptions.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Compliance.Testing; + +/// +/// Options to control the fake redactor. +/// +public class FakeRedactorOptions +{ + internal const string DefaultFormat = "{0}"; + + /// + /// Gets or sets a value indicating how to format redacted data. + /// + /// + /// This is a composite format string that determines how redacted data looks like. + /// Defaults to {0}. + /// + [Required] + [StringSyntax(StringSyntaxAttribute.CompositeFormat)] + public string RedactionFormat { get; set; } = DefaultFormat; +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactorOptionsAutoValidator.cs b/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactorOptionsAutoValidator.cs new file mode 100644 index 0000000000..848234268c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactorOptionsAutoValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Compliance.Testing; + +[OptionsValidator] +internal sealed partial class FakeRedactorOptionsAutoValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactorOptionsCustomValidator.cs b/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactorOptionsCustomValidator.cs new file mode 100644 index 0000000000..e0f3d1dd28 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactorOptionsCustomValidator.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Shared.Text; +using Validation = Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Compliance.Testing; + +internal sealed class FakeRedactorOptionsCustomValidator : IValidateOptions +{ + internal const int MaxNumberOfArgumentsForRedactionFormat = 1; + + public ValidateOptionsResult Validate(string? name, FakeRedactorOptions options) + { + var builder = new ValidateOptionsResultBuilder(); + + if (!CompositeFormat.TryParse(options.RedactionFormat, out var compositeFormat, out var error)) + { + builder.AddError( + $"{nameof(options.RedactionFormat)} must be a valid .NET format string: {error}", + nameof(options.RedactionFormat)); + } + else if (compositeFormat.NumArgumentsNeeded > MaxNumberOfArgumentsForRedactionFormat) + { + builder.AddError( + $"{nameof(options.RedactionFormat)} must take no more than {MaxNumberOfArgumentsForRedactionFormat} arguments. Currently found {compositeFormat.NumArgumentsNeeded}.", + nameof(options.RedactionFormat)); + } + + return builder.Build(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactorProvider.cs b/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactorProvider.cs new file mode 100644 index 0000000000..25b9d57d1b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Testing/FakeRedactorProvider.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; + +namespace Microsoft.Extensions.Compliance.Testing; + +/// +/// A provider of fake redactors. +/// +public class FakeRedactorProvider : IRedactorProvider +{ + private readonly FakeRedactor _redactor; + private int _redactorsRequestedSoFar; + + /// + /// Gets the collector that stores data about usage of fake redaction classes. + /// + public FakeRedactionCollector Collector { get; } + + /// + /// Initializes a new instance of the class. + /// + /// Fake redactor options. + /// Collects information about redactor requests. + public FakeRedactorProvider(FakeRedactorOptions? options = null, FakeRedactionCollector? eventCollector = null) + { + Collector = eventCollector ?? new FakeRedactionCollector(); + _redactor = new FakeRedactor(Microsoft.Extensions.Options.Options.Create(options ?? new FakeRedactorOptions()), Collector); + } + + /// + public Redactor GetRedactor(DataClassification classification) + { + var order = Interlocked.Increment(ref _redactorsRequestedSoFar); + + Collector.Append(new RedactorRequested(classification, order)); + + return _redactor; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Testing/Microsoft.Extensions.Compliance.Testing.csproj b/src/Libraries/Microsoft.Extensions.Compliance.Testing/Microsoft.Extensions.Compliance.Testing.csproj new file mode 100644 index 0000000000..a09c5578f9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Testing/Microsoft.Extensions.Compliance.Testing.csproj @@ -0,0 +1,38 @@ + + + Microsoft.Extensions.Compliance.Testing + Implementation of data classification and redaction designed for testing. + Fundamentals + $(PackageTags);Testing + + + + true + true + true + true + true + + + + normal + 100 + 100 + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Testing/RedactedData.cs b/src/Libraries/Microsoft.Extensions.Compliance.Testing/RedactedData.cs new file mode 100644 index 0000000000..e222001f54 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Testing/RedactedData.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Compliance.Testing; + +/// +/// State representing a single redaction "event". +/// +public readonly struct RedactedData : IEquatable +{ + /// + /// Gets the original data that got redacted. + /// + public string Original { get; } + + /// + /// Gets the redacted data. + /// + public string Redacted { get; } + + /// + /// Gets the order in which data was redacted. + /// + public int SequenceNumber { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// Data that was redacted. + /// Redacted data. + /// Order in which data were redacted. + public RedactedData(string original, string redacted, int sequenceNumber) + { + Original = Throw.IfNull(original); + Redacted = Throw.IfNull(redacted); + SequenceNumber = sequenceNumber; + } + + /// + /// Checks if object is equal to this instance of . + /// + /// Object to check for equality. + /// if object instances are equal otherwise. + public override bool Equals(object? obj) => obj is RedactedData other && Equals(other); + + /// + /// Checks if object is equal to this instance of . + /// + /// Instance to check for equality. + /// if object instances are equal otherwise. + public bool Equals(RedactedData other) => other.Original == Original && other.Redacted == Redacted && other.SequenceNumber == SequenceNumber; + + /// + /// Get hashcode of given . + /// + /// Hash code. + public override int GetHashCode() => HashCode.Combine(Original, Redacted, SequenceNumber); + + /// + /// Compares two instances. + /// + /// Left argument of the comparison. + /// Right argument of the comparison. + /// when equal, otherwise. + public static bool operator ==(RedactedData left, RedactedData right) + { + return left.Equals(right); + } + + /// + /// Compares two instances. + /// + /// Left argument of the comparison. + /// Right argument of the comparison. + /// when not equal, otherwise. + public static bool operator !=(RedactedData left, RedactedData right) + { + return !(left == right); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Testing/RedactorRequested.cs b/src/Libraries/Microsoft.Extensions.Compliance.Testing/RedactorRequested.cs new file mode 100644 index 0000000000..bfb045ae2b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Testing/RedactorRequested.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Compliance.Classification; + +namespace Microsoft.Extensions.Compliance.Testing; + +/// +/// State representing a single request for a redactor. +/// +public readonly struct RedactorRequested : IEquatable +{ + /// + /// Gets the the data classification for which the redactor was returned. + /// + public DataClassification DataClassification { get; } + + /// + /// Gets the order in which the redactor was requested. + /// + public int SequenceNumber { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// Data class for which redactor was used. + /// Order in which the request was used. + public RedactorRequested(DataClassification classification, int sequenceNumber) + { + DataClassification = classification; + SequenceNumber = sequenceNumber; + } + + /// + /// Checks if object is equal to this instance of . + /// + /// Object to check for equality. + /// if object instances are equal otherwise. + public override bool Equals(object? obj) => obj is RedactorRequested other && Equals(other); + + /// + /// Checks if object is equal to this instance of . + /// + /// Instance to check for equality. + /// if object instances are equal otherwise. + public bool Equals(RedactorRequested other) => other.SequenceNumber == SequenceNumber && other.DataClassification == DataClassification; + + /// + /// Get hashcode of given . + /// + /// Hash code. + public override int GetHashCode() => HashCode.Combine(SequenceNumber, DataClassification); + + /// + /// Compares two instances. + /// + /// Left argument of the comparison. + /// Right argument of the comparison. + /// when equal, otherwise. + public static bool operator ==(RedactorRequested left, RedactorRequested right) + { + return left.Equals(right); + } + + /// + /// Compares two instances. + /// + /// Left argument of the comparison. + /// Right argument of the comparison. + /// when not equal, otherwise. + public static bool operator !=(RedactorRequested left, RedactorRequested right) + { + return !(left == right); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Testing/SimpleClassifications.cs b/src/Libraries/Microsoft.Extensions.Compliance.Testing/SimpleClassifications.cs new file mode 100644 index 0000000000..4f1375514a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Testing/SimpleClassifications.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Compliance.Classification; + +namespace Microsoft.Extensions.Compliance.Testing; + +/// +/// Simple data classifications. +/// +public static class SimpleClassifications +{ + /// + /// Gets the name of this classification taxonomy. + /// + public static string TaxonomyName => typeof(SimpleTaxonomy).FullName!; + + /// + /// Gets the private data classification. + /// + public static DataClassification PrivateData => new(TaxonomyName, (ulong)SimpleTaxonomy.PrivateData); + + /// + /// Gets the public data classification. + /// + public static DataClassification PublicData => new(TaxonomyName, (ulong)SimpleTaxonomy.PublicData); +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Testing/SimpleTaxonomy.cs b/src/Libraries/Microsoft.Extensions.Compliance.Testing/SimpleTaxonomy.cs new file mode 100644 index 0000000000..2248463071 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Testing/SimpleTaxonomy.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Compliance.Classification; + +namespace Microsoft.Extensions.Compliance.Testing; + +/// +/// Classes of data used for simple scenarios. +/// +[Flags] +public enum SimpleTaxonomy : ulong +{ + /// + /// No data classification. + /// + None = DataClassification.NoneTaxonomyValue, + + /// + /// This is public data. + /// + PublicData = 1 << 0, + + /// + /// This is private data. + /// + PrivateData = 1 << 1, + + /// + /// Unknown data classification, handle with care. + /// + Unknown = DataClassification.UnknownTaxonomyValue, +} diff --git a/src/Libraries/Microsoft.Extensions.Compliance.Testing/SimpleTaxonomyExtensions.cs b/src/Libraries/Microsoft.Extensions.Compliance.Testing/SimpleTaxonomyExtensions.cs new file mode 100644 index 0000000000..9ccf057998 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Compliance.Testing/SimpleTaxonomyExtensions.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Compliance.Testing; + +/// +/// Extensions for working with the simple data classification taxonomy. +/// +public static class SimpleTaxonomyExtensions +{ + /// + /// Gets the taxonomy value associated with a particular data classification. + /// + /// The data classification of interest. + /// The resulting taxonomy value for the given data classification. + public static SimpleTaxonomy AsSimpleTaxonomy(this DataClassification classification) + { + if (classification.TaxonomyName != SimpleClassifications.TaxonomyName && !string.IsNullOrEmpty(classification.TaxonomyName)) + { + Throw.ArgumentException(nameof(classification), $"Unknown data taxonomy: {classification.TaxonomyName}"); + } + + return (SimpleTaxonomy)classification.Value; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Abstractions/ExceptionSummary.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Abstractions/ExceptionSummary.cs new file mode 100644 index 0000000000..9ca9550c18 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Abstractions/ExceptionSummary.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.ExceptionSummarization; + +/// +/// Holds a summary of an exception for use in telemetry. +/// +/// +/// Metric dimensions typically support a limited number of distinct values, and as such they are not suitable +/// to represent values which are highly variable, such as the result of . +/// An exception summary represents a low-cardinality version of an exception's information, suitable for such +/// cases. The summary never includes sensitive information. +/// +public readonly struct ExceptionSummary : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The type of the exception. + /// A summary description string for telemetry. + /// An additional details string, primarily for diagnostics and not telemetry. + public ExceptionSummary(string exceptionType, string description, string additionalDetails) + { + ExceptionType = Throw.IfNullOrWhitespace(exceptionType); + Description = Throw.IfNullOrWhitespace(description); + AdditionalDetails = Throw.IfNull(additionalDetails); + } + + /// + /// Gets the type description of the exception. + /// + /// + /// This is not guaranteed to be a type name. In particular, for inner exceptions, this will include the + /// type name of the outer exception along with the type name of the inner exception. + /// + public string ExceptionType { get; } + + /// + /// Gets the summary description of the exception. + /// + public string Description { get; } + + /// + /// Gets the additional details of the exception. + /// + /// + /// This string can have a relatively high cardinality and is therefore not suitable as a metric dimension. It + /// is primarily intended for use in low-level diagnostics. + /// + public string AdditionalDetails { get; } + + /// + /// Gets a hash code for this object. + /// + /// A hash code for the current object. + public override int GetHashCode() => HashCode.Combine( + ExceptionType.GetHashCode(StringComparison.Ordinal), + Description.GetHashCode(StringComparison.Ordinal), + AdditionalDetails.GetHashCode(StringComparison.Ordinal)); + + /// + /// Gets a string representation of this object. + /// + /// A string representing this object. + public override string ToString() + { + return AdditionalDetails.Length == 0 + ? $"{ExceptionType}:{Description}:" + : $"{ExceptionType}:{Description}:{AdditionalDetails}"; + } + + /// + /// Determines whether this summary and a specified other summary are identical. + /// + /// The other summary. + /// if the two summaries are identical; otherwise, . + public override bool Equals(object? obj) => obj is ExceptionSummary summary && Equals(summary); + + /// + /// Determines whether this summary and a specified other summary are identical. + /// + /// The other summary. + /// if the two summaries are identical; otherwise, . + public bool Equals(ExceptionSummary other) + { + return other.ExceptionType.Equals(ExceptionType, StringComparison.Ordinal) + && other.Description.Equals(Description, StringComparison.Ordinal) + && other.AdditionalDetails.Equals(AdditionalDetails, StringComparison.Ordinal); + } + + /// + /// Equality operator. + /// + /// First value. + /// Second value. + /// , if its operands are equal, otherwise. + public static bool operator ==(ExceptionSummary left, ExceptionSummary right) + { + return left.Equals(right); + } + + /// + /// Inequality operator. + /// + /// First value. + /// Second value. + /// , if its operands are equal, otherwise. + public static bool operator !=(ExceptionSummary left, ExceptionSummary right) + { + return !left.Equals(right); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Abstractions/IExceptionSummarizationBuilder.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Abstractions/IExceptionSummarizationBuilder.cs new file mode 100644 index 0000000000..a7a08eb4d3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Abstractions/IExceptionSummarizationBuilder.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Diagnostics.ExceptionSummarization; + +/// +/// Abstraction to register new exception summary providers. +/// +public interface IExceptionSummarizationBuilder +{ + /// + /// Gets the service collection into which the summary provider instances are registered. + /// + public IServiceCollection Services { get; } + + /// + /// Adds a summary provider to the builder. + /// + /// The type of the provider. + /// The current instance. + IExceptionSummarizationBuilder AddProvider<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>() + where T : class, IExceptionSummaryProvider; +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Abstractions/IExceptionSummarizer.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Abstractions/IExceptionSummarizer.cs new file mode 100644 index 0000000000..c9c4c4ef48 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Abstractions/IExceptionSummarizer.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Diagnostics.ExceptionSummarization; + +/// +/// Provides a mechanism to summarize exceptions for use in telemetry. +/// +public interface IExceptionSummarizer +{ + /// + /// Gives the best available summary of a given for telemetry. + /// + /// The exception to summarize. + /// The summary of the given . + public ExceptionSummary Summarize(Exception exception); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Abstractions/IExceptionSummaryProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Abstractions/IExceptionSummaryProvider.cs new file mode 100644 index 0000000000..7f75f7ac7d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Abstractions/IExceptionSummaryProvider.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Diagnostics.ExceptionSummarization; + +/// +/// The interface implemented by components which know how to summarize exceptions. +/// +/// +/// This is the interface implemented by summary providers which are consumed by the higher-level +/// summarization components. To receive summary information, applications use +/// instead. +/// +public interface IExceptionSummaryProvider +{ + /// + /// Provides the index of the description for the exception along with optional additional data. + /// + /// The exception. + /// The additional details of the given exception, if any. + /// The index of the description. + /// + /// This method should only get invoked with an exception which is type compatible with a type + /// described by . + /// + public int Describe(Exception exception, out string? additionalDetails); + + /// + /// Gets the set of supported exception types that can be handled by this provider. + /// + public IEnumerable SupportedExceptionTypes { get; } + + /// + /// Gets the set of description strings exposed by this provider. + /// + public IReadOnlyList Descriptions { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Implementation/ExceptionSummarizationBuilder.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Implementation/ExceptionSummarizationBuilder.cs new file mode 100644 index 0000000000..f1a2e267b5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Implementation/ExceptionSummarizationBuilder.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.Diagnostics.ExceptionSummarization; + +internal sealed class ExceptionSummarizationBuilder : IExceptionSummarizationBuilder +{ + public ExceptionSummarizationBuilder(IServiceCollection services) + { + Services = services; + } + + public IServiceCollection Services { get; } + + public IExceptionSummarizationBuilder AddProvider<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>() + where T : class, IExceptionSummaryProvider + { + Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + return this; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Implementation/ExceptionSummarizationExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Implementation/ExceptionSummarizationExtensions.cs new file mode 100644 index 0000000000..dce8faff6f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Implementation/ExceptionSummarizationExtensions.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.ExceptionSummarization; + +/// +/// Controls exception summarization. +/// +public static class ExceptionSummarizationExtensions +{ + /// + /// Registers an exception summarizer into a dependency injection container. + /// + /// The dependency injection container to add the summarizer to. + /// The value of . + /// If is . + public static IServiceCollection AddExceptionSummarizer(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + services.TryAddSingleton(); + return services; + } + + /// + /// Registers an exception summarizer into a dependency injection container. + /// + /// The dependency injection container to add the summarizer to. + /// Delegates that configures the set of registered summary providers. + /// The value of . + /// If or are . + public static IServiceCollection AddExceptionSummarizer(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + services.TryAddSingleton(); + configure(new ExceptionSummarizationBuilder(services)); + + return services; + } + + /// + /// Registers a summary provider that handles , , and . + /// + /// The builder to attach the provider to. + /// The value of . + /// If is . + public static IExceptionSummarizationBuilder AddHttpProvider(this IExceptionSummarizationBuilder builder) => Throw.IfNull(builder).AddProvider(); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Implementation/ExceptionSummarizer.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Implementation/ExceptionSummarizer.cs new file mode 100644 index 0000000000..bcda3f7beb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Implementation/ExceptionSummarizer.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Text; + +namespace Microsoft.Extensions.Diagnostics.ExceptionSummarization; + +/// +/// Looks through all the registered summary providers, returns a summary if possible. +/// +internal sealed class ExceptionSummarizer : IExceptionSummarizer +{ + private const string DefaultDescription = "Unknown"; + private readonly FrozenDictionary _exceptionTypesToProviders; + + /// + /// Initializes a new instance of the class. + /// + /// All registered exception providers. + public ExceptionSummarizer(IEnumerable providers) + { + var exceptionTypesToProvidersBuilder = new Dictionary(); + foreach (var exceptionSummaryProvider in providers) + { + foreach (var exceptionType in exceptionSummaryProvider.SupportedExceptionTypes) + { + exceptionTypesToProvidersBuilder.Add(exceptionType, exceptionSummaryProvider); + } + } + + _exceptionTypesToProviders = exceptionTypesToProvidersBuilder.ToFrozenDictionary(optimizeForReading: true); + } + + /// + /// It iterates through all registered summarizers, returns a summary if possible. + /// Default is message if its length is less than 32, otherwise exception type name. + /// + /// The exception. + /// The summary of the given . + public ExceptionSummary Summarize(Exception exception) + { + _ = Throw.IfNull(exception); + var exceptionType = exception.GetType(); + var exceptionTypeName = exception.GetType().Name; + + if (_exceptionTypesToProviders.TryGetValue(exceptionType, out var exceptionSummaryProvider)) + { + return BuildSummary(exception, exceptionSummaryProvider, exceptionTypeName); + } + + // Let's see if we get lucky with the inner exception + if (exception.InnerException != null) + { + var innerExceptionType = exception.InnerException.GetType(); + if (_exceptionTypesToProviders.TryGetValue(innerExceptionType, out var innerExceptionSummaryProvider)) + { + return BuildSummary( + exception.InnerException, + innerExceptionSummaryProvider, + $"{exceptionTypeName}->{innerExceptionType.Name}"); + } + } + + // Now let's see if we can get something from Exception HResult + var hresult = exception.HResult; + var exceptionDescription = exception.InnerException != null + ? exception.InnerException.GetType().Name + : DefaultDescription; + if (hresult != default) + { + return new ExceptionSummary( + exceptionTypeName, + exceptionDescription, + hresult.ToInvariantString()); + } + + // final recourse, generate a default message + return new ExceptionSummary( + exceptionTypeName, + exceptionDescription, + DefaultDescription); + } + + private static ExceptionSummary BuildSummary( + Exception exception, + IExceptionSummaryProvider exceptionSummaryProvider, + string exceptionType) + { + var descriptionIndex = exceptionSummaryProvider.Describe(exception, out var additionalDetails); + + if (descriptionIndex >= exceptionSummaryProvider.Descriptions.Count || descriptionIndex < 0) + { + return new ExceptionSummary( + exceptionType, + DefaultDescription, + $"Exception summary provider {exceptionSummaryProvider.GetType().Name} returned invalid short description index {descriptionIndex}"); + } + + return new ExceptionSummary(exceptionType, exceptionSummaryProvider.Descriptions[descriptionIndex], additionalDetails ?? DefaultDescription); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Implementation/HttpExceptionSummaryProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Implementation/HttpExceptionSummaryProvider.cs new file mode 100644 index 0000000000..b34e334554 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Implementation/HttpExceptionSummaryProvider.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using Microsoft.Extensions.EnumStrings; +using Microsoft.Shared.Diagnostics; + +[assembly: EnumStrings(typeof(WebExceptionStatus))] +[assembly: EnumStrings(typeof(SocketError))] + +namespace Microsoft.Extensions.Diagnostics.ExceptionSummarization; + +/// +/// Http exception diagnosis for telemetry. +/// +internal sealed class HttpExceptionSummaryProvider : IExceptionSummaryProvider +{ + private const int DefaultDescriptionIndex = -1; + private const string TaskCanceled = "TaskCanceled"; + private const string TaskTimeout = "TaskTimeout"; + private static readonly FrozenDictionary _webExceptionStatusMap; + private static readonly FrozenDictionary _socketErrorMap; + private static readonly ImmutableArray _descriptions; + + [SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline", Justification = "Can't do this since the field values are interdependent")] + static HttpExceptionSummaryProvider() + { + var descriptions = new List + { + TaskCanceled, + TaskTimeout + }; + + var socketErrors = new Dictionary(); + foreach (var v in Enum.GetValues(typeof(SocketError))) + { + var socketError = (SocketError)v!; + var name = socketError.ToInvariantString(); + + socketErrors[socketError] = descriptions.Count; + descriptions.Add(name); + } + + var webStatuses = new Dictionary(); + foreach (var v in Enum.GetValues(typeof(WebExceptionStatus))) + { + var status = (WebExceptionStatus)v!; + var name = status.ToInvariantString(); + + webStatuses[status] = descriptions.Count; + descriptions.Add(name); + } + + _descriptions = descriptions.ToImmutableArray(); + _socketErrorMap = socketErrors.ToFrozenDictionary(optimizeForReading: true); + _webExceptionStatusMap = webStatuses.ToFrozenDictionary(optimizeForReading: true); + } + + public IEnumerable SupportedExceptionTypes { get; } = new[] + { + typeof(TaskCanceledException), + typeof(OperationCanceledException), + typeof(WebException), + typeof(SocketException), + }; + + public IReadOnlyList Descriptions => _descriptions; + + public int Describe(Exception exception, out string? additionalDetails) + { + _ = Throw.IfNull(exception); + + additionalDetails = null; + switch (exception) + { + case OperationCanceledException ex: + { + return ex.CancellationToken.IsCancellationRequested ? 0 : 1; + } + + case WebException ex: + { + if (_webExceptionStatusMap.TryGetValue(ex.Status, out var index)) + { + return index; + } + + break; + } + + case SocketException ex: + { + if (_socketErrorMap.TryGetValue(ex.SocketErrorCode, out var index)) + { + return index; + } + + break; + } + } + + return DefaultDescriptionIndex; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Microsoft.Extensions.Diagnostics.ExceptionSummarization.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Microsoft.Extensions.Diagnostics.ExceptionSummarization.csproj new file mode 100644 index 0000000000..0f91252406 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization/Microsoft.Extensions.Diagnostics.ExceptionSummarization.csproj @@ -0,0 +1,33 @@ + + + Microsoft.Extensions.Diagnostics.ExceptionSummarization + Lets you retrieve exception summary information. + Telemetry + + + + true + true + true + + + + normal + 100 + 100 + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ApplicationLifecycleHealthCheck.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ApplicationLifecycleHealthCheck.cs new file mode 100644 index 0000000000..283202e704 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ApplicationLifecycleHealthCheck.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +/// +/// Health check which considers the application healthy after it is reported as started by +/// and unhealthy when it is shutting down. +/// +internal sealed class ApplicationLifecycleHealthCheck : IHealthCheck +{ + private static readonly Task _healthy = Task.FromResult(HealthCheckResult.Healthy()); + private static readonly Task _unhealthy = Task.FromResult(HealthCheckResult.Unhealthy()); + private readonly IHostApplicationLifetime _appLifetime; + + /// + /// Initializes a new instance of the class. + /// + /// Reference to application lifetime. + public ApplicationLifecycleHealthCheck(IHostApplicationLifetime appLifetime) + { + _appLifetime = appLifetime; + } + + /// + /// Runs the health check, returning the status of the component being checked. + /// + /// + /// This method is called from + /// with period and other settings defined in . + /// + /// A context object associated with the current execution. + /// A System.Threading.CancellationToken that can be used to cancel the health check. + /// + /// A that completes when the health check has finished, + /// yielding the status of the component being checked. + /// + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + bool isStarted = _appLifetime.ApplicationStarted.IsCancellationRequested; + bool isStopping = _appLifetime.ApplicationStopping.IsCancellationRequested; + bool isStopped = _appLifetime.ApplicationStopped.IsCancellationRequested; + bool isHealthy = isStarted && !isStopping && !isStopped; + return isHealthy ? _healthy : _unhealthy; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.ApplicationLifecycle.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.ApplicationLifecycle.cs new file mode 100644 index 0000000000..c2d0cedeca --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.ApplicationLifecycle.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +/// +/// Controls various health check features. +/// +public static partial class CoreHealthChecksExtensions +{ + /// + /// Registers a health check provider that's tied to the application's lifecycle. + /// + /// The builder to add the provider to. + /// The value of . + /// If is . + /// The application's lifecycle is tracked through . + public static IHealthChecksBuilder AddApplicationLifecycleHealthCheck(this IHealthChecksBuilder builder) + { + _ = Throw.IfNull(builder); + + return builder.AddCheck("ApplicationLifecycleHealthCheck"); + } + + /// + /// Registers a health check provider that's tied to the application's lifecycle. + /// + /// The builder to add the provider to. + /// A list of tags that can be used to filter health checks. + /// The value of . + /// If or are . + /// The application's lifecycle is tracked through . + public static IHealthChecksBuilder AddApplicationLifecycleHealthCheck(this IHealthChecksBuilder builder, IEnumerable tags) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(tags); + + return builder.AddCheck("ApplicationLifecycleHealthCheck", tags: tags); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.KubernetesPublisher.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.KubernetesPublisher.cs new file mode 100644 index 0000000000..6316f1d98b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.KubernetesPublisher.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +public static partial class CoreHealthChecksExtensions +{ + /// + /// Registers a health status publisher which opens a TCP port if the application is considered healthy. + /// + /// The to add the publisher to. + /// The value of . + /// If is . + public static IServiceCollection AddKubernetesHealthCheckPublisher(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + return services.AddSingleton(); + } + + /// + /// Registers a health status publisher which opens a TCP port if the application is considered healthy. + /// + /// The to add the publisher to. + /// Configuration for . + /// The value of . + /// If or are . + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(KubernetesHealthCheckPublisherOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed by [DynamicDependency]")] + public static IServiceCollection AddKubernetesHealthCheckPublisher( + this IServiceCollection services, + IConfigurationSection section) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(section); + + return services + .Configure(section) + .AddKubernetesHealthCheckPublisher(); + } + + /// + /// Registers a health status publisher which opens a TCP port if the application is considered healthy. + /// + /// The to add the publisher to. + /// Configuration for . + /// The value of . + /// If or are . + public static IServiceCollection AddKubernetesHealthCheckPublisher( + this IServiceCollection services, + Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + return services + .Configure(configure) + .AddKubernetesHealthCheckPublisher(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.Manual.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.Manual.cs new file mode 100644 index 0000000000..485eb2c930 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.Manual.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +public static partial class CoreHealthChecksExtensions +{ + /// + /// Registers a health check provider that enables manual control of the application's health. + /// + /// The builder to add the provider to. + /// The value of . + /// If is . + public static IHealthChecksBuilder AddManualHealthCheck(this IHealthChecksBuilder builder) + => Throw.IfNull(builder) + .AddManualHealthCheckDependencies() + .AddCheck("ManualHealthCheck"); + + /// + /// Registers a health check provider that enables manual control of the application's health. + /// + /// The builder to add the provider to. + /// A list of tags that can be used to filter health checks. + /// The value of . + /// If or are . + public static IHealthChecksBuilder AddManualHealthCheck(this IHealthChecksBuilder builder, IEnumerable tags) + => Throw.IfNull(builder) + .AddManualHealthCheckDependencies() + .AddCheck("ManualHealthCheck", tags: Throw.IfNull(tags)); + + /// + /// Sets the manual health check to the healthy state. + /// + /// The . + /// If is . + public static void ReportHealthy(this IManualHealthCheck manualHealthCheck) + => Throw.IfNull(manualHealthCheck).Result = HealthCheckResult.Healthy(); + + /// + /// Sets the manual health check to return an unhealthy states and an associated reason. + /// + /// The . + /// The reason why the health check is unhealthy. + /// If is . + public static void ReportUnhealthy(this IManualHealthCheck manualHealthCheck, string reason) + => Throw.IfNull(manualHealthCheck).Result = HealthCheckResult.Unhealthy(Throw.IfNullOrWhitespace(reason)); + + private static IHealthChecksBuilder AddManualHealthCheckDependencies(this IHealthChecksBuilder builder) + => builder + .Services.AddSingleton() + .AddTransient(typeof(IManualHealthCheck<>), typeof(ManualHealthCheck<>)) + .AddHealthChecks(); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.TelemetryPublisher.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.TelemetryPublisher.cs new file mode 100644 index 0000000000..a4d278c60c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/CoreHealthChecksExtensions.TelemetryPublisher.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Telemetry.Metering; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +public static partial class CoreHealthChecksExtensions +{ + /// + /// Registers a health status publisher which emits logs and metrics tracking the application's health. + /// + /// The to add the publisher to. + /// The so that additional calls can be chained. + public static IServiceCollection AddTelemetryHealthCheckPublisher(this IServiceCollection services) + => services + .RegisterMetering() + .AddSingleton(); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/IManualHealthCheck.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/IManualHealthCheck.cs new file mode 100644 index 0000000000..6772a33f0d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/IManualHealthCheck.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +/// +/// Lets you manually set the health status of the application. +/// +public interface IManualHealthCheck : IDisposable +{ + /// + /// Gets or sets the health status. + /// + public HealthCheckResult Result { get; set; } +} + +/// +/// Lets you manually set the application's health status. +/// +/// The type of . +public interface IManualHealthCheck : IManualHealthCheck +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/IManualHealthCheckTracker.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/IManualHealthCheckTracker.cs new file mode 100644 index 0000000000..6fc00cacc7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/IManualHealthCheckTracker.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +/// +/// A helper to track all instances of IManualHealthCheck registered in the application. +/// +internal interface IManualHealthCheckTracker +{ + /// + /// Registers a new IManualHealthCheck into the tracker. + /// + /// The manual health check to be added. + void Register(IManualHealthCheck check); + + /// + /// Removes a IManualHealthCheck from the tracker. + /// + /// The manual health check to be removed. + void Unregister(IManualHealthCheck checkToRemove); + + /// + /// Gets the HealthCheckResult generated from the registered list of IManualHealthCheck. + /// + /// + /// A containing the HealthCheckResult generated. + /// + HealthCheckResult GetHealthCheckResult(); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/KubernetesHealthCheckPublisher.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/KubernetesHealthCheckPublisher.cs new file mode 100644 index 0000000000..3966d1407a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/KubernetesHealthCheckPublisher.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +/// +/// Opens a TCP port if the service is healthy and closes it otherwise. +/// +internal sealed class KubernetesHealthCheckPublisher : IHealthCheckPublisher +{ + private readonly TcpListener _server; + private readonly int _maxLengthOfPendingConnectionsQueue; + + /// + /// Initializes a new instance of the class. + /// + /// Creation options. + public KubernetesHealthCheckPublisher(IOptions options) + { + var value = Throw.IfMemberNull(options, options.Value); + + _server = new TcpListener(IPAddress.Any, value.TcpPort); + _maxLengthOfPendingConnectionsQueue = value.MaxPendingConnections; + } + + /// + /// Publishes the provided report. + /// + /// The . The result of executing a set of health checks. + /// Not used in the current implementation. + /// Task.CompletedTask. + public Task PublishAsync(HealthReport report, CancellationToken cancellationToken) + { + _ = Throw.IfNull(report); + + if (report.Status == HealthStatus.Healthy) + { + if (!_server.Server.IsBound) + { + _server.Start(_maxLengthOfPendingConnectionsQueue); + _ = Task.Run(() => TcpServerAsync(), CancellationToken.None); + } + } + else + { + _server.Stop(); + } + + return Task.CompletedTask; + } + + [SuppressMessage("Blocker Bug", "S2190:Recursion should not be infinite", Justification = "runs in background")] + [SuppressMessage("Resilience", "R9A061:The async method doesn't support cancellation", Justification = "runs in background")] + private async Task TcpServerAsync() + { + while (true) + { + using var client = await _server.AcceptTcpClientAsync().ConfigureAwait(false); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/KubernetesHealthCheckPublisherOptions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/KubernetesHealthCheckPublisherOptions.cs new file mode 100644 index 0000000000..b3d1fbe4f3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/KubernetesHealthCheckPublisherOptions.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +/// +/// Options to control the Kubernetes health status publisher. +/// +[SuppressMessage("Major Code Smell", "S109:Magic numbers should not be used", Justification = "In place numbers make the ranges cleaner")] +public class KubernetesHealthCheckPublisherOptions +{ + private const int DefaultMaxPendingConnections = 10; + private const int DefaultTcpPort = 2305; + + /// + /// Gets or sets the TCP port which gets opened if the application is healthy and closed otherwise. + /// + /// + /// Default set to 2305. + /// + [Range(1, 65535)] + public int TcpPort { get; set; } = DefaultTcpPort; + + /// + /// Gets or sets the maximum length of the pending connections queue. + /// + /// + /// Default set to 10. + /// + [Range(1, 10000)] + public int MaxPendingConnections { get; set; } = DefaultMaxPendingConnections; +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Log.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Log.cs new file mode 100644 index 0000000000..3b4994d1f0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Log.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +internal static partial class Log +{ + [LogMethod(0, LogLevel.Warning, "Process reporting unhealthy: {status}. Health check entries are {entries}")] + public static partial void Unhealthy( + ILogger logger, + HealthStatus status, + StringBuilder entries); + + [LogMethod(1, LogLevel.Debug, "Process reporting healthy: {status}.")] + public static partial void Healthy( + ILogger logger, + HealthStatus status); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheck.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheck.cs new file mode 100644 index 0000000000..cfdf5d39d3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheck.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +internal sealed class ManualHealthCheck : IManualHealthCheck +{ + private static readonly object _lock = new(); + + private HealthCheckResult _result; + + public HealthCheckResult Result + { + get + { + lock (_lock) + { + return _result; + } + } + set + { + lock (_lock) + { + _result = value; + } + } + } + + private readonly IManualHealthCheckTracker _tracker; + + [SuppressMessage("Major Code Smell", "S3366:\"this\" should not be exposed from constructors", Justification = "It's OK, just registering into a list")] + public ManualHealthCheck(IManualHealthCheckTracker tracker) + { + Result = HealthCheckResult.Unhealthy("Initial state"); + + _tracker = tracker; + _tracker.Register(this); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public void Dispose(bool _) + { + _tracker.Unregister(this); + } + + [ExcludeFromCodeCoverage] + ~ManualHealthCheck() => Dispose(false); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheckService.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheckService.cs new file mode 100644 index 0000000000..24a08f4bf3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheckService.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +/// +/// Lets you manually modify the healthiness of your application. This health check will only report healthy on all registered instances of are healthy. +/// +/// +/// This health check should be used when you want to have the flexibility to claim your service as unhealthy. +/// +internal sealed class ManualHealthCheckService : IHealthCheck +{ + private readonly IManualHealthCheckTracker _tracker; + + public ManualHealthCheckService(IManualHealthCheckTracker tracker) + { + _tracker = tracker; + } + + /// + /// Runs the health check, returning the status of the component being checked. + /// + /// + /// This method is called from + /// with period and other settings defined in . + /// + /// A context object associated with the current execution. + /// Not used in the current implementation. + /// + /// A that completes when the health check has finished, + /// yielding the status of the component being checked. + /// + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) => Task.FromResult(_tracker.GetHealthCheckResult()); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheckTracker.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheckTracker.cs new file mode 100644 index 0000000000..63e653c267 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/ManualHealthCheckTracker.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Text; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +internal sealed class ManualHealthCheckTracker : IManualHealthCheckTracker +{ + private static readonly HealthCheckResult _healthy = HealthCheckResult.Healthy(); + + private readonly ConcurrentDictionary _checks = new(); + + /// + public void Register(IManualHealthCheck check) + { + _ = _checks.AddOrUpdate(check, true, (_, _) => true); + } + + public void Unregister(IManualHealthCheck checkToRemove) + { + _ = _checks.TryRemove(checkToRemove, out _); + } + + /// + public HealthCheckResult GetHealthCheckResult() + { + // Construct string showing all reasons for unhealthy manual health checks + StringBuilder? stringBuilder = null; + + try + { + var worstStatus = HealthStatus.Healthy; + foreach (var checkPair in _checks) + { + var check = checkPair.Key.Result; + if (check.Status != HealthStatus.Healthy) + { + stringBuilder = (stringBuilder == null) ? PoolFactory.SharedStringBuilderPool.Get() : stringBuilder.Append(", "); + _ = stringBuilder.Append(check.Description); + if (worstStatus > check.Status) + { + worstStatus = check.Status; + } + } + } + + if (stringBuilder == null) + { + return _healthy; + } + + return new HealthCheckResult(worstStatus, stringBuilder.ToString()); + } + finally + { + if (stringBuilder != null) + { + PoolFactory.SharedStringBuilderPool.Return(stringBuilder); + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Metric.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Metric.cs new file mode 100644 index 0000000000..7e0ddbd1d5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Metric.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Metrics; +using System.Globalization; +using Microsoft.Extensions.EnumStrings; +using Microsoft.Extensions.Telemetry.Metering; + +[assembly: EnumStrings(typeof(Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus))] + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +internal static partial class Metric +{ + [Counter("healthy", "status", Name = @"R9\\HealthCheck\\Report")] + public static partial HealthCheckReportCounter CreateHealthCheckReportCounter(Meter meter); + + [Counter("name", "status", Name = @"R9\\HealthCheck\\UnhealthyHealthCheck")] + public static partial UnhealthyHealthCheckCounter CreateUnhealthyHealthCheckCounter(Meter meter); + + public static void RecordMetric(this HealthCheckReportCounter counterMetric, bool isHealthy, HealthStatus status) + => counterMetric.Add(1, isHealthy.ToString(CultureInfo.InvariantCulture), status.ToInvariantString()); + + public static void RecordMetric(this UnhealthyHealthCheckCounter counterMetric, string name, HealthStatus status) + => counterMetric.Add(1, name, status.ToInvariantString()); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Microsoft.Extensions.Diagnostics.HealthChecks.Core.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Microsoft.Extensions.Diagnostics.HealthChecks.Core.csproj new file mode 100644 index 0000000000..d02cff1f7d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/Microsoft.Extensions.Diagnostics.HealthChecks.Core.csproj @@ -0,0 +1,36 @@ + + + Microsoft.Extensions.Diagnostics.HealthChecks + Health check implementations. + Resilience + + + + true + true + true + true + true + + + + normal + 100 + 100 + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/TelemetryHealthCheckPublisher.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/TelemetryHealthCheckPublisher.cs new file mode 100644 index 0000000000..b5c1b3e781 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core/TelemetryHealthCheckPublisher.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +/// +/// Performs metering and logging telemetry on the HealthReport. +/// +internal sealed class TelemetryHealthCheckPublisher : IHealthCheckPublisher +{ + private readonly HealthCheckReportCounter _healthCheckReportCounter; + private readonly UnhealthyHealthCheckCounter _unhealthyHealthCheckCounter; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The meter. + /// The logger. + public TelemetryHealthCheckPublisher(Meter meter, ILogger logger) + { + _logger = logger; + _healthCheckReportCounter = Metric.CreateHealthCheckReportCounter(meter); + _unhealthyHealthCheckCounter = Metric.CreateUnhealthyHealthCheckCounter(meter); + } + + /// + /// Performs logging and metering before publishing the provided report. + /// + /// The . The result of executing a set of health checks. + /// Not used in the current implementation. + /// Task.CompletedTask. + public Task PublishAsync(HealthReport report, CancellationToken cancellationToken) + { + _ = Throw.IfNull(report); + + if (report.Status == HealthStatus.Healthy) + { + Log.Healthy(_logger, report.Status); + _healthCheckReportCounter.RecordMetric(true, report.Status); + } + else + { + var stringBuilder = PoolFactory.SharedStringBuilderPool.Get(); + + // Construct string showing list of all health entries status and description for logs + string separator = string.Empty; + foreach (var entry in report.Entries) + { + if (entry.Value.Status != HealthStatus.Healthy) + { + _unhealthyHealthCheckCounter.RecordMetric(entry.Key, entry.Value.Status); + } + + _ = stringBuilder.Append(separator) + .Append(entry.Key) + .Append(": {") + .Append("status: ") + .Append(entry.Value.Status.ToInvariantString()) + .Append(", description: ") + .Append(entry.Value.Description) + .Append('}'); + separator = ", "; + } + + Log.Unhealthy(_logger, report.Status, stringBuilder); + PoolFactory.SharedStringBuilderPool.Return(stringBuilder); + + _healthCheckReportCounter.RecordMetric(false, report.Status); + } + + return Task.CompletedTask; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.csproj new file mode 100644 index 0000000000..cacc432687 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.csproj @@ -0,0 +1,32 @@ + + + Microsoft.Extensions.Diagnostics.HealthChecks + Resource utilization health check. + Resilience + + + + true + true + true + + + + normal + 100 + 85 + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUsageThresholds.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUsageThresholds.cs new file mode 100644 index 0000000000..0aff091d8e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUsageThresholds.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +/// +/// Threshold settings for . +/// +[SuppressMessage("Major Code Smell", "S109:Magic numbers should not be used", Justification = "In place numbers make the ranges cleaner")] +public class ResourceUsageThresholds +{ + /// + /// Gets or sets the percentage threshold for the degraded state. + /// + /// + /// Default set to . + /// + [Range(0.0, 100.0)] + public double? DegradedUtilizationPercentage { get; set; } + + /// + /// Gets or sets the percentage threshold for the unhealthy state. + /// + /// + /// Default set to . + /// + [Range(0.0, 100.0)] + public double? UnhealthyUtilizationPercentage { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheck.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheck.cs new file mode 100644 index 0000000000..db5252874b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheck.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +/// +/// Represents a health check for in-container resources . +/// +internal sealed class ResourceUtilizationHealthCheck : IHealthCheck +{ + private static readonly Task _healthy = Task.FromResult(HealthCheckResult.Healthy()); + private readonly ResourceUtilizationHealthCheckOptions _options; + private readonly IResourceUtilizationTracker _dataTracker; + + /// + /// Initializes a new instance of the class. + /// + /// The options. + /// The datatracker. + public ResourceUtilizationHealthCheck(IOptions options, + IResourceUtilizationTracker dataTracker) + { + _options = Throw.IfMemberNull(options, options.Value); + _dataTracker = Throw.IfNull(dataTracker); + } + + /// + /// Runs the health check. + /// + /// A context object associated with the current execution. + /// A that can be used to cancel the health check. + /// A that completes when the health check has finished, yielding the status of the component being checked. + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + var utilization = _dataTracker.GetUtilization(_options.SamplingWindow); + if (utilization.CpuUsedPercentage > _options.CpuThresholds?.UnhealthyUtilizationPercentage) + { + return Task.FromResult(HealthCheckResult.Unhealthy("CPU usage is above the limit")); + } + + if (utilization.MemoryUsedPercentage > _options.MemoryThresholds?.UnhealthyUtilizationPercentage) + { + return Task.FromResult(HealthCheckResult.Unhealthy("Memory usage is above the limit")); + } + + if (utilization.CpuUsedPercentage > _options.CpuThresholds?.DegradedUtilizationPercentage) + { + return Task.FromResult(HealthCheckResult.Degraded("CPU usage is close to the limit")); + } + + if (utilization.MemoryUsedPercentage > _options.MemoryThresholds?.DegradedUtilizationPercentage) + { + return Task.FromResult(HealthCheckResult.Degraded("Memory usage is close to the limit")); + } + + return _healthy; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheckOptions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheckOptions.cs new file mode 100644 index 0000000000..5ff7650605 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheckOptions.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Shared.Data.Validation; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +/// +/// Options for the resource utilization health check. +/// +public class ResourceUtilizationHealthCheckOptions +{ + internal const int MinimumSamplingWindow = 100; + internal static readonly TimeSpan DefaultSamplingWindow = TimeSpan.FromSeconds(5); + + /// + /// Gets or sets thresholds for CPU utilization. + /// + /// + /// The thresholds are periodically compared against the utilization samples provided by + /// the registered . + /// + [ValidateObjectMembers] + public ResourceUsageThresholds CpuThresholds { get; set; } = new ResourceUsageThresholds(); + + /// + /// Gets or sets thresholds for memory utilization. + /// + /// + /// The thresholds are periodically compared against the utilization samples provided by + /// the registered . + /// + [ValidateObjectMembers] + public ResourceUsageThresholds MemoryThresholds { get; set; } = new ResourceUsageThresholds(); + + /// + /// Gets or sets the time window for used for calculating CPU and memory utilization averages. + /// + /// + /// Default set to 5 seconds. + /// + [TimeSpan(MinimumSamplingWindow, int.MaxValue)] + public TimeSpan SamplingWindow { get; set; } = DefaultSamplingWindow; +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheckOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheckOptionsValidator.cs new file mode 100644 index 0000000000..5ed6da4d50 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthCheckOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +[OptionsValidator] +internal sealed partial class ResourceUtilizationHealthCheckOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthChecksExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthChecksExtensions.cs new file mode 100644 index 0000000000..93729bfbc9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization/ResourceUtilizationHealthChecksExtensions.cs @@ -0,0 +1,138 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks; + +/// +/// Controls resource utilization health check features. +/// +public static class ResourceUtilizationHealthChecksExtensions +{ + private const string HealthCheckName = "container resources"; + + /// + /// Registers a health check provider that monitors resource utilization to assess the application's health. + /// + /// The builder to add the provider to. + /// The value of . + /// If is . + public static IHealthChecksBuilder AddResourceUtilizationHealthCheck(this IHealthChecksBuilder builder) + { + _ = Throw.IfNull(builder); + + _ = builder.Services.AddValidatedOptions(); + return builder.AddCheck(HealthCheckName); + } + + /// + /// Registers a health check provider that monitors resource utilization to assess the application's health. + /// + /// The builder to add the provider to. + /// A list of tags that can be used to filter health checks. + /// The value of . + /// If is . + public static IHealthChecksBuilder AddResourceUtilizationHealthCheck(this IHealthChecksBuilder builder, IEnumerable tags) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(tags); + + _ = builder.Services.AddValidatedOptions(); + return builder.AddCheck(HealthCheckName, tags: tags); + } + + /// + /// Registers a health check provider that monitors resource utilization to assess the application's health. + /// + /// The builder to add the provider to. + /// Configuration for . + /// The value of . + /// If or are . + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(ResourceUtilizationHealthCheckOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed by [DynamicDependency]")] + public static IHealthChecksBuilder AddResourceUtilizationHealthCheck( + this IHealthChecksBuilder builder, + IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + + _ = builder.Services.Configure(section); + return AddResourceUtilizationHealthCheck(builder); + } + + /// + /// Registers a health check provider that monitors resource utilization to assess the application's health. + /// + /// The builder to add the provider to. + /// Configuration section holding an instance of . + /// A list of tags that can be used to filter health checks. + /// The value of . + /// If , or are . + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(ResourceUtilizationHealthCheckOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed by [DynamicDependency]")] + public static IHealthChecksBuilder AddResourceUtilizationHealthCheck( + this IHealthChecksBuilder builder, + IConfigurationSection section, + IEnumerable tags) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + _ = Throw.IfNull(tags); + + _ = builder.Services.Configure(section); + return AddResourceUtilizationHealthCheck(builder, tags); + } + + /// + /// Registers a health check provider that monitors resource utilization to assess the application's health. + /// + /// The builder to add the provider to. + /// Configuration callback. + /// The value of . + /// If or are . + public static IHealthChecksBuilder AddResourceUtilizationHealthCheck( + this IHealthChecksBuilder builder, + Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + _ = builder.Services.Configure(configure); + return AddResourceUtilizationHealthCheck(builder); + } + + /// + /// Registers a health check provider that monitors resource utilization to assess the application's health. + /// + /// The builder to add the provider to. + /// Configuration callback. + /// A list of tags that can be used to filter health checks. + /// The value of . + /// If , or are . + public static IHealthChecksBuilder AddResourceUtilizationHealthCheck( + this IHealthChecksBuilder builder, + Action configure, + IEnumerable tags) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + _ = Throw.IfNull(tags); + + _ = builder.Services.Configure(configure); + return AddResourceUtilizationHealthCheck(builder, tags); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceUtilizationPublisher.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceUtilizationPublisher.cs new file mode 100644 index 0000000000..445a63819f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceUtilizationPublisher.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +/// +/// An interface for utilization publisher. +/// +public interface IResourceUtilizationPublisher +{ + /// + /// This method is called to update subscribers when new utilization state has been computed. + /// + /// The utilization struct to be published. + /// A used to cancel the publish operation. + /// ValueTask because operation can finish synchronously. + ValueTask PublishAsync(Utilization utilization, CancellationToken cancellationToken); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceUtilizationTracker.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceUtilizationTracker.cs new file mode 100644 index 0000000000..7943579614 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceUtilizationTracker.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +/// +/// Provides the ability to sample the system for current resource utilization. +/// +public interface IResourceUtilizationTracker +{ + /// + /// Gets utilization for the specified time window. + /// + /// A representing the time window for which utilization is requested. + /// The utilization during the time window specified by . + /// + /// Thrown when is greater than the maximum window size configured while adding the service to the services collection. + /// + Utilization GetUtilization(TimeSpan window); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceUtilizationTrackerBuilder.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceUtilizationTrackerBuilder.cs new file mode 100644 index 0000000000..5cace7493b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/IResourceUtilizationTrackerBuilder.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +/// +/// Helps building resource monitoring infra. +/// +public interface IResourceUtilizationTrackerBuilder +{ + /// + /// Gets the service collection being manipulated by the builder. + /// + IServiceCollection Services { get; } + + /// + /// Adds implementation of the utilization data publisher. + /// + /// An implementation of that is used by the tracker to publish to 3rd parties. + /// Instance of for further configurations. + IResourceUtilizationTrackerBuilder AddPublisher() + where T : class, IResourceUtilizationPublisher; +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/Calculator.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/Calculator.cs new file mode 100644 index 0000000000..ed2ef5fe34 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/Calculator.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// A Utilization Calculator. +/// +internal static class Calculator +{ + private const double Hundred = 100.0; + + /// + /// Calculate utilization based on two successive samples. + /// + /// Sample one. + /// Sample two. + /// CPU and memory limits of the system. + /// A utilization score. + public static Utilization CalculateUtilization(in ResourceUtilizationSnapshot first, in ResourceUtilizationSnapshot second, in SystemResources systemResources) + { + // Compute the length of the interval between samples. + long runtimeTickDelta = second.TotalTimeSinceStart.Ticks - first.TotalTimeSinceStart.Ticks; + + // Compute the total number of ticks available on the machine during that interval + double totalSystemTicks = runtimeTickDelta * systemResources.GuaranteedCpuUnits; + + // Now, compute the amount of usage between the intervals + long oldUsageTicks = first.KernelTimeSinceStart.Ticks + first.UserTimeSinceStart.Ticks; + long newUsageTicks = second.KernelTimeSinceStart.Ticks + second.UserTimeSinceStart.Ticks; + long totalUsageTickDelta = newUsageTicks - oldUsageTicks; + + var utilization = Math.Max(0.0, (totalUsageTickDelta / totalSystemTicks) * Hundred); + var cpuUtilization = Math.Min(Hundred, utilization); + + return new Utilization(cpuUtilization, second.MemoryUsageInBytes, systemResources, second); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/CircularBuffer.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/CircularBuffer.cs new file mode 100644 index 0000000000..3227537c52 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/CircularBuffer.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +internal sealed class CircularBuffer +{ + private const int BufferSizeMultiplier = 2; + private readonly T[] _elements; + private readonly int _maxDistance; + private int _bufferCursor; + + public CircularBuffer(int size, T defaultElement) + { + _ = Throw.IfLessThan(size, 1); + + _elements = new T[size * BufferSizeMultiplier]; + + _maxDistance = size; + + for (var i = 0; i < _elements.Length; i++) + { + _elements[i] = defaultElement; + } + } + + public (T firstElement, T lastElement) GetFirstAndLastFromWindow(int distance) + { + if (distance > _maxDistance) + { + distance = _maxDistance; + } + + var lastElementCursor = _bufferCursor; + var firstElementCursor = lastElementCursor - distance + 1; + if (firstElementCursor < 0) + { + firstElementCursor += _elements.Length; + } + + return (_elements[firstElementCursor], _elements[lastElementCursor]); + } + + public void Add(T newElement) + { + var cursor = (_bufferCursor + 1) % _elements.Length; + _elements[cursor] = newElement; + _bufferCursor = cursor; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ISnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ISnapshotProvider.cs new file mode 100644 index 0000000000..d8e14c0554 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ISnapshotProvider.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// An interface to be implemented by a provider that represents an underlying system and gets resources data about it. +/// +internal interface ISnapshotProvider +{ + /// + /// Gets the static values of CPU and memory limitations defined by the system. + /// + SystemResources Resources { get; } + + /// + /// Get a snapshot of the resource utilization of the system. + /// + /// An appropriate sample. +#pragma warning disable S4049 // Properties should be preferred + ResourceUtilizationSnapshot GetSnapshot(); +#pragma warning restore S4049 // Properties should be preferred +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/Log.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/Log.cs new file mode 100644 index 0000000000..27e11a64cc --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/Log.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +[SuppressMessage("Usage", "CA1801:Review unused parameters", Justification = "Generators.")] +[SuppressMessage("Major Code Smell", "S109:Magic numbers should not be used", Justification = "Generators.")] +internal static partial class Log +{ + [LogMethod(1, LogLevel.Error, "Unable to gather utilization statistics.")] + public static partial void HandledGatherStatisticsException(ILogger logger, Exception e); + + [LogMethod(2, LogLevel.Error, "Publisher `{publisher}` was unable to publish utilization statistics.")] + public static partial void HandlePublishUtilizationException(ILogger logger, Exception e, string publisher); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationBuilder.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationBuilder.cs new file mode 100644 index 0000000000..b7702aaaf8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationBuilder.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +internal sealed class ResourceUtilizationBuilder : IResourceUtilizationTrackerBuilder +{ + public IServiceCollection Services { get; } + + public ResourceUtilizationBuilder(IServiceCollection services) + { + services.TryAddSingleton(); + services.TryAddEnumerable(ServiceDescriptor.Singleton(static sp => sp.GetRequiredService())); + services.TryAddSingleton(static sp => sp.GetRequiredService()); + + Services = services; + } + + public IResourceUtilizationTrackerBuilder AddPublisher() + where T : class, IResourceUtilizationPublisher + { + Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + return this; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationSnapshot.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationSnapshot.cs new file mode 100644 index 0000000000..ce005a282d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationSnapshot.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// A snapshot of CPU and memory usage taken periodically over time. +/// +[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Comparing instances is not an expected scenario")] +internal readonly struct ResourceUtilizationSnapshot +{ + /// + /// Gets the total CPU time that has elapsed since startup. + /// + public TimeSpan TotalTimeSinceStart { get; } + + /// + /// Gets the amount of kernel time that has elapsed since startup. + /// + public TimeSpan KernelTimeSinceStart { get; } + + /// + /// Gets the amount of user time that has elapsed since startup. + /// + public TimeSpan UserTimeSinceStart { get; } + + /// + /// Gets the memory usage within the system in bytes. + /// + public ulong MemoryUsageInBytes { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// The time at which the snapshot was taken. + /// The amount of kernel time that has elapsed since startup. + /// The amount of user time that has elapsed since startup. + /// The memory usage within the system in bytes. + public ResourceUtilizationSnapshot( + TimeSpan totalTimeSinceStart, + TimeSpan kernelTimeSinceStart, + TimeSpan userTimeSinceStart, + ulong memoryUsageInBytes) + { + _ = Throw.IfLessThan(memoryUsageInBytes, 0); + _ = Throw.IfLessThan(kernelTimeSinceStart.Ticks, 0); + _ = Throw.IfLessThan(userTimeSinceStart.Ticks, 0); + + TotalTimeSinceStart = totalTimeSinceStart; + KernelTimeSinceStart = kernelTimeSinceStart; + UserTimeSinceStart = userTimeSinceStart; + MemoryUsageInBytes = memoryUsageInBytes; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The time provider. + /// The amount of kernel time that has elapsed since startup. + /// The amount of user time that has elapsed since startup. + /// The memory usage within the system in bytes. + /// This is a internal constructor to be used in unit tests only. + internal ResourceUtilizationSnapshot( + TimeProvider timeProvider, + TimeSpan kernelTimeSinceStart, + TimeSpan userTimeSinceStart, + ulong memoryUsageInBytes) + { + _ = Throw.IfLessThan(memoryUsageInBytes, 0); + _ = Throw.IfLessThan(kernelTimeSinceStart.Ticks, 0); + _ = Throw.IfLessThan(userTimeSinceStart.Ticks, 0); + + TotalTimeSinceStart = TimeSpan.FromTicks(timeProvider.GetUtcNow().Ticks); + KernelTimeSinceStart = kernelTimeSinceStart; + UserTimeSinceStart = userTimeSinceStart; + MemoryUsageInBytes = memoryUsageInBytes; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationTrackerOptionsManualValidator.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationTrackerOptionsManualValidator.cs new file mode 100644 index 0000000000..3e47078943 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationTrackerOptionsManualValidator.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Validation = Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +internal sealed class ResourceUtilizationTrackerOptionsManualValidator : IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, ResourceUtilizationTrackerOptions options) + { + var builder = new ValidateOptionsResultBuilder(); + + if (options.CalculationPeriod > options.CollectionWindow) + { + builder.AddError( + $"Value must be <= to {nameof(options.CollectionWindow)} ({options.CollectionWindow}), but is {options.CalculationPeriod}.", + nameof(options.CalculationPeriod)); + } + + return builder.Build(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationTrackerOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationTrackerOptionsValidator.cs new file mode 100644 index 0000000000..9b13d9b642 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationTrackerOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +[OptionsValidator] +internal sealed partial class ResourceUtilizationTrackerOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationTrackerService.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationTrackerService.cs new file mode 100644 index 0000000000..27c998ac49 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Internal/ResourceUtilizationTrackerService.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +/// +/// The implementation of that computes average resource utilization over a configured period of time. +/// +/// +/// The class also acts as a hosted singleton, intended to be used to manage the +/// background process of periodically inspecting and monitoring the utilization +/// of an enclosing system. +/// +internal sealed class ResourceUtilizationTrackerService : BackgroundService, IResourceUtilizationTracker +{ + /// + /// The data source. + /// + private readonly ISnapshotProvider _provider; + + /// + /// The publishers to use with the data we are tracking. + /// + private readonly IResourceUtilizationPublisher[] _publishers; + + /// + /// Logger to be used in this class. + /// + private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; + + /// + /// Circular buffer for storing samples. + /// + private readonly CircularBuffer _snapshotsStore; + + private readonly TimeSpan _samplingInterval; + + private readonly TimeSpan _calculationPeriod; + + private readonly TimeSpan _collectionWindow; + + private readonly CancellationTokenSource _stoppingTokenSource = new(); + + public ResourceUtilizationTrackerService( + ISnapshotProvider provider, + ILogger logger, + IOptions options, + IEnumerable publishers) + : this(provider, logger, options, publishers, TimeProvider.System) + { + } + + internal ResourceUtilizationTrackerService( + ISnapshotProvider provider, + ILogger logger, + IOptions options, + IEnumerable publishers, + TimeProvider timeProvider) + { + _provider = provider; + _logger = logger; + _timeProvider = timeProvider; + var optionsValue = Throw.IfMemberNull(options, options.Value); + _calculationPeriod = optionsValue.CalculationPeriod; + _samplingInterval = optionsValue.SamplingInterval; + _collectionWindow = optionsValue.CollectionWindow; + + _publishers = publishers.ToArray(); + + var bufferSize = (int)(_collectionWindow.TotalMilliseconds / _samplingInterval.TotalMilliseconds); + + var firstSnapshot = _provider.GetSnapshot(); + + _snapshotsStore = new CircularBuffer(bufferSize + 1, firstSnapshot); + } + + /// + /// Dispose the tracker. + /// + public override void Dispose() + { + _stoppingTokenSource.Dispose(); + base.Dispose(); + } + + /// + public Utilization GetUtilization(TimeSpan window) + { + _ = Throw.IfLessThanOrEqual(window.Ticks, 0); + _ = Throw.IfGreaterThan(window.Ticks, _collectionWindow.Ticks); + + var samplesToRead = (int)(window.Ticks / _samplingInterval.Ticks) + 1; + var (firstElement, lastElement) = _snapshotsStore.GetFirstAndLastFromWindow(samplesToRead); + + return Calculator.CalculateUtilization(firstElement, lastElement, _provider.Resources); + } + + /// + public override async Task StopAsync(CancellationToken cancellationToken) + { + // Stop the execution. +#if NET8_0_OR_GREATER + await _stoppingTokenSource.CancelAsync().ConfigureAwait(false); +#else + _stoppingTokenSource.Cancel(); +#endif + await base.StopAsync(cancellationToken).ConfigureAwait(false); + } + + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Intentionally Consume All. Allow no escapes.")] + internal async Task PublishUtilizationAsync(CancellationToken cancellationToken) + { + var u = GetUtilization(_calculationPeriod); + foreach (var publisher in _publishers) + { + try + { + await publisher.PublishAsync(u, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + // By Design: Swallow the exception, as they're non-actionable in this code path. + // Prioritize app reliability over error visibility + Log.HandlePublishUtilizationException(_logger, e, publisher.GetType().FullName!); + } + } + } + + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Intentionally Consume All. Allow no escapes.")] + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _stoppingTokenSource.Token); + var linkedTokenSourceToken = linkedTokenSource.Token; + + while (!linkedTokenSourceToken.IsCancellationRequested) + { + await _timeProvider.Delay(_samplingInterval, linkedTokenSourceToken).ConfigureAwait(false); + + try + { + _snapshotsStore.Add(_provider.GetSnapshot()); + } + catch (Exception e) + { + // By Design: Swallow the exception, as they're non-actionable in this code path. + // Prioritize app reliability over error visibility + Log.HandledGatherStatisticsException(_logger, e); + } + + await PublishUtilizationAsync(linkedTokenSource.Token).ConfigureAwait(false); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/IFileSystem.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/IFileSystem.cs new file mode 100644 index 0000000000..d1899011fa --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/IFileSystem.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// A helper interface used to mock the IO operation in the tests. +/// +internal interface IFileSystem +{ + /// + /// Reads content from the file. + /// + /// + /// Chars written. + /// + int Read(FileInfo file, int length, Span destination); + + /// + /// Read all content from a file. + /// + void ReadAll(FileInfo file, BufferWriter destination); + + /// + /// Reads first line from the file. + /// + void ReadFirstLine(FileInfo file, BufferWriter destination); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/IOperatingSystem.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/IOperatingSystem.cs new file mode 100644 index 0000000000..cea9087707 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/IOperatingSystem.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// Mocking running OS to be able to test library on windows machine. +/// +internal interface IOperatingSystem +{ + /// + /// Gets a value indicating whether the program is running on Linux. + /// + bool IsLinux { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/IUserHz.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/IUserHz.cs new file mode 100644 index 0000000000..c8a5791115 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/IUserHz.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// Mock Linux OS call to run tests on windows. +/// +internal interface IUserHz +{ + /// + /// Gets value of Linux UserHz. + /// + long Value { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/IsOperatingSystem.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/IsOperatingSystem.cs new file mode 100644 index 0000000000..c768019602 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/IsOperatingSystem.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +internal sealed class IsOperatingSystem : IOperatingSystem +{ + public bool IsLinux => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/LinuxResourceUtilizationProviderOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/LinuxResourceUtilizationProviderOptionsValidator.cs new file mode 100644 index 0000000000..b8e0ebc445 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/LinuxResourceUtilizationProviderOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +[OptionsValidator] +internal sealed partial class LinuxCountersOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/LinuxUtilizationParser.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/LinuxUtilizationParser.cs new file mode 100644 index 0000000000..728efce5f3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/LinuxUtilizationParser.cs @@ -0,0 +1,398 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// Parses Linux files to retrieve resource utilization data. +/// +internal sealed class LinuxUtilizationParser +{ + /// + /// File contains the amount of CPU time (in microseconds) available to the group during each accounting period. + /// + private static readonly FileInfo _cpuCfsQuotaUs = new("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"); + + /// + /// File contains the length of the accounting period in microseconds. + /// + private static readonly FileInfo _cpuCfsPeriodUs = new("/sys/fs/cgroup/cpu/cpu.cfs_period_us"); + + /// + /// Stat file contains information about all CPUs and their time. + /// + /// + /// The file has format of whitespace separated values. Each value has its own meaning and unit. + /// To know which value we read, why and what it means refer to proc (5) man page (its POSIX). + /// + private static readonly FileInfo _procStat = new("/proc/stat"); + + /// + /// File that contains information about available memory. + /// + private static readonly FileInfo _memInfo = new("/proc/meminfo"); + + /// + /// List of available CPUs for host. + /// + private static readonly FileInfo _cpuSetCpus = new("/sys/fs/cgroup/cpuset/cpuset.cpus"); + + /// + /// Cgroup memory limit. + /// + private static readonly FileInfo _memoryLimitInBytes = new("/sys/fs/cgroup/memory/memory.limit_in_bytes"); + + /// + /// Cgroup memory stats. + /// + /// + /// Single line representing used memory by cgroup in bytes. + /// + private static readonly FileInfo _memoryUsageInBytes = new("/sys/fs/cgroup/memory/memory.usage_in_bytes"); + + /// + /// Cgroup memory stats. + /// + /// + /// This file contains the details about memory usage. + /// The format is (type of memory spent) (value) (unit of measure). + /// + private static readonly FileInfo _memoryStat = new("/sys/fs/cgroup/memory/memory.stat"); + + /// + /// File containing usage in nanoseconds. + /// + /// + /// This value refers to the container/cgroup utilization. + /// The format is single line with one number value. + /// + private static readonly FileInfo _cpuacctUsage = new("/sys/fs/cgroup/cpuacct/cpuacct.usage"); + + private readonly IFileSystem _fileSystem; + private readonly long _userHz; + private readonly ObjectPool> _buffers; + + public LinuxUtilizationParser(IFileSystem fileSystem, IUserHz userHz) + { + _fileSystem = fileSystem; + _userHz = userHz.Value; + _buffers = BufferWriterPool.CreateBufferWriterPool(maxCapacity: 64); + } + + public long GetCgroupCpuUsageInNanoseconds() + { + var buffer = _buffers.Get(); + _fileSystem.ReadAll(_cpuacctUsage, buffer); + + var usage = buffer.WrittenSpan; + + _ = GetNextNumber(usage, out var nanoseconds); + + if (nanoseconds == -1) + { + Throw.InvalidOperationException($"Could not get cpu usage from '{_cpuacctUsage}'. Expected positive number, but got '{new string(usage)}'."); + } + + _buffers.Return(buffer); + + return nanoseconds; + } + + public long GetHostCpuUsageInNanoseconds() + { + const string StartingTokens = "cpu "; + const int NumberOfColumnsRepresentingCpuUsage = 8; + const int NanosecondsInSecond = 1_000_000_000; + + var buffer = _buffers.Get(); + _fileSystem.ReadFirstLine(_procStat, buffer); + + var stat = buffer.WrittenSpan; + var total = 0L; + + if (!buffer.WrittenSpan.StartsWith(StartingTokens)) + { + Throw.InvalidOperationException($"Expected proc/stat to start with '{StartingTokens}' but it was '{new string(buffer.WrittenSpan)}'."); + } + + stat = stat.Slice(StartingTokens.Length, stat.Length - StartingTokens.Length); + + for (var i = 0; i < NumberOfColumnsRepresentingCpuUsage; i++) + { + var next = GetNextNumber(stat, out var number); + + if (number != -1) + { + total += number; + } + + if (next == -1) + { + Throw.InvalidOperationException( + $"'{_procStat}' should contain whitespace separated values according to POSIX. We've failed trying to get {i}th value. File content: '{new string(stat)}'."); + } + + stat = stat.Slice(next, stat.Length - next); + } + + _buffers.Return(buffer); + + return (long)(total / (double)_userHz * NanosecondsInSecond); + } + + /// + /// When CGroup limits are set, we can calculate number of cores based on the file settings. + /// It should be 99% of the cases when app is hosted in the container environment. + /// Otherwise, we assume that all host's CPUs are available, which we read from proc/stat file. + /// + public float GetCgroupLimitedCpus() + { + if (TryGetCpuUnitsFromCgroups(_fileSystem, out var cpus)) + { + return cpus; + } + + return GetHostCpuCount(); + } + + public ulong GetAvailableMemoryInBytes() + { + const long UnsetCgroupMemoryLimit = 9_223_372_036_854_771_712; + + var buffer = _buffers.Get(); + _fileSystem.ReadAll(_memoryLimitInBytes, buffer); + + var memoryBuffer = buffer.WrittenSpan; + _ = GetNextNumber(memoryBuffer, out var maybeMemory); + + if (maybeMemory == -1) + { + Throw.InvalidOperationException($"Could not parse '{_memoryLimitInBytes}' content. Expected to find available memory in bytes but got '{new string(memoryBuffer)}' instead."); + } + + _buffers.Return(buffer); + + if (maybeMemory == UnsetCgroupMemoryLimit) + { + return GetHostAvailableMemory(); + } + + return (ulong)maybeMemory; + } + + public ulong GetMemoryUsageInBytes() + { + const string TotalInactiveFile = "total_inactive_file"; + + var buffer = _buffers.Get(); + _fileSystem.ReadAll(_memoryStat, buffer); + var memoryFile = buffer.WrittenSpan; + + var index = memoryFile.IndexOf(TotalInactiveFile.AsSpan()); + + if (index == -1) + { + Throw.InvalidOperationException($"Unable to find total_inactive_file from '{_memoryStat}'."); + } + + var inactiveMemorySlice = memoryFile.Slice(index + TotalInactiveFile.Length, memoryFile.Length - index - TotalInactiveFile.Length); + _ = GetNextNumber(inactiveMemorySlice, out var inactiveMemory); + + if (inactiveMemory == -1) + { + Throw.InvalidOperationException($"The value of total_inactive_file found in '{_memoryStat}' is not a positive number: '{new string(inactiveMemorySlice)}'."); + } + + buffer.Reset(); + + _fileSystem.ReadAll(_memoryUsageInBytes, buffer); + + var containerMemoryUsageFile = buffer.WrittenSpan; + var next = GetNextNumber(containerMemoryUsageFile, out var containerMemoryUsage); + + // this file format doesn't expect to contain anything after the number. + if (containerMemoryUsage == -1) + { + Throw.InvalidOperationException( + $"We tried to read '{_memoryUsageInBytes}', and we expected to get a positive number but instead it was: '{new string(containerMemoryUsageFile)}'."); + } + + _buffers.Return(buffer); + + var memoryUsage = containerMemoryUsage - inactiveMemory; + + if (memoryUsage < 0) + { + Throw.InvalidOperationException($"The total memory usage read from '{_memoryUsageInBytes}' is lesser than inactive memory usage read from '{_memoryStat}'."); + } + + return (ulong)memoryUsage; + } + + [SuppressMessage("Major Code Smell", "S109:Magic numbers should not be used", + Justification = "Shifting bits left by number n is multiplying the value by 2 to the power of n.")] + public ulong GetHostAvailableMemory() + { + // The value we are interested in starts with this. We just want to make sure it is true. + const string MemTotal = "MemTotal:"; + + var buffer = _buffers.Get(); + _fileSystem.ReadFirstLine(_memInfo, buffer); + var firstLine = buffer.WrittenSpan; + + if (!firstLine.StartsWith(MemTotal)) + { + Throw.InvalidOperationException($"Could not parse '{_memInfo}'. We expected first line of the file to start with '{MemTotal}' but it was '{new string(firstLine)}' instead."); + } + + var totalMemory = firstLine.Slice(MemTotal.Length, firstLine.Length - MemTotal.Length); + + var next = GetNextNumber(totalMemory, out var totalMemoryAvailable); + + if (totalMemoryAvailable == -1) + { + Throw.InvalidOperationException($"Could not parse '{_memInfo}'. We expected to get total memory usage on first line but we've got: '{new string(firstLine)}'."); + } + + if (next == -1 || totalMemory.Length - next < 2) + { + Throw.InvalidOperationException($"Could not parse '{_memInfo}'. We expected to get memory usage followed by the unit (kB, MB, GB) but found no unit: '{new string(firstLine)}'."); + } + + var unit = totalMemory.Slice(totalMemory.Length - 2, 2); + var memory = (ulong)totalMemoryAvailable; + + var u = unit switch + { + "kB" => memory << 10, + "MB" => memory << 20, + "GB" => memory << 30, + "TB" => memory << 40, + _ => throw new InvalidOperationException( + $"We tried to convert total memory usage value from '{_memInfo}' to bytes, but we've got a unit that we don't recognize: '{new string(unit)}'.") + }; + + _buffers.Return(buffer); + + return u; + } + + /// + /// The file format is the following: + /// 0-18 + /// So, it is array indexed number of cpus. + /// + public float GetHostCpuCount() + { + var buffer = _buffers.Get(); + _fileSystem.ReadFirstLine(_cpuSetCpus, buffer); + var stats = buffer.WrittenSpan; + + var start = stats.IndexOf("-", StringComparison.Ordinal); + + if (stats.IsEmpty || start == -1 || start == 0) + { + Throw.InvalidOperationException($"Could not parse '{_cpuSetCpus}'. Expected integer based range separated by dash (like 0-8) but got '{new string(stats)}'."); + } + + var first = stats.Slice(0, start); + var second = stats.Slice(start + 1, stats.Length - start - 1); + + _ = GetNextNumber(first, out var startCpu); + var next = GetNextNumber(second, out var endCpu); + + if (startCpu == -1 || endCpu == -1 || endCpu < startCpu || next != -1) + { + Throw.InvalidOperationException($"Could not parse '{_cpuSetCpus}'. Expected integer based range separated by dash (like 0-8) but got '{new string(stats)}'."); + } + + _buffers.Return(buffer); + + return endCpu - startCpu + 1; + } + + /// + /// The input must contain only number. If there is something more than whitespace before the number, it will return failure. + /// + [SuppressMessage("Major Code Smell", "S109:Magic numbers should not be used", + Justification = "We are adding another digit, so we need to multiply by ten.")] + private static int GetNextNumber(ReadOnlySpan buffer, out long number) + { + var numberStart = 0; + + while (numberStart < buffer.Length && char.IsWhiteSpace(buffer[numberStart])) + { + numberStart++; + } + + if (numberStart == buffer.Length || !char.IsDigit(buffer[numberStart])) + { + number = -1; + return -1; + } + + var numberEnd = numberStart; + number = 0; + + while (numberEnd < buffer.Length && char.IsDigit(buffer[numberEnd])) + { + var current = buffer[numberEnd] - '0'; + number *= 10; + number += current; + numberEnd++; + } + + return numberEnd < buffer.Length ? numberEnd : -1; + } + + private bool TryGetCpuUnitsFromCgroups(IFileSystem fileSystem, out float cpuUnits) + { + var buffer = _buffers.Get(); + fileSystem.ReadFirstLine(_cpuCfsQuotaUs, buffer); + + var quotaBuffer = buffer.WrittenSpan; + + if (quotaBuffer.IsEmpty || (quotaBuffer.Length == 2 && quotaBuffer[0] == '-' && quotaBuffer[1] == '1')) + { + cpuUnits = -1; + return false; + } + + var nextQuota = GetNextNumber(quotaBuffer, out var quota); + + if (quota == -1 || nextQuota != -1) + { + Throw.InvalidOperationException($"Could not parse '{_cpuCfsQuotaUs}'. Expected an integer but got: '{new string(quotaBuffer)}'."); + } + + buffer.Reset(); + + fileSystem.ReadFirstLine(_cpuCfsPeriodUs, buffer); + var periodBuffer = buffer.WrittenSpan; + + if (periodBuffer.IsEmpty || (periodBuffer.Length == 2 && periodBuffer[0] == '-' && periodBuffer[1] == '1')) + { + cpuUnits = -1; + return false; + } + + var nextPeriod = GetNextNumber(periodBuffer, out var period); + + if (period == -1 || nextPeriod != -1) + { + Throw.InvalidOperationException($"Could not parse '{_cpuCfsPeriodUs}'. Expected to get an integer but got: '{new string(periodBuffer)}'."); + } + + _buffers.Return(buffer); + + cpuUnits = (float)quota / period; + return true; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/LinuxUtilizationProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/LinuxUtilizationProvider.cs new file mode 100644 index 0000000000..1a091c1b9a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/LinuxUtilizationProvider.cs @@ -0,0 +1,158 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +internal sealed class LinuxUtilizationProvider : ISnapshotProvider +{ + private const float Hundred = 100.0f; + private readonly object _cpuLocker = new(); + private readonly object _memoryLocker = new(); + private readonly LinuxUtilizationParser _parser; + private readonly ulong _totalMemoryInBytes; + private readonly TimeSpan _cpuRefreshInterval; + private readonly TimeSpan _memoryRefreshInterval; + private readonly TimeProvider _timeProvider; + private readonly double _scale; + private readonly double _scaleForTrackerApi; + + private DateTimeOffset _refreshAfterCpu; + private DateTimeOffset _refreshAfterMemory; + + private double _cpuPercentage = double.NaN; + private double _memoryPercentage; + private double _previousCgroupCpuTime; + private double _previousHostCpuTime; + + public SystemResources Resources { get; } + + public LinuxUtilizationProvider(IOptions options, LinuxUtilizationParser parser, + Meter meter, IOperatingSystem os, TimeProvider? timeProvider = null) + { + _ = Throw.IfMemberNull(options, options?.Value); + + if (!os.IsLinux) + { + throw new NotSupportedException($"Method 'AddLinuxProvider' was used on '{Environment.OSVersion.Platform}' while it was expected to run on Linux."); + } + + _parser = parser; + _timeProvider = timeProvider ?? TimeProvider.System; + var now = _timeProvider.GetUtcNow(); + _cpuRefreshInterval = options.Value.CpuConsumptionRefreshInterval; + _memoryRefreshInterval = options.Value.MemoryConsumptionRefreshInterval; + _refreshAfterCpu = now; + _refreshAfterMemory = now; + _totalMemoryInBytes = _parser.GetAvailableMemoryInBytes(); + _previousHostCpuTime = _parser.GetHostCpuUsageInNanoseconds(); + _previousCgroupCpuTime = _parser.GetCgroupCpuUsageInNanoseconds(); + + var hostMemory = _parser.GetHostAvailableMemory(); + var hostCpus = _parser.GetHostCpuCount(); + var availableCpus = _parser.GetCgroupLimitedCpus(); + + _scale = hostCpus * Hundred / availableCpus; + _scaleForTrackerApi = hostCpus / availableCpus; + + _ = meter.CreateObservableGauge(name: LinuxResourceUtilizationCounters.CpuConsumptionPercentage, observeValue: CpuPercentage); + _ = meter.CreateObservableGauge(name: LinuxResourceUtilizationCounters.MemoryConsumptionPercentage, observeValue: MemoryPercentage); + + Resources = new SystemResources(1, hostCpus, _totalMemoryInBytes, hostMemory); + } + + public double CpuPercentage() + { + var now = _timeProvider.GetUtcNow(); + bool needUpdate = false; + + lock (_cpuLocker) + { + if (now >= _refreshAfterCpu) + { + needUpdate = true; + } + } + + if (needUpdate) + { + var hostCpuTime = _parser.GetHostCpuUsageInNanoseconds(); + var cgroupCpuTime = _parser.GetCgroupCpuUsageInNanoseconds(); + + lock (_cpuLocker) + { + if (now >= _refreshAfterCpu) + { + var deltaHost = hostCpuTime - _previousHostCpuTime; + var deltaCgroup = cgroupCpuTime - _previousCgroupCpuTime; + + if (deltaHost > 0 && deltaCgroup > 0) + { + var percentage = Math.Min(Hundred, (deltaCgroup / deltaHost) * _scale); + + _cpuPercentage = percentage; + _refreshAfterCpu = now.Add(_cpuRefreshInterval); + _previousCgroupCpuTime = cgroupCpuTime; + _previousHostCpuTime = hostCpuTime; + } + } + } + } + + return _cpuPercentage; + } + + public double MemoryPercentage() + { + var now = _timeProvider.GetUtcNow(); + bool needUpdate = false; + + lock (_memoryLocker) + { + if (now >= _refreshAfterMemory) + { + needUpdate = true; + } + } + + if (needUpdate) + { + var memoryUsed = _parser.GetMemoryUsageInBytes(); + + lock (_memoryLocker) + { + if (now >= _refreshAfterMemory) + { + var memoryPercentage = Math.Min(Hundred, ((double)memoryUsed / _totalMemoryInBytes) * Hundred); + + _memoryPercentage = memoryPercentage; + _refreshAfterMemory = now.Add(_memoryRefreshInterval); + } + } + } + + return _memoryPercentage; + } + + /// + /// Not adding caching, to preserve original semantics of the code. + /// The snapshot provider is called in intervals configured by the tracker. + /// We multiply by scale to make hardcoded algorithm in tracker's calculator to produce right results. + /// + public ResourceUtilizationSnapshot GetSnapshot() + { + var hostTime = _parser.GetHostCpuUsageInNanoseconds(); + var cgroupTime = _parser.GetCgroupCpuUsageInNanoseconds(); + var memoryUsed = _parser.GetMemoryUsageInBytes(); + + return new ResourceUtilizationSnapshot( + totalTimeSinceStart: TimeSpan.FromTicks(hostTime / (long)Hundred), + kernelTimeSinceStart: TimeSpan.Zero, + userTimeSinceStart: TimeSpan.FromTicks((long)(cgroupTime / (long)Hundred * _scaleForTrackerApi)), + memoryUsageInBytes: memoryUsed); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/OSFileSystem.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/OSFileSystem.cs new file mode 100644 index 0000000000..3a8ece82fe --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/OSFileSystem.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +#pragma warning disable R9A017 // Use asynchronous operations instead of legacy thread blocking code + +/// +/// We are reading files from /proc and /cgroup. Those files are dynamically generated by kernel, when the access to them is requested. +/// Those files then, are stored entirely in RAM; it is called Virtual File System (VFS). The access to the files is done by syscall that is non blocking. +/// Thus, this API can synchronous without performance loss. +/// +internal sealed class OSFileSystem : IFileSystem +{ + public int Read(FileInfo file, int length, Span destination) + { + using var stream = file.OpenRead(); + using var rentedBuffer = new RentedSpan(length); + + var read = stream.Read(rentedBuffer.Span); + + return Encoding.ASCII.GetChars(rentedBuffer.Span.Slice(0, read), destination); + } + + public void ReadFirstLine(FileInfo file, BufferWriter destination) + => ReadUntilTerminatorOrEnd(file, destination, (byte)'\n'); + + public void ReadAll(FileInfo file, BufferWriter destination) + => ReadUntilTerminatorOrEnd(file, destination, null); + + [SkipLocalsInit] + private static void ReadUntilTerminatorOrEnd(FileInfo file, BufferWriter destination, byte? terminator) + { + const int MaxStackalloc = 256; + + using var stream = file.OpenRead(); + + Span buffer = stackalloc byte[MaxStackalloc]; + var read = stream.Read(buffer); + + while (read != 0) + { + var end = 0; + + for (end = 0; end < read; end++) + { + if (buffer[end] == terminator) + { + _ = Encoding.ASCII.GetChars(buffer.Slice(0, end), destination.GetSpan(end)); + destination.Advance(end); + + return; + } + } + + _ = Encoding.ASCII.GetChars(buffer.Slice(0, end), destination.GetSpan(end)); + destination.Advance(end); + read = stream.Read(buffer); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/UserHz.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/UserHz.cs new file mode 100644 index 0000000000..f5ae3b4718 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/Internal/UserHz.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +#if NET7_0_OR_GREATER +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Nah")] +[ExcludeFromCodeCoverage(Justification = "This is just a call to a native method. We access it by the interface at runtime. It is not testable.")] +internal static partial class NativeMethods +{ + [LibraryImport("libc", SetLastError = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + public static partial long sysconf(int name); +} +#endif + +[ExcludeFromCodeCoverage] // Justification: This is just a call to a native method. We access it by the interface at runtime. It is not testable. +internal sealed class UserHz : IUserHz +{ + private const int SystemConfigurationUserHz = 2; + + public long Value { get; } = NativeMethods.sysconf(SystemConfigurationUserHz); + +#if !NET7_0_OR_GREATER + private static class NativeMethods + { + [DllImport("libc", SetLastError = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + public static extern long sysconf(int name); + } +#endif +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxResourceUtilizationCounters.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxResourceUtilizationCounters.cs new file mode 100644 index 0000000000..65e5c5a5ab --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxResourceUtilizationCounters.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +/// +/// The names of instruments published by this package. +/// +/// +/// . +/// +public static class LinuxResourceUtilizationCounters +{ + /// + /// Gets CPU consumption by running application in percentages. + /// + /// + /// The type of an instrument is (long). + /// + public static string CpuConsumptionPercentage { get; } = "cpu_consumption_percentage"; + + /// + /// Gets memory consumption by running application in percentages. + /// + /// + /// The type of an instrument is (long). + /// + public static string MemoryConsumptionPercentage { get; } = "memory_consumption_percentage"; +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxResourceUtilizationProviderOptions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxResourceUtilizationProviderOptions.cs new file mode 100644 index 0000000000..a497095e65 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxResourceUtilizationProviderOptions.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Data.Validation; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +/// +/// Options for Linux resource utilization provider. +/// +public class LinuxResourceUtilizationProviderOptions +{ + internal const int MinimumCachingInterval = 100; + internal const int MaximumCachingInterval = 900000; // 15 minutes. + internal static readonly TimeSpan DefaultRefreshInterval = TimeSpan.FromSeconds(5); + + /// + /// Gets or sets the default interval used for refreshing values reported by . + /// + /// + /// This is the time interval for a metric value to fetch resource utilization data from the operating system. + /// Default set to 5 seconds. + /// + [TimeSpan(MinimumCachingInterval, MaximumCachingInterval)] + public TimeSpan CpuConsumptionRefreshInterval { get; set; } = DefaultRefreshInterval; + + /// + /// Gets or sets the default interval used for refreshing values reported by . + /// + /// + /// This is the time interval for a metric value to fetch resource utilization data from the operating system. + /// Default set to 5 seconds. + /// + [TimeSpan(MinimumCachingInterval, MaximumCachingInterval)] + public TimeSpan MemoryConsumptionRefreshInterval { get; set; } = DefaultRefreshInterval; +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationExtensions.cs new file mode 100644 index 0000000000..45a377aa48 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationExtensions.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +/// +/// Extensions for adding Linux Resource Utilization providers. +/// +public static class LinuxUtilizationExtensions +{ + /// + /// An extension method to configure and add the Linux utilization provider to services collection. + /// + /// The tracker builder instance used to add the provider. + /// Returns the input tracker builder for call chaining. + /// is . + public static IResourceUtilizationTrackerBuilder AddLinuxProvider(this IResourceUtilizationTrackerBuilder builder) + { + _ = Throw.IfNull(builder); + + builder.Services + .RegisterMetering() + .AddValidatedOptions() + .Services.TryAddActivatedSingleton(); + + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + + return builder; + } + + /// + /// An extension method to configure and add the Linux utilization provider to services collection. + /// + /// The builder. + /// The to use for configuring of . + /// Returns the builder. + /// is . + /// + /// . + /// + public static IResourceUtilizationTrackerBuilder AddLinuxProvider(this IResourceUtilizationTrackerBuilder builder, IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + + _ = builder.Services.AddOptions().Bind(section); + + return builder.AddLinuxProvider(); + } + + /// + /// An extension method to configure and add the Linux utilization provider to services collection. + /// + /// The builder. + /// The delegate for configuring of . + /// Returns the builder. + /// or is . + public static IResourceUtilizationTrackerBuilder AddLinuxProvider(this IResourceUtilizationTrackerBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + _ = builder.Services.AddOptions().Configure(configure); + + return builder.AddLinuxProvider(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj new file mode 100644 index 0000000000..23be7a51db --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj @@ -0,0 +1,49 @@ + + + Microsoft.Extensions.Diagnostics.ResourceMonitoring + Measures processor and memory usage. + Fundamentals + + + + true + true + true + true + true + true + true + + + + normal + 100 + 90 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Null/NullResourceUtilizationTrackerService.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Null/NullResourceUtilizationTrackerService.cs new file mode 100644 index 0000000000..dbc846aafb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Null/NullResourceUtilizationTrackerService.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +internal sealed class NullResourceUtilizationTrackerService : IResourceUtilizationTracker +{ + private const double CpuUnits = 1.0; + private static readonly Utilization _utilization = new(0.0, 0U, new(CpuUnits, CpuUnits, long.MaxValue, long.MaxValue)); + + public Utilization GetUtilization(TimeSpan aggregationPeriod) => _utilization; +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Null/NullSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Null/NullSnapshotProvider.cs new file mode 100644 index 0000000000..20af395ad9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Null/NullSnapshotProvider.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +internal sealed class NullSnapshotProvider : ISnapshotProvider +{ + private const double AvailableCpuUnits = 1.0; + private const ulong MemoryTotalInBytes = long.MaxValue; + private const ulong MemoryUsageInBytes = 0UL; + + private readonly TimeProvider _timeProvider; + + public NullSnapshotProvider() + : this(TimeProvider.System) + { + } + + internal NullSnapshotProvider(TimeProvider timeProvider) + { + _timeProvider = timeProvider; + } + + public SystemResources Resources { get; } = new(AvailableCpuUnits, AvailableCpuUnits, MemoryTotalInBytes, MemoryTotalInBytes); + + public ResourceUtilizationSnapshot GetSnapshot() + => new(TimeSpan.FromTicks(_timeProvider.GetUtcNow().Ticks), TimeSpan.Zero, TimeSpan.Zero, MemoryUsageInBytes); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringExtensions.cs new file mode 100644 index 0000000000..4d66cf3e82 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceMonitoringExtensions.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +/// +/// Lets you configure and register resource utilization components. +/// +public static class ResourceMonitoringExtensions +{ + /// + /// Configures and adds an implementation to your service collection. + /// + /// The dependency injection container to add the tracker to. + /// Delegate to configure . + /// The value of . + /// If either or are . + public static IServiceCollection AddResourceUtilization( + this IServiceCollection services, + Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + return services.AddResourceUtilizationInternal(configure); + } + + /// + /// Configures and adds an implementation to your host. + /// + /// The host builder to bind to. + /// Delegate to configure . + /// The value of . + /// If either or are . + public static IHostBuilder ConfigureResourceUtilization( + this IHostBuilder builder, + Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + return builder + .ConfigureServices((_, services) => + services.AddResourceUtilizationInternal(configure)); + } + + /// + /// Configures the resource utilization tracker. + /// + /// The builder instance used to configure the tracker. + /// Delegate to configure . + /// The value of . + /// If either or are . + public static IResourceUtilizationTrackerBuilder ConfigureTracker( + this IResourceUtilizationTrackerBuilder builder, + Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + return builder.ConfigureTrackerInternal(optionsBuilder => optionsBuilder.Configure(configure)); + } + + /// + /// Configures the resource utilization tracker. + /// + /// The builder instance used to configure the tracker. + /// The to use for configuring . + /// The value of . + /// If either or are . + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed with [DynamicDependency]")] + public static IResourceUtilizationTrackerBuilder ConfigureTracker( + this IResourceUtilizationTrackerBuilder builder, + IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + + return builder.ConfigureTrackerInternal(configure: optionsBuilder => optionsBuilder.Bind(section)); + } + + /// + /// Adds Null Resource Utilization services to the . + /// + /// The DI container to bind to. + /// Returns the input container. + /// When is . + public static IServiceCollection AddNullResourceUtilization(this IServiceCollection services) + { + services = Throw.IfNull(services); + + return services.AddSingleton(); + } + + /// + /// Adds a platform independent and non-operational to the service collection. + /// + /// The builder instance used to configure the tracker. + /// The value of . + /// This extension method will add a non-operational provider that generates fixed CPU and Memory information. Don't use this in + /// production, but you can use it in development environment when you're uncertain about the underlying platform and don't need real data + /// to be generated. + /// If is . + public static IResourceUtilizationTrackerBuilder AddNullResourceUtilizationProvider(this IResourceUtilizationTrackerBuilder builder) + { + _ = Throw.IfNull(builder); + + _ = builder.Services.AddSingleton(); + return builder; + } + + private static IServiceCollection AddResourceUtilizationInternal( + this IServiceCollection services, + Action configure) + { + configure.Invoke(new ResourceUtilizationBuilder(services)); + return services; + } + + private static IResourceUtilizationTrackerBuilder ConfigureTrackerInternal( + this IResourceUtilizationTrackerBuilder builder, + Action> configure) + { + var optionsBuilder = builder + .Services.AddValidatedOptions() + .Services.AddValidatedOptions(); + + configure.Invoke(optionsBuilder); + return builder; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceUtilizationTrackerOptions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceUtilizationTrackerOptions.cs new file mode 100644 index 0000000000..d6dc0c5bfc --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/ResourceUtilizationTrackerOptions.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Data.Validation; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +/// +/// Options for . +/// +public class ResourceUtilizationTrackerOptions +{ + /// + /// Internal for testing. + /// + internal const int MinimumSamplingWindow = 100; + internal const int MaximumSamplingWindow = 900000; // 15 minutes. + internal const int MinimumSamplingPeriod = 1; + internal const int MaximumSamplingPeriod = 900000; // 15 minutes. + internal static readonly TimeSpan DefaultCollectionWindow = TimeSpan.FromSeconds(5); + internal static readonly TimeSpan DefaultSamplingInterval = TimeSpan.FromSeconds(1); + + /// + /// Gets or sets the maximum time window of which utilization can be requested. + /// + /// + /// Default set to 5 seconds. + /// + [TimeSpan(MinimumSamplingWindow, MaximumSamplingWindow)] + public TimeSpan CollectionWindow { get; set; } = DefaultCollectionWindow; + + /// + /// Gets or sets the interval at which a new sample is captured. + /// + /// + /// Default set to 1 second. + /// + [TimeSpan(MinimumSamplingPeriod, MaximumSamplingPeriod)] + public TimeSpan SamplingInterval { get; set; } = DefaultSamplingInterval; + + /// + /// Gets or sets the default period used for utilization calculation. + /// + /// + /// Default set to 5 seconds. The value needs to be less than or equal to the . + /// Most importantly, this period is used to calculate instances pushed to publishers. + /// + [Experimental] + [TimeSpan(MinimumSamplingWindow, MaximumSamplingWindow)] + public TimeSpan CalculationPeriod { get; set; } = DefaultCollectionWindow; +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs new file mode 100644 index 0000000000..7a6d67b4ae --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/SystemResources.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +/// +/// CPU and memory limits defined by the underlying system. +/// +#pragma warning disable CA1815 // Override equals and operator equals on value types +public readonly struct SystemResources +#pragma warning restore CA1815 // Override equals and operator equals on value types +{ + /// + /// Gets the CPU units available in the system. + /// + /// + /// This value corresponds to the number of the guaranteed CPUs as described by Kubernetes CPU request parameter, each 1000 CPU units + /// represent 1 CPU or 1 Core. For example, if the POD is configured with 1500m units as the CPU request, this property will be assigned + /// to 1.5 which means one and a half CPU will be dedicated for the POD. + /// + public double GuaranteedCpuUnits { get; } + + /// + /// Gets the maximum CPU units available in the system. + /// + /// + /// This value corresponds to the number of the maximum CPUs as described by Kubernetes CPU limit parameter, each 1000 CPU units + /// represent 1 CPU or 1 Core. For example, if the is configured with 1500m units as the CPU limit, this property will be assigned + /// to 1.5 which means one and a half CPU will be the maximum CPU available for the POD. + /// + public double MaximumCpuUnits { get; } + + /// + /// Gets the memory allocated to the system in bytes. + /// + /// + /// This value corresponds to the number of the guaranteed memory as configured by a Kubernetes memory request parameter. + /// + public ulong GuaranteedMemoryInBytes { get; } + + /// + /// Gets the maximum memory allocated to the system in bytes. + /// + /// + /// This value corresponds to the number of the maximum memory as configured by a Kubernetes memory limit parameter. + /// + public ulong MaximumMemoryInBytes { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// The CPU units available in the system. + /// The maximum CPU units available in the system. + /// The memory allocated to the system in bytes. + /// The maximum memory allocated to the system in bytes. + public SystemResources(double guaranteedCpuUnits, double maximumCpuUnits, ulong guaranteedMemoryInBytes, ulong maximumMemoryInBytes) + { + GuaranteedCpuUnits = Throw.IfLessThanOrEqual(guaranteedCpuUnits, 0.0); + MaximumCpuUnits = Throw.IfLessThanOrEqual(maximumCpuUnits, 0.0); + GuaranteedMemoryInBytes = Throw.IfLessThan(guaranteedMemoryInBytes, 1UL); + MaximumMemoryInBytes = Throw.IfLessThan(maximumMemoryInBytes, 1UL); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Utilization.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Utilization.cs new file mode 100644 index 0000000000..6798cc9203 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Utilization.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +/// +/// Captures resource usage at a given point in time. +/// +[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Comparing instances is not an expected scenario")] +public readonly struct Utilization +{ + private const double Hundred = 100.0; + + /// + /// Gets the CPU utilization percentage. + /// + /// + /// This percentage is calculated relative to the . + /// + public double CpuUsedPercentage { get; } + + /// + /// Gets the memory utilization percentage. + /// + /// + /// This percentage is calculated relative to the . + /// This is an instantaneous measure when the utilization was computed, NOT an average. + /// + public double MemoryUsedPercentage { get; } + + /// + /// Gets the total memory used. + /// + /// + /// This is an instantaneous measure when the utilization was computed, NOT an average. + /// + public ulong MemoryUsedInBytes { get; } + + /// + /// Gets the CPU and memory limits defined by the underlying system. + /// + public SystemResources SystemResources { get; } + + /// + /// Gets the latest snapshot of resource utilization. + /// + internal ResourceUtilizationSnapshot Snapshot { get; } = new(new TimeSpan(0), new TimeSpan(0), new TimeSpan(0), 0); + + /// + /// Initializes a new instance of the struct. + /// + /// CPU utilization. + /// Memory used in bytes (instantaneous). + /// CPU and memory limits. + public Utilization(double cpuUsedPercentage, ulong memoryUsedInBytes, SystemResources systemResources) + { + CpuUsedPercentage = Throw.IfLessThan(cpuUsedPercentage, 0.0); + MemoryUsedInBytes = Throw.IfLessThan(memoryUsedInBytes, 0); + SystemResources = systemResources; + MemoryUsedPercentage = Math.Min(Hundred, (double)MemoryUsedInBytes / SystemResources.GuaranteedMemoryInBytes * Hundred); + } + + /// + /// Initializes a new instance of the struct. + /// + /// CPU utilization. + /// Memory used in bytes (instantaneous). + /// CPU and memory limits. + /// Latest ResourceUtilizationSnapshot. + internal Utilization(double cpuUsedPercentage, ulong memoryUsedInBytes, SystemResources systemResources, ResourceUtilizationSnapshot snapShot) + : this(cpuUsedPercentage, memoryUsedInBytes, systemResources) + { + Snapshot = snapShot; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/CountersSetup.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/CountersSetup.cs new file mode 100644 index 0000000000..7b336504bb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/CountersSetup.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +/// +/// A static class for performance counters preparation. +/// +public static class CountersSetup +{ + /// + /// Setup Category function. + /// + [ExcludeFromCodeCoverage] +#pragma warning disable IDE0079 // Remove unnecessary suppression + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "Indeed, this whole class is for Windows only")] + [SuppressMessage("Major Code Smell", "S2589:Boolean expressions should not be gratuitous", Justification = "We allow to setup initial boolean value even if it may not be changed under if-block")] +#pragma warning restore IDE0079 // Remove unnecessary suppression + public static void PreparePerformanceCounters() + { + var exists = PerformanceCounterCategory.Exists(WindowsPerfCounterConstants.ContainerCounterCategoryName); + var counterTypeChanged = false; + if (exists) + { + var counterSet = PerformanceCounterCategory + .GetCategories() + .Where(cat => cat.CategoryName == WindowsPerfCounterConstants.ContainerCounterCategoryName) + .Select(cat => cat.GetInstanceNames().Length > 0 + ? cat.GetInstanceNames().Select(i => cat.GetCounters(i)).SelectMany(counter => counter) + : cat.GetCounters(string.Empty)).SelectMany(counter => counter) + .Where(counter => counter.CounterName == WindowsPerfCounterConstants.CpuUtilizationCounterName + && counter.CounterType == PerformanceCounterType.RawFraction); + + counterTypeChanged = !(counterSet == null || !counterSet.Any()); + + if (counterTypeChanged) + { + PerformanceCounterCategory.Delete(WindowsPerfCounterConstants.ContainerCounterCategoryName); + } + } + + if (!exists || counterTypeChanged) + { + var counterDataCollection = new CounterCreationDataCollection(); + + // Add the counter. + var cpuUtilization = new CounterCreationData + { + CounterType = PerformanceCounterType.Timer100Ns, + CounterName = WindowsPerfCounterConstants.CpuUtilizationCounterName + }; + _ = counterDataCollection.Add(cpuUtilization); + + var memUtilization = new CounterCreationData + { + CounterType = PerformanceCounterType.RawFraction, + CounterName = WindowsPerfCounterConstants.MemoryUtilizationCounterName + }; + _ = counterDataCollection.Add(memUtilization); + + var memLimit = new CounterCreationData + { + CounterType = PerformanceCounterType.RawBase, + CounterName = WindowsPerfCounterConstants.MemoryLimitCounterName + }; + _ = counterDataCollection.Add(memLimit); + + // Create the category. + _ = PerformanceCounterCategory.Create(WindowsPerfCounterConstants.ContainerCounterCategoryName, + WindowsPerfCounterConstants.ContainerCounterCategoryName, + PerformanceCounterCategoryType.MultiInstance, counterDataCollection); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/IJobHandle.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/IJobHandle.cs new file mode 100644 index 0000000000..0cbaad5960 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/IJobHandle.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using static Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal.JobObjectInfo; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// An interface to enable the mocking of job information retrieval. +/// +internal interface IJobHandle : IDisposable +{ + /// + /// Get the current CPU rate information from the Job object. + /// + /// CPU rate information. + JOBOBJECT_CPU_RATE_CONTROL_INFORMATION GetJobCpuLimitInfo(); + + /// + /// Get the current CPU rate information from the Job object. + /// + /// CPU rate information. + JOBOBJECT_BASIC_ACCOUNTING_INFORMATION GetBasicAccountingInfo(); + + /// + /// Get the extended limit information from the Job object. + /// + /// Extended limit information. + JOBOBJECT_EXTENDED_LIMIT_INFORMATION GetExtendedLimitInfo(); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/IMemoryInfo.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/IMemoryInfo.cs new file mode 100644 index 0000000000..0cffc5ac90 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/IMemoryInfo.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// An interface to enable the mocking of memory information retrieval. +/// +internal interface IMemoryInfo +{ + /// + /// Get the memory status of the host. + /// + /// Memory status information. + MEMORYSTATUSEX GetMemoryStatus(); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/IProcessInfo.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/IProcessInfo.cs new file mode 100644 index 0000000000..486420d8a4 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/IProcessInfo.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using static Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal.ProcessInfo; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// An interface to enable the mocking of process information retrieval. +/// +internal interface IProcessInfo +{ + /// + /// Retrieve the current application memory information. + /// + /// An appropriate memory data structure. + APP_MEMORY_INFORMATION GetCurrentAppMemoryInfo(); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/ISystemInfo.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/ISystemInfo.cs new file mode 100644 index 0000000000..ba1fd87fa8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/ISystemInfo.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// An interface to enable the mocking of system information retrieval. +/// +internal interface ISystemInfo +{ + /// + /// Get the system info. + /// + /// System information structure. + SYSTEM_INFO GetSystemInfo(); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/JobHandleWrapper.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/JobHandleWrapper.cs new file mode 100644 index 0000000000..ec31e55903 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/JobHandleWrapper.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using static Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal.JobObjectInfo; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// Wrapper class for the class. +/// +[ExcludeFromCodeCoverage] +internal sealed class JobHandleWrapper : IJobHandle +{ + private readonly JobObjectInfo.SafeJobHandle _winJobHandle; + + public JobHandleWrapper() + { + _winJobHandle = new JobObjectInfo.SafeJobHandle(); + } + + public void Dispose() + { + _winJobHandle.Dispose(); + } + + public JOBOBJECT_CPU_RATE_CONTROL_INFORMATION GetJobCpuLimitInfo() + { + return _winJobHandle.GetJobCpuLimitInfo(); + } + + public JOBOBJECT_BASIC_ACCOUNTING_INFORMATION GetBasicAccountingInfo() + { + return _winJobHandle.GetBasicAccountingInfo(); + } + + public JOBOBJECT_EXTENDED_LIMIT_INFORMATION GetExtendedLimitInfo() + { + return _winJobHandle.GetExtendedLimitInfo(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/JobObjectInfo.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/JobObjectInfo.cs new file mode 100644 index 0000000000..c001d091d0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/JobObjectInfo.cs @@ -0,0 +1,400 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +#if NETFRAMEWORK +using System.Runtime.ConstrainedExecution; +#endif +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// JobObject class. +/// +/// +/// This will not be covered by UTs, as those classes have insufficient and inconsistent privileges, +/// depending on runtime environment. +/// +[ExcludeFromCodeCoverage] +internal static class JobObjectInfo +{ + /// + /// Job object info class. + /// + [SuppressMessage("Design", "CA1027:Mark enums with FlagsAttribute", Justification = "Analyzer is confused")] + public enum JOBOBJECTINFOCLASS + { + JobObjectBasicAccountingInformation = 1, + JobObjectExtendedLimitInformation = 9, + JobObjectCpuRateControlInformation = 15, + } + + /// + /// Job object CPU rate control limit flags. + /// + [Flags] + public enum JobCpuRateControlLimit : uint + { + CpuRateControlEnable = 1, + CpuRateControlHardCap = 4, + } + + /// + /// I/O counters which are part of the JOBOBJECT_EXTENDED_LIMIT_INFORMATION structure. + /// + [StructLayout(LayoutKind.Sequential)] + public struct IO_COUNTERS + { + /// ReadOperationCount field. + public ulong ReadOperationCount; + + /// WriteOperationCount field. + public ulong WriteOperationCount; + + /// OtherOperationCount field. + public ulong OtherOperationCount; + + /// ReadTransferCount field. + public ulong ReadTransferCount; + + /// WriteTransferCount field. + public ulong WriteTransferCount; + + /// OtherTransferCount field. + public ulong OtherTransferCount; + } + + /// + /// The job object basic limit information structure. + /// + [StructLayout(LayoutKind.Sequential)] + public struct JOBOBJECT_BASIC_LIMIT_INFORMATION + { + /// PerProcessUserTimeLimit field. + public long PerProcessUserTimeLimit; + + /// PerJobUserTimeLimit field. + public long PerJobUserTimeLimit; + + /// LimitFlags field. + public uint LimitFlags; + + /// MinimumWorkingSetSize field. + public UIntPtr MinimumWorkingSetSize; + + /// MaximumWorkingSetSize field. + public UIntPtr MaximumWorkingSetSize; + + /// ActiveProcessLimit field. + public uint ActiveProcessLimit; + + /// Affinity field. + public UIntPtr Affinity; + + /// PriorityClass field. + public uint PriorityClass; + + /// SchedulingClass field. + public uint SchedulingClass; + } + + /// + /// The job object extended limit information structure. + /// + [StructLayout(LayoutKind.Sequential)] + public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + /// BasicLimitInformation field. + public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; + + /// IoInfo field. + public IO_COUNTERS IoInfo; + + /// ProcessMemoryLimit field. + public UIntPtr ProcessMemoryLimit; + + /// JobMemoryLimit field. + public UIntPtr JobMemoryLimit; + + /// PeakProcessMemoryUsed field. + public UIntPtr PeakProcessMemoryUsed; + + /// PeakJobMemoryUsed field. + public UIntPtr PeakJobMemoryUsed; + } + + /// + /// The job object CPU rate control information structure. + /// + [StructLayout(LayoutKind.Explicit)] + public struct JOBOBJECT_CPU_RATE_CONTROL_INFORMATION + { + /// ControlFlags field. + [FieldOffset(0)] + public uint ControlFlags; + + /// CpuRate field. + [FieldOffset(4)] + public uint CpuRate; + + /// Weight field. + [FieldOffset(4)] + public uint Weight; + } + + /// + /// Contains basic accounting information for a job object. + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct JOBOBJECT_BASIC_ACCOUNTING_INFORMATION + { + /// The total user time. + public long TotalUserTime; + + /// The total kernel time. + public long TotalKernelTime; + + /// The this period total user time. + public long ThisPeriodTotalUserTime; + + /// The this period total kernel time. + public long ThisPeriodTotalKernelTime; + + /// The total page fault count. + public int TotalPageFaultCount; + + /// The total processes. + public int TotalProcesses; + + /// The active processes. + public int ActiveProcesses; + + /// The total terminated processes. + public int TotalTerminatedProcesses; + } + + /// + /// Wrapper class for job handle. + /// + public sealed class SafeJobHandle : SafeHandleZeroOrMinusOneIsInvalid + { + /// + /// Validate that the process is inside a JobObject. + /// + /// if the process is not running in a job. + /// A boolean to determine if the process is inside a JobObject or not. + public static bool IsProcessInJob() + { + const uint DUPLICATE_SAME_ACCESS = 0x00000002; + + // Get a pseudo handle of the current process. + var processHandle = UnsafeNativeMethods.GetCurrentProcess(); + + // Using the pseudo handle get a copy of the current process handle. + _ = UnsafeNativeMethods.DuplicateHandle(processHandle, + processHandle, + processHandle, + out var realProcessHandle, + 0, + false, + DUPLICATE_SAME_ACCESS); + + using SafeJobHandle jobHandle = new SafeJobHandle(); + + // Check if the process is running inside a job. + _ = UnsafeNativeMethods.IsProcessInJob(realProcessHandle, jobHandle, out var processInJob); + + // Close the duplicated handle. + _ = UnsafeNativeMethods.CloseHandle(realProcessHandle); + + return processInJob; + } + + /// + /// Initializes a new instance of the class. + /// + public SafeJobHandle() + : base(true) + { + } + + /// + /// Get the current CPU rate information from the Job object. + /// + /// CPU rate information. + public JOBOBJECT_CPU_RATE_CONTROL_INFORMATION GetJobCpuLimitInfo() + { + unsafe + { + JOBOBJECT_CPU_RATE_CONTROL_INFORMATION limit = default; + void* buffer = &limit; + GetJobObjectInformation( + JOBOBJECTINFOCLASS.JobObjectCpuRateControlInformation, + buffer, + sizeof(JOBOBJECT_CPU_RATE_CONTROL_INFORMATION)); + + return limit; + } + } + + /// + /// Get the current CPU rate information from the Job object. + /// + /// CPU rate information. + public JOBOBJECT_BASIC_ACCOUNTING_INFORMATION GetBasicAccountingInfo() + { + unsafe + { + JOBOBJECT_BASIC_ACCOUNTING_INFORMATION limit = default; + void* buffer = &limit; + GetJobObjectInformation( + JOBOBJECTINFOCLASS.JobObjectBasicAccountingInformation, + buffer, + sizeof(JOBOBJECT_BASIC_ACCOUNTING_INFORMATION)); + + return limit; + } + } + + /// + /// Get the extended limit information from the Job object. + /// + /// Extended limit information. + public JOBOBJECT_EXTENDED_LIMIT_INFORMATION GetExtendedLimitInfo() + { + unsafe + { + JOBOBJECT_EXTENDED_LIMIT_INFORMATION limit = default; + void* buffer = &limit; + GetJobObjectInformation( + JOBOBJECTINFOCLASS.JobObjectExtendedLimitInformation, + buffer, + sizeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); + + return limit; + } + } + + /// + /// Release the encapsulated handle. + /// + /// True: released successfully, otherwise false. +#if NETFRAMEWORK + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] +#endif + protected override bool ReleaseHandle() + { + return UnsafeNativeMethods.CloseHandle(handle); + } + + protected override void Dispose(bool disposing) + { + _ = ReleaseHandle(); + base.Dispose(disposing); + } + + /// + /// Get the job object limit. + /// + /// Job object info class. + /// Buffer containing the limit. + /// Buffer size. + /// + /// An application cannot obtain a handle to the job object in which it is running unless it has the name of the job object. + /// However, an application can call the QueryInformationJobObject function with NULL to obtain information about the job object. + /// + private unsafe void GetJobObjectInformation(JOBOBJECTINFOCLASS infoClass, void* buffer, int size) + { + if (!UnsafeNativeMethods.QueryInformationJobObject( + this, + infoClass, + buffer, + size, + out _)) + { + Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); + } + } + } + + private static class UnsafeNativeMethods + { + /// + /// Retrieves a pseudo handle for the current process. + /// + /// Pseudo handle to the current process. + [DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + public static extern IntPtr GetCurrentProcess(); + + /// + /// Duplicates an object handle. + /// + /// A handle to the process with the handle to be duplicated. + /// The handle to be duplicated. + /// A handle to the process that is to receive the duplicated handle. + /// A pointer to a variable that receives the duplicate handle. + /// The access requested for the new handle. + /// A variable that indicates whether the handle is inheritable. + /// Optional actions. + /// Returns true if the function succeeds. + /// Used with to get real handle of the current process instead of the pseudo handle. + [DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + public static extern bool DuplicateHandle( + IntPtr hSourceProcessHandle, + IntPtr hSourceHandle, + IntPtr hTargetProcessHandle, + out IntPtr lpTargetHandle, + uint dwDesiredAccess, + bool bInheritHandle, + uint dwOptions); + + /// + /// Determines whether the process is running in the specified job. + /// + /// A handle to the process to be tested. + /// A handle to the job. + /// A pointer to a value that receives true if the process is running in the job, and false otherwise. + /// True f the function succeeds, and false otherwise. + [DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool IsProcessInJob( + IntPtr processHandle, + SafeJobHandle jobHandle, + out bool result); + + /// + /// OS import for CloseHandle. + /// + /// the object to close. + /// true if the handle was valid and closed. + [DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CloseHandle(IntPtr handle); + + /// + /// OS import for QueryInformationJobObject. + /// + /// The job handle. + /// The job object information class. + /// The information buffer. + /// The information length. + /// The data written. + /// True if the call succeeded; otherwise false. + [DllImport("kernel32.dll", SetLastError = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern unsafe bool QueryInformationJobObject( + SafeJobHandle job, + JOBOBJECTINFOCLASS jobObjectInfoClass, + void* jobObjectInfo, + int jobObjectInfoLength, + out int returnLength); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/MemoryInfo.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/MemoryInfo.cs new file mode 100644 index 0000000000..6d931ecce9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/MemoryInfo.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// Native memory interop methods. +/// +[ExcludeFromCodeCoverage] +internal sealed class MemoryInfo : IMemoryInfo +{ + internal MemoryInfo() + { + } + + /// + /// Get the memory status of the host. + /// + /// Memory status information. + public unsafe MEMORYSTATUSEX GetMemoryStatus() + { + MEMORYSTATUSEX info = default; + info.Length = (uint)sizeof(MEMORYSTATUSEX); + if (!SafeNativeMethods.GlobalMemoryStatusEx(ref info)) + { + Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); + } + + return info; + } + + private static class SafeNativeMethods + { + /// + /// GlobalMemoryStatusEx. + /// + /// Memory Status structure. + /// Success or failure. + [DllImport("kernel32.dll", SetLastError = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX memoryStatus); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/MemoryStatusEx.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/MemoryStatusEx.cs new file mode 100644 index 0000000000..01108c2b53 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/MemoryStatusEx.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// The Win32 MEMORYSTATUSEX structure. +/// +[StructLayout(LayoutKind.Sequential)] +internal struct MEMORYSTATUSEX +{ + public uint Length; // DWORD + public uint MemoryLoad; // DWORD + public ulong TotalPhys; // DWORDLONG + public ulong AvailPhys; // DWORDLONG + public ulong TotalPageFile; // DWORDLONG + public ulong AvailPageFile; // DWORDLONG + public ulong TotalVirtual; // DWORDLONG + public ulong AvailVirtual; // DWORDLONG + public ulong AvailExtendedVirtual; // DWORDLONG +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/ProcessInfo.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/ProcessInfo.cs new file mode 100644 index 0000000000..036a768e3b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/ProcessInfo.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// Process native methods class. +/// +/// This will not be covered by UTs, as those +/// classes have insufficient and inconsistent privileges, +/// depending on runtime environment. +[ExcludeFromCodeCoverage] +internal static class ProcessInfo +{ + private enum PROCESS_INFORMATION_CLASS + { + ProcessAppMemoryInfo = 2 + } + + /// + /// The APP_MEMORY_INFORMATION structure. + /// + [StructLayout(LayoutKind.Sequential)] + internal struct APP_MEMORY_INFORMATION + { + public ulong AvailableCommit; + public ulong PrivateCommitUsage; + public ulong PeakPrivateCommitUsage; + public ulong TotalCommitUsage; + } + + /// + /// Retrieve the current application memory information. + /// + /// An appropriate memory data structure. + public static APP_MEMORY_INFORMATION GetCurrentAppMemoryInfo() + { + unsafe + { + APP_MEMORY_INFORMATION info = default; + void* buffer = &info; + using var currentProcess = Process.GetCurrentProcess(); + NtGetProcessInformation( + currentProcess.Handle, + PROCESS_INFORMATION_CLASS.ProcessAppMemoryInfo, + buffer, + sizeof(APP_MEMORY_INFORMATION)); + + return info; + } + } + + /// + /// Get process information. + /// + /// The handle of the object to query. + /// Process info class. + /// Buffer containing the limit. + /// Buffer size. + private static unsafe void NtGetProcessInformation(IntPtr handle, PROCESS_INFORMATION_CLASS infoClass, void* buffer, int size) + { + if (!UnsafeNativeMethods.GetProcessInformation( + handle, + infoClass, + buffer, + size)) + { + Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); + } + } + + private static class UnsafeNativeMethods + { + [DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + [return: MarshalAs(UnmanagedType.Bool)] + public static unsafe extern bool GetProcessInformation( + IntPtr processHandle, + PROCESS_INFORMATION_CLASS processInformationClass, + void* processInformation, + int processInformationSize); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/ProcessInfoWrapper.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/ProcessInfoWrapper.cs new file mode 100644 index 0000000000..2557e77a9a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/ProcessInfoWrapper.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +internal sealed class ProcessInfoWrapper : IProcessInfo +{ + [ExcludeFromCodeCoverage] + public ProcessInfo.APP_MEMORY_INFORMATION GetCurrentAppMemoryInfo() + { + return ProcessInfo.GetCurrentAppMemoryInfo(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/SYSTEM_INFO.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/SYSTEM_INFO.cs new file mode 100644 index 0000000000..9c4589357f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/SYSTEM_INFO.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// SYSTEM_INFO struct needed for GetSystemInfo. +/// +[StructLayout(LayoutKind.Sequential)] +internal struct SYSTEM_INFO +{ + public ushort ProcessorArchitecture; + public ushort Reserved; + public uint PageSize; + public UIntPtr MinimumApplicationAddress; + public UIntPtr MaximumApplicationAddress; + public UIntPtr ActiveProcessorMask; + public uint NumberOfProcessors; + public uint ProcessorType; + public uint AllocationGranularity; + public ushort ProcessorLevel; + public ushort ProcessorRevision; +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/SystemInfo.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/SystemInfo.cs new file mode 100644 index 0000000000..c8a102aaeb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Interop/SystemInfo.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// Native system interop methods. +/// +internal sealed class SystemInfo : ISystemInfo +{ + /// + /// Get the system info. + /// + /// System information structure. + public SYSTEM_INFO GetSystemInfo() + { + SYSTEM_INFO info = default; + SafeNativeMethods.GetSystemInfo(ref info); + return info; + } + + private static class SafeNativeMethods + { + /// + /// Import of GetSystemInfo win32 function. + /// + /// SYSTEM_INFO struct to fill. + [DllImport("kernel32.dll")] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + public static extern void GetSystemInfo(ref SYSTEM_INFO s); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Log.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Log.cs new file mode 100644 index 0000000000..cb5ad630ff --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Log.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +#pragma warning disable S109 +#pragma warning disable IDE0060 // Remove unused parameters - Reason: used by source generator. + +internal static partial class Log +{ + [LogMethod(1, LogLevel.Error, "Windows performance counter `{counterName}` does not exist.")] + public static partial void CounterDoesNotExist(ILogger logger, string counterName); + + [LogMethod(2, LogLevel.Information, "Resource Utilization is running inside a Job Object. For more information about Job Objects see https://aka.ms/job-objects")] + public static partial void RunningInsideJobObject(ILogger logger); + + [LogMethod(3, LogLevel.Information, "Resource Utilization is running outside of Job Object. For more information about Job Objects see https://aka.ms/job-objects")] + public static partial void RunningOutsideJobObject(ILogger logger); +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/MIB_TCPROW.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/MIB_TCPROW.cs new file mode 100644 index 0000000000..de0e9f5659 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/MIB_TCPROW.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// The MIB_TCPROW structure contains information for an IPv4 TCP connection. +[StructLayout(LayoutKind.Sequential)] +internal struct MIB_TCPROW +{ + /// The state of the TCP connection. + internal MIB_TCP_STATE State; + + /// The local IPv4 address for the TCP connection on the local computer. + internal uint LocalAddr; + + /// The local port number in network byte order for the TCP connection on the local computer. + internal uint LocalPort; + + /// The IPv4 address for the TCP connection on the remote computer. + internal uint RemoteAddr; + + /// The remote port number in network byte order for the TCP connection on the remote computer. + internal uint RemotePort; +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/MIB_TCPTABLE.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/MIB_TCPTABLE.cs new file mode 100644 index 0000000000..05880ca552 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/MIB_TCPTABLE.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +[StructLayout(LayoutKind.Sequential)] +internal struct MIB_TCPTABLE +{ + public uint NumberOfEntries; + public MIB_TCPROW Table; +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/MIB_TCP_STATE.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/MIB_TCP_STATE.cs new file mode 100644 index 0000000000..88b7c24d22 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/MIB_TCP_STATE.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// The MIB_TCP_STATE enumeration enumerates different possible TCP states. +internal enum MIB_TCP_STATE : uint +{ + /// The TCP connection is closed. + CLOSED = 1, + + /// The TCP connection is in the listen state. + LISTEN = 2, + + /// A SYN packet has been sent. + SYN_SENT = 3, + + /// A SYN packet has been received. + SYN_RCVD = 4, + + /// The TCP connection has been established. + ESTAB = 5, + + /// The TCP connection is waiting for a FIN packet. + FIN_WAIT1 = 6, + + /// The TCP connection is waiting for a FIN packet. + FIN_WAIT2 = 7, + + /// The TCP connection is in the close wait state. + CLOSE_WAIT = 8, + + /// The TCP connection is closing. + CLOSING = 9, + + /// The TCP connection is in the last ACK state. + LAST_ACK = 10, + + /// The TCP connection is in the time wait state. + TIME_WAIT = 11, + + /// The TCP connection is in the delete TCB state. + DELETE_TCB = 12 +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/NTSTATUS.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/NTSTATUS.cs new file mode 100644 index 0000000000..fdbaa7d5a0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/NTSTATUS.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// Win32 Error Code and NTSTATUS. +/// +internal enum NTSTATUS : uint +{ + /// ERROR_SUCCESS. + Success = 0x00000000, + + /// ERROR_INVALID_PARAMETER. + InvalidParameter = 0x00000057, + + /// ERROR_INSUFFICIENT_BUFFER. + InsufficientBuffer = 0x0000007A, + + /// {Operation Failed} The requested operation was unsuccessful. + UnsuccessfulStatus = 0xC0000001 +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/TcpStateInfo.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/TcpStateInfo.cs new file mode 100644 index 0000000000..376ca0d179 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/TcpStateInfo.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// TcpStateInfo contains different possible TCP state counts. +/// +internal sealed class TcpStateInfo +{ + /// Gets the count about the TCP connections which are closed. + public long ClosedCount; + + /// Gets the count about the TCP connections which are in the listen state. + public long ListenCount; + + /// Gets the count about the SYN packets which have been sent. + public long SynSentCount; + + /// Gets the count about the SYN packets which have been received. + public long SynRcvdCount; + + /// Gets the count about the TCP connections which have been established. + public long EstabCount; + + /// Gets the count about the TCP connections which are waiting for a FIN packet 1. + public long FinWait1Count; + + /// Gets the count about the TCP connections which are waiting for a FIN packet 2. + public long FinWait2Count; + + /// Gets the count about the TCP connections which are in the close wait state. + public long CloseWaitCount; + + /// Gets the count about the TCP connections which are closing. + public long ClosingCount; + + /// Gets the count about the TCP connections which are in the last ACK state. + public long LastAckCount; + + /// Gets the count about the TCP connections which are in the time wait state. + public long TimeWaitCount; + + /// Gets the count about the TCP connections which are in the delete TCB state. + public long DeleteTcbCount; +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/TcpTableInfo.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/TcpTableInfo.cs new file mode 100644 index 0000000000..d452a0d5f7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/Network/TcpTableInfo.cs @@ -0,0 +1,165 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Frozen; +using System.Linq; +using System.Net; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +internal sealed class TcpTableInfo +{ + private readonly object _lock = new(); + private readonly FrozenSet _localIPAddresses; + private readonly TimeSpan _cachingInterval; + internal delegate uint GetTcpTableDelegate(IntPtr pTcpTable, ref uint pdwSize, bool bOrder); + private static GetTcpTableDelegate _getTcpTable = SafeNativeMethods.GetTcpTable; + private TcpStateInfo _snapshot = new(); + private DateTimeOffset _refreshAfter; + private TimeProvider TimeProvider { get; set; } = TimeProvider.System; + + public TcpTableInfo(IOptions options) + { + var stringAddresses = options.Value.InstanceIpAddresses; + _localIPAddresses = stringAddresses +#pragma warning disable CS0618 + .Select(s => (uint)IPAddress.Parse(s).Address) +#pragma warning restore CS0618 + .ToFrozenSet(optimizeForReading: true); + _cachingInterval = options.Value.CachingInterval; + _refreshAfter = default; + } + + public TcpStateInfo GetCachingSnapshot() + { + lock (_lock) + { + var utcNow = TimeProvider.GetUtcNow(); + if (_refreshAfter < utcNow) + { + _snapshot = GetSnapshot(); + _refreshAfter = utcNow.Add(_cachingInterval); + } + } + + return _snapshot; + } + + internal static void SetGetTcpTableDelegate(GetTcpTableDelegate getTcpTableDelegate) => _getTcpTable = getTcpTableDelegate; + + private static IntPtr RetryCalling() + { + const int LoopTimes = 5; + uint size = 0; + var result = (uint)NTSTATUS.UnsuccessfulStatus; + var tcpTable = IntPtr.Zero; + + // Stryker disable once all : Loop for 5 times as a default setting. + for (int i = 0; i < LoopTimes; ++i) + { + // Stryker disable once all : True or false will not affect the results. + result = _getTcpTable(tcpTable, ref size, false); + switch ((NTSTATUS)result) + { + case NTSTATUS.Success: + return tcpTable; + case NTSTATUS.InsufficientBuffer: + Marshal.FreeHGlobal(tcpTable); + tcpTable = Marshal.AllocHGlobal((int)size); + break; + case NTSTATUS.UnsuccessfulStatus: + break; + default: + Marshal.FreeHGlobal(tcpTable); + throw new InvalidOperationException( + $"Failed to GetTcpTable. Return value is {result}. " + + $"For more information see https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-gettcptable"); + } + } + + Marshal.FreeHGlobal(tcpTable); + throw new InvalidOperationException( + $"Failed to GetTcpTable. Return value is {result}. " + + $"For more information see https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-gettcptable"); + } + + private unsafe TcpStateInfo GetSnapshot() + { + var tcpTable = RetryCalling(); + var rawTcpTable = Marshal.PtrToStructure(tcpTable); + + var tcpStateInfo = new TcpStateInfo(); + var offset = Marshal.OffsetOf(nameof(MIB_TCPTABLE.Table)).ToInt32(); + var rowPtr = IntPtr.Add(tcpTable, offset); + for (int i = 0; i < rawTcpTable.NumberOfEntries; ++i) + { + var row = Marshal.PtrToStructure(rowPtr); + rowPtr = IntPtr.Add(rowPtr, sizeof(MIB_TCPROW)); + + if (_localIPAddresses.Count > 0 && !_localIPAddresses.Contains(row.LocalAddr)) + { + continue; + } + + switch (row.State) + { + case MIB_TCP_STATE.CLOSED: + tcpStateInfo.ClosedCount++; + break; + case MIB_TCP_STATE.LISTEN: + tcpStateInfo.ListenCount++; + break; + case MIB_TCP_STATE.SYN_SENT: + tcpStateInfo.SynSentCount++; + break; + case MIB_TCP_STATE.SYN_RCVD: + tcpStateInfo.SynRcvdCount++; + break; + case MIB_TCP_STATE.ESTAB: + tcpStateInfo.EstabCount++; + break; + case MIB_TCP_STATE.FIN_WAIT1: + tcpStateInfo.FinWait1Count++; + break; + case MIB_TCP_STATE.FIN_WAIT2: + tcpStateInfo.FinWait2Count++; + break; + case MIB_TCP_STATE.CLOSE_WAIT: + tcpStateInfo.CloseWaitCount++; + break; + case MIB_TCP_STATE.CLOSING: + tcpStateInfo.ClosingCount++; + break; + case MIB_TCP_STATE.LAST_ACK: + tcpStateInfo.LastAckCount++; + break; + case MIB_TCP_STATE.TIME_WAIT: + tcpStateInfo.TimeWaitCount++; + break; + case MIB_TCP_STATE.DELETE_TCB: + tcpStateInfo.DeleteTcbCount++; + break; + } + } + + Marshal.FreeHGlobal(tcpTable); + return tcpStateInfo; + } + + private static class SafeNativeMethods + { + /// + /// Import of GetTcpTable win32 function. + /// + /// A pointer of a MIB_TCPTABLE struct. + /// On input, specifies the size in bytes of the buffer pointed to by the pTcpTable parameter. + /// On output, if the buffer is not large enough to hold the returned connection table, the function sets this parameter equal to the required buffer size in bytes. + /// A Boolean value that specifies whether the TCP connection table should be sorted. + [DllImport("Iphlpapi.dll", SetLastError = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + public static extern uint GetTcpTable(IntPtr pTcpTable, ref uint pdwSize, bool bOrder); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsContainerSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsContainerSnapshotProvider.cs new file mode 100644 index 0000000000..e98aae20dd --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsContainerSnapshotProvider.cs @@ -0,0 +1,159 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// A data source acquiring data from the kernel. +/// +internal sealed class WindowsContainerSnapshotProvider : ISnapshotProvider +{ + internal TimeProvider TimeProvider = TimeProvider.System; + + /// + /// The memory status. + /// + private readonly Lazy _memoryStatus; + + /// + /// This represents a factory method for creating the JobHandle. + /// + private readonly Func _createJobHandleObject; + + private readonly IProcessInfo _processInfo; + + public SystemResources Resources { get; } + + /// + /// Initializes a new instance of the class. + /// + [ExcludeFromCodeCoverage] + public WindowsContainerSnapshotProvider(ILogger logger) + { + Log.RunningInsideJobObject(logger); + + _memoryStatus = new Lazy( + new MemoryInfo().GetMemoryStatus, + LazyThreadSafetyMode.ExecutionAndPublication); + + Lazy systemInfo = new Lazy( + new SystemInfo().GetSystemInfo, + LazyThreadSafetyMode.ExecutionAndPublication); + + _createJobHandleObject = CreateJobHandle; + + _processInfo = new ProcessInfoWrapper(); + + // initialize system resources information + using var jobHandle = _createJobHandleObject(); + + var cpuUnits = GetGuaranteedCpuUnits(jobHandle, systemInfo); + var memory = GetMemoryLimits(jobHandle); + + Resources = new SystemResources(cpuUnits, cpuUnits, memory, memory); + } + + /// + /// Initializes a new instance of the class. + /// + /// A wrapper for the memory information retrieval object. + /// A wrapper for the system information retrieval object. + /// A wrapper for the process info retrieval object. + /// A factory method that creates object. + /// This constructor enables the mocking the dependencies for the purpose of Unit Testing only. + internal WindowsContainerSnapshotProvider(IMemoryInfo memoryInfo, ISystemInfo systemInfoObject, IProcessInfo processInfo, Func createJobHandleObject) + { + _memoryStatus = new Lazy(memoryInfo.GetMemoryStatus, LazyThreadSafetyMode.ExecutionAndPublication); + Lazy systemInfo = new Lazy(systemInfoObject.GetSystemInfo, LazyThreadSafetyMode.ExecutionAndPublication); + _processInfo = processInfo; + _createJobHandleObject = createJobHandleObject; + + // initialize system resources information + using var jobHandle = _createJobHandleObject(); + + var cpuUnits = GetGuaranteedCpuUnits(jobHandle, systemInfo); + var memory = GetMemoryLimits(jobHandle); + + Resources = new SystemResources(cpuUnits, cpuUnits, memory, memory); + } + + public ResourceUtilizationSnapshot GetSnapshot() + { + // Gather the information + // Cpu kernel and user ticks + using var jobHandle = _createJobHandleObject(); + var basicAccountingInfo = jobHandle.GetBasicAccountingInfo(); + + return new ResourceUtilizationSnapshot( + TimeSpan.FromTicks(TimeProvider.GetUtcNow().Ticks), + TimeSpan.FromTicks(basicAccountingInfo.TotalKernelTime), + TimeSpan.FromTicks(basicAccountingInfo.TotalUserTime), + GetMemoryUsage()); + } + + private static double GetGuaranteedCpuUnits(IJobHandle jobHandle, Lazy systemInfo) + { + // Note: This function convert the CpuRate from CPU cycles to CPU units, also it scales + // the CPU units with the number of processors (cores) available in the system. + const double CpuCycles = 10_000U; + + var cpuLimit = jobHandle.GetJobCpuLimitInfo(); + double cpuRatio = 1.0; + if (((cpuLimit.ControlFlags & (uint)JobObjectInfo.JobCpuRateControlLimit.CpuRateControlEnable) != 0) && + (cpuLimit.ControlFlags & (uint)JobObjectInfo.JobCpuRateControlLimit.CpuRateControlHardCap) != 0) + { + // The CpuRate is represented as number of cycles during scheduling interval, where + // a full cpu cycles number would equal 10_000, so for example if the CpuRate is 2_000, + // that means that the application (or container) is assigned 20% of the total CPU available. + // So, here we divide the CpuRate by 10_000 to convert it to a ratio (ex: 0.2 for 20% CPU). + // For more info: https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_cpu_rate_control_information?redirectedfrom=MSDN + cpuRatio = cpuLimit.CpuRate / CpuCycles; + } + + // Multiply the cpu ratio by the number of processors to get you the portion + // of processors used from the system. + return cpuRatio * systemInfo.Value.NumberOfProcessors; + } + + /// + /// Gets memory limit of the system. + /// + /// Memory limit allocated to the system in bytes. + private ulong GetMemoryLimits(IJobHandle jobHandle) + { + var memoryLimitInBytes = jobHandle.GetExtendedLimitInfo().JobMemoryLimit.ToUInt64(); + + if (memoryLimitInBytes <= 0) + { + var memoryStatus = _memoryStatus.Value; + + // Technically, the unconstrained limit is memoryStatus.TotalPageFile. + // Leaving this at physical as it is more understandable to R9 consumers at present. + memoryLimitInBytes = memoryStatus.TotalPhys; + } + + return memoryLimitInBytes; + } + + /// + /// Gets memory usage within the system. + /// + /// Memory usage within the system in bytes. + private ulong GetMemoryUsage() + { + var memoryInfo = _processInfo.GetCurrentAppMemoryInfo(); + + return memoryInfo.TotalCommitUsage; + } + + [ExcludeFromCodeCoverage] + private JobHandleWrapper CreateJobHandle() + { + return new JobHandleWrapper(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsCounters.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsCounters.cs new file mode 100644 index 0000000000..a62321f309 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsCounters.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Metering; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +internal sealed class WindowsCounters : IDisposable +{ + private readonly Meter _meter; + private readonly TcpTableInfo _tcpTableInfo; + + public WindowsCounters(IOptions options, Meter meter) + { + _tcpTableInfo = new TcpTableInfo(options); + + _meter = meter; + + _ = _meter.CreateObservableGauge( + "ipv4_tcp_connection_closed_count", + () => + { + var snapshot = _tcpTableInfo.GetCachingSnapshot(); + return snapshot.ClosedCount; + }); + + _ = _meter.CreateObservableGauge( + "ipv4_tcp_connection_listen_count", + () => + { + var snapshot = _tcpTableInfo.GetCachingSnapshot(); + return snapshot.ListenCount; + }); + + _ = _meter.CreateObservableGauge( + "ipv4_tcp_connection_syn_sent_count", + () => + { + var snapshot = _tcpTableInfo.GetCachingSnapshot(); + return snapshot.SynSentCount; + }); + + _ = _meter.CreateObservableGauge( + "ipv4_tcp_connection_syn_received_count", + () => + { + var snapshot = _tcpTableInfo.GetCachingSnapshot(); + return snapshot.SynRcvdCount; + }); + + _ = _meter.CreateObservableGauge( + "ipv4_tcp_connection_established_count", + () => + { + var snapshot = _tcpTableInfo.GetCachingSnapshot(); + return snapshot.EstabCount; + }); + + _ = _meter.CreateObservableGauge( + "ipv4_tcp_connection_fin_wait_1_count", + () => + { + var snapshot = _tcpTableInfo.GetCachingSnapshot(); + return snapshot.FinWait1Count; + }); + + _ = _meter.CreateObservableGauge( + "ipv4_tcp_connection_fin_wait_2_count", + () => + { + var snapshot = _tcpTableInfo.GetCachingSnapshot(); + return snapshot.FinWait2Count; + }); + + _ = _meter.CreateObservableGauge( + "ipv4_tcp_connection_close_wait_count", + () => + { + var snapshot = _tcpTableInfo.GetCachingSnapshot(); + return snapshot.CloseWaitCount; + }); + + _ = _meter.CreateObservableGauge( + "ipv4_tcp_connection_closing_count", + () => + { + var snapshot = _tcpTableInfo.GetCachingSnapshot(); + return snapshot.ClosingCount; + }); + + _ = _meter.CreateObservableGauge( + "ipv4_tcp_connection_last_ack_count", + () => + { + var snapshot = _tcpTableInfo.GetCachingSnapshot(); + return snapshot.LastAckCount; + }); + + _ = _meter.CreateObservableGauge( + "ipv4_tcp_connection_time_wait_count", + () => + { + var snapshot = _tcpTableInfo.GetCachingSnapshot(); + return snapshot.TimeWaitCount; + }); + + _ = _meter.CreateObservableGauge( + "ipv4_tcp_connection_delete_tcb_count", + () => + { + var snapshot = _tcpTableInfo.GetCachingSnapshot(); + return snapshot.DeleteTcbCount; + }); + } + + public void Dispose() + { + _meter.Dispose(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsCountersOptionsCustomValidator.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsCountersOptionsCustomValidator.cs new file mode 100644 index 0000000000..67c23fbaef --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsCountersOptionsCustomValidator.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Options; +using Validation = Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +internal sealed class WindowsCountersOptionsCustomValidator : IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, WindowsCountersOptions options) + { + var builder = new ValidateOptionsResultBuilder(); + foreach (var s in options.InstanceIpAddresses) + { + if (!IPAddress.TryParse(s, out var ipAddress) + || ipAddress.AddressFamily != AddressFamily.InterNetwork) + { + builder.AddError(nameof(options.InstanceIpAddresses), "must only contain IPv4 addresses"); + } + } + + return builder.Build(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsCountersOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsCountersOptionsValidator.cs new file mode 100644 index 0000000000..501d31049d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsCountersOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +[OptionsValidator] +internal sealed partial class WindowsCountersOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsPerfCounterConstants.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsPerfCounterConstants.cs new file mode 100644 index 0000000000..ef27bd7431 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsPerfCounterConstants.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// Performance counter constants. +/// +internal static class WindowsPerfCounterConstants +{ + /// + /// The container counter category name. + /// + public const string ContainerCounterCategoryName = "COSMIC Containers"; + + /// + /// The container counter category label. + /// + public const string ContainerCounterCategoryLabel = "Container counters"; + + /// + /// The cpu utilization percentage. + /// + public const string CpuUtilizationCounterName = "% CPU Limit Utilization"; + + /// + /// The memory utilization percentage. + /// + public const string MemoryUtilizationCounterName = "% Memory Limit Utilization"; + + /// + /// The memory limit. + /// + public const string MemoryLimitCounterName = "Available Memory Limit"; +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsPerfCounterPublisher.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsPerfCounterPublisher.cs new file mode 100644 index 0000000000..a05ce062d5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsPerfCounterPublisher.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +/// +/// The Perf Counter publisher. +/// +[ExcludeFromCodeCoverage] +#pragma warning disable IDE0079 // Remove unnecessary suppression +[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "Indeed, this whole class is for Windows only")] +#pragma warning restore IDE0079 // Remove unnecessary suppression +internal sealed class WindowsPerfCounterPublisher : IResourceUtilizationPublisher +{ + // Since everything is represented in PerfMon as longs, rather than doubles internally, represent everything as + // a fraction of N / 10,000. + // When displaying, PerfMon will auto-adjust this to K / 100, and thus represent places to two decimal places. + private const double MaximumPercentageValue = 100.0; + private const double ScaleToRepresentTwoDecimalPlaces = 100.0; + private const long MaximumIntervalValue = (long)(MaximumPercentageValue * ScaleToRepresentTwoDecimalPlaces); + + private readonly WindowsPerfCounters _counters; + + /// + /// Initializes a new instance of the class. + /// + /// The constructor mainly checks for the existence of windows performance counters category and logs an error if it does not exist. + public WindowsPerfCounterPublisher(ILogger logger) + { + if (!PerformanceCounterCategory.Exists(WindowsPerfCounterConstants.ContainerCounterCategoryName)) + { + Log.CounterDoesNotExist(logger, WindowsPerfCounterConstants.ContainerCounterCategoryName); + } + + _counters = CreateInstanceCounter(); + } + + /// + public ValueTask PublishAsync(Utilization utilization, CancellationToken cancellationToken) + { + // Throw exception if cancellation was requested. + cancellationToken.ThrowIfCancellationRequested(); + + _counters.CpuUtilization.RawValue = (long)((utilization.Snapshot.UserTimeSinceStart.Ticks + utilization.Snapshot.KernelTimeSinceStart.Ticks) / utilization.SystemResources.GuaranteedCpuUnits); + _counters.MemUtilization.RawValue = (long)(utilization.MemoryUsedPercentage * ScaleToRepresentTwoDecimalPlaces); + _counters.MemLimit.RawValue = MaximumIntervalValue; + return default; + } + + private static WindowsPerfCounters CreateInstanceCounter() + { + const string InstanceName = "Total"; + return new WindowsPerfCounters( + cpuUtilization: new PerformanceCounter(WindowsPerfCounterConstants.ContainerCounterCategoryName, WindowsPerfCounterConstants.CpuUtilizationCounterName, InstanceName, false), + memUtilization: new PerformanceCounter(WindowsPerfCounterConstants.ContainerCounterCategoryName, WindowsPerfCounterConstants.MemoryUtilizationCounterName, InstanceName, false), + memLimit: new PerformanceCounter(WindowsPerfCounterConstants.ContainerCounterCategoryName, WindowsPerfCounterConstants.MemoryLimitCounterName, InstanceName, false)); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsPerfCounters.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsPerfCounters.cs new file mode 100644 index 0000000000..b2c00e3097 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsPerfCounters.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +[ExcludeFromCodeCoverage] +internal readonly struct WindowsPerfCounters +{ + public PerformanceCounter CpuUtilization { get; } + + public PerformanceCounter MemUtilization { get; } + + public PerformanceCounter MemLimit { get; } + + public WindowsPerfCounters( + PerformanceCounter cpuUtilization, + PerformanceCounter memUtilization, + PerformanceCounter memLimit) + { + CpuUtilization = cpuUtilization; + MemUtilization = memUtilization; + MemLimit = memLimit; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsSnapshotProvider.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsSnapshotProvider.cs new file mode 100644 index 0000000000..c830b3112d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/Internal/WindowsSnapshotProvider.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +internal sealed class WindowsSnapshotProvider : ISnapshotProvider +{ + public SystemResources Resources { get; } + + internal TimeProvider TimeProvider = TimeProvider.System; + + public WindowsSnapshotProvider(ILogger logger) + { + Log.RunningOutsideJobObject(logger); + + var memoryStatus = new MemoryInfo().GetMemoryStatus(); + var cpuUnits = Environment.ProcessorCount; + Resources = new SystemResources(cpuUnits, cpuUnits, memoryStatus.TotalPhys, memoryStatus.TotalPhys); + } + + public ResourceUtilizationSnapshot GetSnapshot() + { + // Gather the information + // Get CPU kernel and user ticks + var process = Process.GetCurrentProcess(); + + return new ResourceUtilizationSnapshot( + TimeSpan.FromTicks(TimeProvider.GetUtcNow().Ticks), + TimeSpan.FromTicks(process.PrivilegedProcessorTime.Ticks), + TimeSpan.FromTicks(process.UserProcessorTime.Ticks), + (ulong)process.WorkingSet64); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsCountersOptions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsCountersOptions.cs new file mode 100644 index 0000000000..e481a0ec3c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsCountersOptions.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Data.Validation; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +/// +/// Options for WindowsCounters. +/// +[Experimental] +public class WindowsCountersOptions +{ + internal const int MinimumCachingInterval = 100; + internal const int MaximumCachingInterval = 900000; // 15 minutes. + internal static readonly TimeSpan DefaultCachingInterval = TimeSpan.FromSeconds(5); + + /// + /// Gets or sets the list of source IPv4 addresses to track the connections for in telemetry. + /// + [Required] +#pragma warning disable CA2227 // Collection properties should be read only + public ISet InstanceIpAddresses { get; set; } = new HashSet(); +#pragma warning restore CA2227 // Collection properties should be read only + + /// + /// Gets or sets the default interval used for freshing TcpStateInfo Cache. + /// + /// + /// Default set to 5 seconds. + /// + [Experimental] + [TimeSpan(MinimumCachingInterval, MaximumCachingInterval)] + public TimeSpan CachingInterval { get; set; } = DefaultCachingInterval; +} diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsUtilizationExtensions.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsUtilizationExtensions.cs new file mode 100644 index 0000000000..76daed5931 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Windows/WindowsUtilizationExtensions.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring; + +/// +/// Lets you track memory and CPU usage of applications. +/// +public static class WindowsUtilizationExtensions +{ + /// + /// An extension method to configure and add the default windows utilization provider to services collection. + /// + /// The tracker builder instance used to add the provider. + /// Returns the input tracker builder for call chaining. + /// is . + [ExcludeFromCodeCoverage] + public static IResourceUtilizationTrackerBuilder AddWindowsProvider(this IResourceUtilizationTrackerBuilder builder) + { + _ = Throw.IfNull(builder); + + if (JobObjectInfo.SafeJobHandle.IsProcessInJob()) + { + builder.Services.TryAddSingleton(); + } + else + { + builder.Services.TryAddSingleton(); + } + + return builder; + } + + /// + /// An extension method to configure and add the default windows performance counters publisher to services collection. + /// + /// The tracker builder instance used to add the publisher. + /// Returns the input tracker builder for call chaining. + /// is . + public static IResourceUtilizationTrackerBuilder AddWindowsPerfCounterPublisher(this IResourceUtilizationTrackerBuilder builder) + { + _ = Throw.IfNull(builder); + _ = builder + .AddWindowsProvider() + .AddPublisher(); + + return builder; + } + + /// + /// An extension method that creates a few OpenTelemetry instruments for system counters. + /// + /// The builder. + /// Returns the builder. + /// is . + /// + /// . + /// + [Experimental] + public static IResourceUtilizationTrackerBuilder AddWindowsCounters(this IResourceUtilizationTrackerBuilder builder) + { + _ = Throw.IfNull(builder); + + _ = builder.Services + .AddActivatedSingleton(); + + _ = builder.Services + .RegisterMetering(); + + _ = builder.Services + .AddValidatedOptions(); + + builder.Services + .TryAddEnumerable(ServiceDescriptor.Singleton, WindowsCountersOptionsCustomValidator>()); + + return builder; + } + + /// + /// An extension method that creates a few OpenTelemetry instruments for system counters. + /// + /// The builder. + /// The to use for configuring of . + /// Returns the builder. + /// is . + /// + /// . + /// + [Experimental] + public static IResourceUtilizationTrackerBuilder AddWindowsCounters(this IResourceUtilizationTrackerBuilder builder, IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + + _ = builder.Services + .AddActivatedSingleton(); + + _ = builder.Services + .RegisterMetering(); + + _ = builder.Services + .AddValidatedOptions() + .Bind(section); + + builder.Services + .TryAddEnumerable(ServiceDescriptor.Singleton, WindowsCountersOptionsCustomValidator>()); + + return builder; + } + + /// + /// An extension method that creates a few OpenTelemetry instruments for system counters. + /// + /// The builder. + /// The delegate for configuring of . + /// Returns the builder. + /// is . + /// + /// . + /// + [Experimental] + public static IResourceUtilizationTrackerBuilder AddWindowsCounters(this IResourceUtilizationTrackerBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + _ = builder.Services + .AddActivatedSingleton(); + + _ = builder.Services + .RegisterMetering(); + + _ = builder.Services + .AddValidatedOptions() + .Configure(configure); + + builder.Services + .TryAddEnumerable(ServiceDescriptor.Singleton, WindowsCountersOptionsCustomValidator>()); + + return builder; + } +} diff --git a/src/Libraries/Microsoft.Extensions.EnumStrings/EnumStringsAttribute.cs b/src/Libraries/Microsoft.Extensions.EnumStrings/EnumStringsAttribute.cs new file mode 100644 index 0000000000..6cff8cd8c8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.EnumStrings/EnumStringsAttribute.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; + +namespace Microsoft.Extensions.EnumStrings; + +/// +/// Provides information to guide the production of an extension method to efficiently convert an enum value into string form. +/// +[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Enum, AllowMultiple = true)] +[Conditional("CODE_GENERATION_ATTRIBUTES")] +public sealed class EnumStringsAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// Use this overload when directly annotating an enum type. + /// + public EnumStringsAttribute() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The type of the enum to work with. + /// + /// Use this overload when applying the attribute at the assembly-level when working with an enum declared in a + /// different assembly. + /// + /// + /// + /// [assembly: EnumStrings(typeof(System.ConsoleKey))] + /// + /// + public EnumStringsAttribute(Type enumType) + { + EnumType = enumType; + } + + /// + /// Gets the type of the enum to annotate. + /// + /// + /// This is when the attribute is applied directly to an enum type. + /// + public Type? EnumType { get; } + + /// + /// Gets or sets the namespace of the generated class. + /// + /// + /// If this is , then the class is generated in the same namespace as the enum. + /// Defaults to . + /// + public string? ExtensionNamespace { get; set; } + + /// + /// Gets or sets the name of the generated class. + /// + /// + /// If this is , then the class name is generated by appending Extensions to the enum type name. + /// Defaults to . + /// + public string? ExtensionClassName { get; set; } + + /// + /// Gets or sets the name of the generated extension method. + /// + /// + /// Defaults to ToInvariantString. + /// + public string ExtensionMethodName { get; set; } = "ToInvariantString"; + + /// + /// Gets or sets the modifiers to apply to the generated class. + /// + /// + /// Defaults to internal static. + /// + public string ExtensionClassModifiers { get; set; } = "internal static"; +} diff --git a/src/Libraries/Microsoft.Extensions.EnumStrings/Microsoft.Extensions.EnumStrings.csproj b/src/Libraries/Microsoft.Extensions.EnumStrings/Microsoft.Extensions.EnumStrings.csproj new file mode 100644 index 0000000000..aab4bbe46e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.EnumStrings/Microsoft.Extensions.EnumStrings.csproj @@ -0,0 +1,27 @@ + + + Microsoft.Extensions.EnumStrings + Abstractions to support the enum-to-string code generator. + Fundamentals + + + + true + + + + normal + 100 + 100 + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.EnumStrings/buildTransitive/Microsoft.Extensions.EnumStrings.props b/src/Libraries/Microsoft.Extensions.EnumStrings/buildTransitive/Microsoft.Extensions.EnumStrings.props new file mode 100644 index 0000000000..7bc91e4438 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.EnumStrings/buildTransitive/Microsoft.Extensions.EnumStrings.props @@ -0,0 +1,2 @@ + + diff --git a/src/Libraries/Microsoft.Extensions.EnumStrings/buildTransitive/Microsoft.Extensions.EnumStrings.targets b/src/Libraries/Microsoft.Extensions.EnumStrings/buildTransitive/Microsoft.Extensions.EnumStrings.targets new file mode 100644 index 0000000000..ceadfacb28 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.EnumStrings/buildTransitive/Microsoft.Extensions.EnumStrings.targets @@ -0,0 +1,3 @@ + + + diff --git a/src/Libraries/Microsoft.Extensions.Hosting.Testing/FakeHost.cs b/src/Libraries/Microsoft.Extensions.Hosting.Testing/FakeHost.cs new file mode 100644 index 0000000000..15a4e971fb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Hosting.Testing/FakeHost.cs @@ -0,0 +1,126 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing.Internal; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Hosting.Testing; + +/// +/// Unit testing friendly configured host. +/// +public sealed class FakeHost : IHost +{ + /// + /// Gets the programs configured services. + /// + public IServiceProvider Services => _host.Services; + internal TimeProvider TimeProvider = TimeProvider.System; + private readonly IHost _host; + private readonly FakeHostOptions _options; + private bool _disposed; + + internal FakeHost(IHost host, FakeHostOptions options) + { + _host = host; + _options = options; + } + + /// + /// Creates an instance of to configure and build the host. + /// + /// An instance of . + public static IHostBuilder CreateBuilder() => new FakeHostBuilder(new FakeHostOptions()); + + /// + /// Creates an instance of to configure and build the host. + /// + /// Use to configure the instance. + /// An instance of . + public static IHostBuilder CreateBuilder(Action configure) + { + _ = Throw.IfNull(configure); + + var options = new FakeHostOptions(); + configure(options); + return CreateBuilder(options); + } + + /// + /// Creates an instance of to configure and build the host. + /// + /// An instance. + /// An instance of . + public static IHostBuilder CreateBuilder(FakeHostOptions options) + { + _ = Throw.IfNull(options); + return new FakeHostBuilder(options); + } + + /// + /// Start the program. + /// + /// Used to abort program start. + /// A that will be completed when the starts. + /// If no cancellation token is given, a new one using is used. + public async Task StartAsync(CancellationToken cancellationToken = default) + { + using var cancellationTokenSource = TimeProvider.CreateCancellationTokenSource(_options.StartUpTimeout); + + if (cancellationToken == default) + { + await _host.StartAsync(cancellationTokenSource.Token).ConfigureAwait(false); + } + else + { + using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + cancellationTokenSource.Token); + await _host.StartAsync(linkedTokenSource.Token).ConfigureAwait(false); + } + } + + /// + /// Attempts to gracefully stop the program. + /// + /// Used to indicate when stop should no longer be graceful. + /// A that will be completed when the stops. + /// If no cancellation token is given, a new one using is used. + public async Task StopAsync(CancellationToken cancellationToken = default) + { + using var cancellationTokenSource = TimeProvider.CreateCancellationTokenSource(_options.ShutDownTimeout); + + if (cancellationToken == default) + { + await _host.StopAsync(cancellationTokenSource.Token).ConfigureAwait(false); + } + else + { + using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + cancellationTokenSource.Token); + await _host.StopAsync(linkedTokenSource.Token).ConfigureAwait(false); + } + } + + /// + /// Disposes the instance. + /// + /// Tries to gracefully shut down the host. Can be called multiple times. + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + + StopAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + _host.Dispose(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Hosting.Testing/FakeHostOptions.cs b/src/Libraries/Microsoft.Extensions.Hosting.Testing/FakeHostOptions.cs new file mode 100644 index 0000000000..620ceddc3b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Hosting.Testing/FakeHostOptions.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Hosting.Testing; + +/// +/// Options to configure . +/// +public class FakeHostOptions +{ + /// + /// Gets or sets time limit for host to start. + /// + /// Default is 5 seconds. This limit is used if no cancellation token is used by user. + public TimeSpan StartUpTimeout { get; set; } = TimeSpan.FromSeconds(5); + + /// + /// Gets or sets time limit for host to shut down. + /// + /// Default is 10 seconds. This limit is used if no cancellation token is used by user. + public TimeSpan ShutDownTimeout { get; set; } = TimeSpan.FromSeconds(10); + + /// + /// Gets or sets time limit for host to be up. + /// + /// + /// Default is 30 seconds. + /// Value -1 millisecond means infinite time to live. + /// TimeToLive is not enforced when debugging. + /// + public TimeSpan TimeToLive { get; set; } = TimeSpan.FromSeconds(30); + + /// + /// Gets or sets a value indicating whether fake logging would be configured automatically. + /// + /// Default is . + public bool FakeLogging { get; set; } = true; + + /// + public bool ValidateScopes { get; set; } = true; + + /// + public bool ValidateOnBuild { get; set; } = true; + + /// + /// Gets or sets a value indicating whether fake redaction would be configured automatically. + /// + /// Default is . + public bool FakeRedaction { get; set; } = true; +} diff --git a/src/Libraries/Microsoft.Extensions.Hosting.Testing/HostingFakesExtensions.cs b/src/Libraries/Microsoft.Extensions.Hosting.Testing/HostingFakesExtensions.cs new file mode 100644 index 0000000000..d93ef4b91a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Hosting.Testing/HostingFakesExtensions.cs @@ -0,0 +1,187 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing.Internal; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Hosting.Testing; + +/// +/// Extension methods supporting host unit testing scenarios. +/// +[Experimental] +public static class HostingFakesExtensions +{ + /// + /// Starts and immediately stops the service. + /// + /// The tested service. + /// Cancellation token. See . + /// A representing the asynchronous operation. + public static async Task StartAndStopAsync(this IHostedService service, CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(service); + + try + { + await service.StartAsync(cancellationToken).ConfigureAwait(false); + } + finally + { + await service.StopAsync(cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Gets the object that collects log records sent to the fake logger. + /// + /// An instance. + /// When no collector exists in the provider. + /// The collector which tracks records logged to fake loggers. + public static FakeLogCollector GetFakeLogCollector(this IHost host) + { + _ = Throw.IfNull(host); + return host.Services.GetFakeLogCollector(); + } + + /// + /// Gets the object reporting all redactions performed. + /// + /// An instance. + /// When no collector exists in the provider. + /// The collector which tracks redactions performed on log messages. + public static FakeRedactionCollector GetFakeRedactionCollector(this IHost host) + { + _ = Throw.IfNull(host); + return host.Services.GetFakeRedactionCollector(); + } + + /// + /// Adds an action invoked on each log message. + /// + /// An instance. + /// The action to invoke on each log message. + /// The instance. + public static IHostBuilder AddFakeLoggingOutputSink(this IHostBuilder builder, Action callback) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(callback); + + return builder.ConfigureServices(services => services.AddFakeLogging(logging => + { + if (logging.OutputSink is null) + { + logging.OutputSink = callback; + } + else + { + var currentCallback = logging.OutputSink; + logging.OutputSink = x => + { + currentCallback(x); + callback(x); + }; + } + })); + } + + /// + /// Exposes for changes via a delegate. + /// + /// An instance. + /// Configures the instance. + /// The instance. + /// Designed to ease host configuration in unit tests by defining common configuration methods. + [SuppressMessage("Minor Code Smell", "S3872:Parameter names should not duplicate the names of their methods", + Justification = "We want to keep the parameter name for consistency.")] + public static IHostBuilder Configure(this IHostBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + configure(builder); + return builder; + } + + /// + /// Adds configuration entries. + /// + /// An instance. + /// A list of key and value tuples that will be used as configuration entries. + /// The instance. + public static IHostBuilder ConfigureHostConfiguration(this IHostBuilder builder, params (string key, string value)[] configurations) + { + _ = Throw.IfNull(configurations); + + foreach ((var key, var value) in configurations) + { + _ = builder.ConfigureHostConfiguration(key, value); + } + + return builder; + } + + /// + /// Adds a configuration value. + /// + /// An instance. + /// The configuration key. + /// The configuration value. + /// The instance. + public static IHostBuilder ConfigureHostConfiguration(this IHostBuilder builder, string key, string value) + { + _ = Throw.IfNull(builder); + return builder.ConfigureHostConfiguration(configBuilder => ConfigureConfiguration(configBuilder, key, value)); + } + + /// + /// Adds configuration entries. + /// + /// An instance. + /// A list of key and value tuples that will be used as configuration entries. + /// The instance. + public static IHostBuilder ConfigureAppConfiguration(this IHostBuilder builder, params (string key, string value)[] configurations) + { + _ = Throw.IfNull(configurations); + + foreach ((var key, var value) in configurations) + { + _ = builder.ConfigureAppConfiguration(key, value); + } + + return builder; + } + + /// + /// Adds a configuration value. + /// + /// An instance. + /// The configuration key. + /// The configuration value. + /// The instance. + public static IHostBuilder ConfigureAppConfiguration(this IHostBuilder builder, string key, string value) + { + _ = Throw.IfNull(builder); + return builder.ConfigureAppConfiguration((_, configBuilder) => ConfigureConfiguration(configBuilder, key, value)); + } + + private static void ConfigureConfiguration(IConfigurationBuilder builder, string key, string value) + { + if (builder.Sources.LastOrDefault() is FakeConfigurationSource source) + { + source.InitialData = source.InitialData!.Concat(new[] { new KeyValuePair(key, value) }); + return; + } + + _ = builder.Add(new FakeConfigurationSource(new KeyValuePair(key, value))); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Hosting.Testing/Internal/FakeConfigurationSource.cs b/src/Libraries/Microsoft.Extensions.Hosting.Testing/Internal/FakeConfigurationSource.cs new file mode 100644 index 0000000000..11faffae0b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Hosting.Testing/Internal/FakeConfigurationSource.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Configuration.Memory; + +namespace Microsoft.Extensions.Hosting.Testing.Internal; + +internal sealed class FakeConfigurationSource : MemoryConfigurationSource +{ + public FakeConfigurationSource(params KeyValuePair[] initialData) + { + InitialData = initialData; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Hosting.Testing/Internal/FakeHostBuilder.cs b/src/Libraries/Microsoft.Extensions.Hosting.Testing/Internal/FakeHostBuilder.cs new file mode 100644 index 0000000000..f8242ab71f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Hosting.Testing/Internal/FakeHostBuilder.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Testing.Logging; + +namespace Microsoft.Extensions.Hosting.Testing.Internal; + +internal sealed class FakeHostBuilder : IHostBuilder +{ + private readonly IHostBuilder _builder; + private readonly FakeHostOptions _options; + + internal FakeHostBuilder(FakeHostOptions options) + : this(new HostBuilder(), options) + { + } + + internal FakeHostBuilder(IHostBuilder builder, FakeHostOptions options) + { + _options = options; + _builder = builder; + + _ = builder + .ConfigureServices(services => services + .AddSingleton(options) + .AddHostedService()); + + if (options.FakeLogging) + { + _ = _builder.ConfigureServices(services => + { + services + .AddFakeLogging() + .TryAddSingleton(); + + }); + } + + if (options.FakeRedaction) + { + _builder = _builder.ConfigureServices(services => services.AddFakeRedaction()); + } + + if (options.ValidateScopes || options.ValidateOnBuild) + { + var serviceProviderOptions = new ServiceProviderOptions + { + ValidateScopes = options.ValidateScopes, + ValidateOnBuild = options.ValidateOnBuild + }; + + _builder = _builder.UseServiceProviderFactory(new DefaultServiceProviderFactory(serviceProviderOptions)); + } + } + + public IHostBuilder ConfigureHostConfiguration(Action configureDelegate) + { + return _builder.ConfigureHostConfiguration(configureDelegate); + } + + public IHostBuilder ConfigureAppConfiguration(Action configureDelegate) + { + return _builder.ConfigureAppConfiguration(configureDelegate); + } + + public IHostBuilder ConfigureServices(Action configureDelegate) + { + return _builder.ConfigureServices(configureDelegate); + } + + public IHostBuilder UseServiceProviderFactory(IServiceProviderFactory factory) + where TContainerBuilder : notnull + { + return _builder.UseServiceProviderFactory(factory); + } + + public IHostBuilder UseServiceProviderFactory(Func> factory) + where TContainerBuilder : notnull + { + return _builder.UseServiceProviderFactory(factory); + } + + public IHostBuilder ConfigureContainer(Action configureDelegate) => _builder.ConfigureContainer(configureDelegate); + + public IHost Build() => new FakeHost(_builder.Build(), _options); + + public IDictionary Properties => _builder.Properties; +} diff --git a/src/Libraries/Microsoft.Extensions.Hosting.Testing/Internal/HostTerminatorService.cs b/src/Libraries/Microsoft.Extensions.Hosting.Testing/Internal/HostTerminatorService.cs new file mode 100644 index 0000000000..9a742b3826 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Hosting.Testing/Internal/HostTerminatorService.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.Extensions.Hosting.Testing.Internal; + +/// +/// Terminates its host after a timeout set in . +/// +internal sealed partial class HostTerminatorService : BackgroundService +{ + internal bool DebuggerAttached = Debugger.IsAttached; + internal TimeProvider TimeProvider = TimeProvider.System; + private readonly IHost _host; + private readonly FakeHostOptions _options; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The instance. + /// Options containing the time to live. + /// An instance. + public HostTerminatorService(IHost host, FakeHostOptions options, ILogger logger) + { + _host = host; + _options = options; + _logger = logger; + } + + /// + /// Waits till the time to live is up or till the service is stopped. + /// + /// Triggered when is called. + /// A that represents the long running operations. + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (DebuggerAttached) + { + LogDebuggerAttached(); + return; + } + + await TimeProvider.Delay(_options.TimeToLive, stoppingToken).ConfigureAwait(false); + + using var timeoutTokenSource = TimeProvider.CreateCancellationTokenSource(_options.ShutDownTimeout); + + using var combinedTokenSource = CancellationTokenSource.CreateLinkedTokenSource( + stoppingToken, + timeoutTokenSource.Token); + + LogTimeToLiveUp(_options.TimeToLive); + await _host.StopAsync(combinedTokenSource.Token).ConfigureAwait(false); + _host.Dispose(); + } + + [LogMethod(0, LogLevel.Warning, "FakeHostOptions.TimeToLive set to {timeToLive} is up, disposing the host.")] + private partial void LogTimeToLiveUp(TimeSpan timeToLive); + + [LogMethod(1, LogLevel.Information, "Debugger is attached. The host won't be automatically disposed.")] + private partial void LogDebuggerAttached(); +} diff --git a/src/Libraries/Microsoft.Extensions.Hosting.Testing/Microsoft.Extensions.Hosting.Testing.csproj b/src/Libraries/Microsoft.Extensions.Hosting.Testing/Microsoft.Extensions.Hosting.Testing.csproj new file mode 100644 index 0000000000..983a13410f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Hosting.Testing/Microsoft.Extensions.Hosting.Testing.csproj @@ -0,0 +1,35 @@ + + + Microsoft.Extensions.Hosting + Testing for integration test hosting and related test oriented helpers + Fundamentals + $(PackageTags);Testing + + + + true + true + + + + dev + 100 + 90 + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/AutoClientAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/AutoClientAttribute.cs new file mode 100644 index 0000000000..0c3323cde5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/AutoClientAttribute.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; + +namespace Microsoft.Extensions.Http.AutoClient; + +/// +/// Triggers the generation of REST APIs and provides information about the HTTP client and, optionally, the name of the dependency. +/// +/// +/// This attribute triggers the production of REST APIs and provides information about the HTTP client and optionally the name of the dependency. +/// It can only be applied to interfaces and their name must start with an 'I', for example IMyClient. +/// This attribute must receive as a first parameter the HTTP client name to be retrieved from the . +/// Optionally, it may receive a second attribute that will set the dependency name used in generated telemetry. If this value is not set, it will use the name of the interface +/// without the leading 'I'. +/// If the interface name ends in 'Client' or 'Api', the dependency name will exclude that. Example: IMyDependencyClient would result in dependency name MyDependency. +/// +/// +/// +/// [AutoClient("MyClient")] +/// interface IMyDependencyClient +/// { +/// } +/// +/// +[Experimental] +[AttributeUsage(AttributeTargets.Interface)] +[Conditional("CODE_GENERATION_ATTRIBUTES")] +public sealed class AutoClientAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the HTTP client to be retrieved from . + public AutoClientAttribute(string httpClientName) + { + HttpClientName = httpClientName; + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the HTTP client to be retrieved from . + /// The dependency name override to be used with R9 Telemetry. + public AutoClientAttribute(string httpClientName, string customDependencyName) + { + HttpClientName = httpClientName; + CustomDependencyName = customDependencyName; + } + + /// + /// Gets the HTTP client name of the API. + /// + public string HttpClientName { get; } + + /// + /// Gets the custom dependency name of the API. + /// + public string? CustomDependencyName { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/AutoClientException.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/AutoClientException.cs new file mode 100644 index 0000000000..e244ea0691 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/AutoClientException.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.AutoClient; + +/// +/// Exception used whenever REST API requests are not successful. +/// +/// +/// This exception is thrown whenever a REST API call returns a non-successful status code. It contains the status code +/// and the HTTP content returned by the dependency, so that the user can handle exceptions accordingly. +/// +/// +/// +/// try +/// { +/// await _myClient.SendRequest(); +/// } +/// catch (Exception ex) when (ex.StatusCode == 403) +/// { +/// // Handle forbidden scenario +/// } +/// +/// +[SuppressMessage("Design", "CA1032:Implement standard exception constructors", Justification = "Not applicable to this exception")] +[Experimental] +public class AutoClientException : Exception +{ + /// + /// Gets the HTTP response. + /// + public AutoClientHttpError? HttpError { get; } + + /// + /// Gets the initial path of the HTTP request. + /// + public string Path { get; } + + /// + /// Gets the HTTP status code. + /// + public int? StatusCode => HttpError?.StatusCode; + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// The path of the request. + /// The HTTP error details. + public AutoClientException(string? message, string path, AutoClientHttpError? error = null) + : base(message) + { + Path = Throw.IfNull(path); + HttpError = error; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/AutoClientHttpError.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/AutoClientHttpError.cs new file mode 100644 index 0000000000..05b9b71397 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/AutoClientHttpError.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Primitives; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.AutoClient; + +/// +/// Holds details about an HTTP error. +/// +/// +/// When a REST API client fails, it will throw a . +/// This exception contains a instance that holds details like content, headers and status code. +/// +[Experimental] +public class AutoClientHttpError +{ + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code of the response. + /// The response headers. + /// The raw string content of the response. + /// The HTTP error reason. + public AutoClientHttpError(int statusCode, IReadOnlyDictionary responseHeaders, string rawContent, string? reasonPhrase) + { + StatusCode = Throw.IfNull(statusCode); + ResponseHeaders = Throw.IfNull(responseHeaders); + RawContent = Throw.IfNull(rawContent); + ReasonPhrase = reasonPhrase; + } + + /// + /// Gets the HTTP status code returned in the response. + /// + public int StatusCode { get; } + + /// + /// Gets the HTTP response headers. + /// + public IReadOnlyDictionary ResponseHeaders { get; } + + /// + /// Gets the raw string content returned in the response. + /// + public string RawContent { get; } + + /// + /// Gets the HTTP error reason. + /// + public string? ReasonPhrase { get; } + + /// + /// Creates an instance of based on an . + /// + /// The response to be used. + /// Cancellation token used on asynchronous calls. + /// An instance of . + public static async Task CreateAsync(HttpResponseMessage response, CancellationToken cancellationToken) + { + Throw.IfNull(response); + +#if NET5_0_OR_GREATER + var rawContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); +#else + cancellationToken.ThrowIfCancellationRequested(); + var rawContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); +#endif + + var responseHeaders = response.Headers.ToDictionary(p => p.Key, p => new StringValues(p.Value.ToArray())); + + foreach (var header in response.Content.Headers) + { + responseHeaders[header.Key] = new StringValues(header.Value.ToArray()); + } + + return new AutoClientHttpError((int)response.StatusCode, responseHeaders, rawContent, response.ReasonPhrase); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/AutoClientOptions.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/AutoClientOptions.cs new file mode 100644 index 0000000000..63a961dcde --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/AutoClientOptions.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; + +namespace Microsoft.Extensions.Http.AutoClient; + +/// +/// Options to configure generated REST API clients. +/// +/// +/// This options class is used to configure generated REST API clients. +/// +/// +/// +/// services.AddMyDependencyClient(options => +/// { +/// options.JsonSerializerOptions = new MyJsonSerializerOptions(); +/// }); +/// +/// +[Experimental] +public class AutoClientOptions +{ + /// + /// Gets or sets JSON payload serialization options. + /// + [Required] + public JsonSerializerOptions JsonSerializerOptions { get; set; } = new(); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/BodyAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/BodyAttribute.cs new file mode 100644 index 0000000000..d6b3d8bf8d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/BodyAttribute.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Http.AutoClient; + +/// +/// Defines the body for the API request. +/// +/// +/// Marks a method parameter as the body for the request. +/// This attribute cannot be used with a GET or HEAD request. +/// +/// +/// +/// [AutoClient("MyClient")] +/// interface IMyDependencyClient +/// { +/// [Post("/api/users")] +/// Task<User> PostUserAsync([Body] User user, CancellationToken cancellationToken); +/// } +/// +/// +[Experimental] +[AttributeUsage(AttributeTargets.Parameter)] +[Conditional("CODE_GENERATION_ATTRIBUTES")] +public sealed class BodyAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// This defaults to a body content type of . + /// + public BodyAttribute() + : this(BodyContentType.ApplicationJson) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The content type to be used on the request content. + public BodyAttribute(BodyContentType contentType) + { + ContentType = contentType; + } + + /// + /// Gets the body content type. + /// + public BodyContentType ContentType { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/BodyContentType.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/BodyContentType.cs new file mode 100644 index 0000000000..93baae2b79 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/BodyContentType.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Http.AutoClient; + +/// +/// Defines the types of encoding possible for request bodies. +/// +[Experimental] +public enum BodyContentType +{ + /// + /// Represents the "application/json" content type. + /// + /// + /// With this content type, the parameter value is serialized to JSON before sending it in the request. + /// + ApplicationJson, + + /// + /// Represents the "text/plain" content type. + /// + /// + /// With this content type, TString is called on the parameter value before sending it in the request. + /// + TextPlain +} diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/HeaderAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/HeaderAttribute.cs new file mode 100644 index 0000000000..48a3d45a67 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/HeaderAttribute.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Http.AutoClient; + +/// +/// Defines a header to be used in the API request. +/// +/// +/// Marks a method parameter as a header to insert in the request. +/// +/// +/// +/// [AutoClient("MyClient")] +/// interface IMyDependencyClient +/// { +/// [Get("/api/users")] +/// Task<string> GetUsersAsync([Header("X-UserName")] string userName, CancellationToken cancellationToken); +/// } +/// +/// +[Experimental] +[AttributeUsage(AttributeTargets.Parameter)] +[Conditional("CODE_GENERATION_ATTRIBUTES")] +public sealed class HeaderAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the header. + public HeaderAttribute(string header) + { + Header = header; + } + + /// + /// Gets the name of the header. + /// + public string Header { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/DeleteAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/DeleteAttribute.cs new file mode 100644 index 0000000000..8b2a2fd473 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/DeleteAttribute.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; + +namespace Microsoft.Extensions.Http.AutoClient; + +/// +/// Defines an API DELETE method. +/// +/// +/// Marks a method within an interface annotated with as an API DELETE method. +/// +/// The return type of an API method must be a Task<T>. +/// If T is a and the dependency returns "text/plain" content type, the result will be the raw content of the response. Otherwise, it will be deserialized from JSON. +/// If T is of type , the result will be the actual response message without further processing. +/// +/// If you provide an extra parameter to the method, you should use it between curly braces in the URL to make it a URL parameter. For example: /api/users/{userId}. +/// +/// +/// +/// [AutoClient("MyClient")] +/// interface IMyDependencyClient +/// { +/// [Delete("/api/users/{userId}")] +/// Task<bool> DeleteUserAsync(string userId, CancellationToken cancellationToken = default); +/// } +/// +/// +[Experimental] +[AttributeUsage(AttributeTargets.Method)] +public sealed class DeleteAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The path of the request. + public DeleteAttribute(string path) + { + Path = path; + } + + /// + /// Gets the path of the request. + /// + public string Path { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/GetAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/GetAttribute.cs new file mode 100644 index 0000000000..577a82321e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/GetAttribute.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; + +namespace Microsoft.Extensions.Http.AutoClient; + +/// +/// Defines an API GET method. +/// +/// +/// Marks a method within an interface annotated with as an API GET method. +/// +/// The return type of an API method must be a Task<T>. +/// If T is a and the dependency returns "text/plain" content type, the result will be the raw content of the response. Otherwise, it will be deserialized from JSON. +/// If T is of type , the result will be the actual response message without further processing. +/// +/// If you provide an extra parameter to the method, you should use it between curly braces in the URL to make it an URL parameter. For example: /api/users/{userId}. +/// +/// +/// +/// [AutoClient("MyClient")] +/// interface IMyDependencyClient +/// { +/// [Get("/api/users/{userId}")] +/// Task<User> GetUserAsync(string userId, [Query] string filter, CancellationToken cancellationToken); +/// } +/// +/// +[Experimental] +[AttributeUsage(AttributeTargets.Method)] +public sealed class GetAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The path of the request. + public GetAttribute(string path) + { + Path = path; + } + + /// + /// Gets the path of the request. + /// + public string Path { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/HeadAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/HeadAttribute.cs new file mode 100644 index 0000000000..e9121a281b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/HeadAttribute.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; + +namespace Microsoft.Extensions.Http.AutoClient; + +/// +/// Defines an API HEAD method. +/// +/// +/// Marks a method within an interface annotated with as an API HEAD method. +/// +/// The return type of an API method must be a Task<T>. +/// If T is a and the dependency returns "text/plain" content type, the result will be the raw content of the response. Otherwise, it will be deserialized from JSON. +/// If T is of type , the result will be the actual response message without further processing. +/// +/// If you provide an extra parameter to the method, you should use it between curly braces in the URL to make it an URL parameter. For example: /api/users/{userId}. +/// +/// +/// +/// [AutoClient("MyClient")] +/// interface IMyDependencyClient +/// { +/// [Head("/api/users/{userId}")] +/// Task<UserHead> HeadUserAsync(string userId, CancellationToken cancellationToken); +/// } +/// +/// +[Experimental] +[AttributeUsage(AttributeTargets.Method)] +public sealed class HeadAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The path of the request. + public HeadAttribute(string path) + { + Path = path; + } + + /// + /// Gets the path of the request. + /// + public string Path { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/OptionsAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/OptionsAttribute.cs new file mode 100644 index 0000000000..de23fa8d0f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/OptionsAttribute.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; + +namespace Microsoft.Extensions.Http.AutoClient; + +/// +/// Defines an API OPTIONS method. +/// +/// +/// Marks a method within an interface annotated with as an API OPTIONS method. +/// +/// The return type of an API method must be a Task<T>. +/// If T is a and the dependency returns "text/plain" content type, the result will be the raw content of the response. Otherwise, it will be deserialized from JSON. +/// If T is of type , the result will be the actual response message without further processing. +/// +/// If you provide an extra parameter to the method, you should use it between curly braces in the URL to make it an URL parameter. For example: /api/users/{userId}. +/// +/// +/// +/// [AutoClient("MyClient")] +/// interface IMyDependencyClient +/// { +/// [Options("/api/users/{userId}")] +/// Task<UserOptions> UserOptionsAsync(string userId, CancellationToken cancellationToken); +/// } +/// +/// +[Experimental] +[AttributeUsage(AttributeTargets.Method)] +public sealed class OptionsAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The path of the request. + public OptionsAttribute(string path) + { + Path = path; + } + + /// + /// Gets the path of the request. + /// + public string Path { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/PatchAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/PatchAttribute.cs new file mode 100644 index 0000000000..04e5803ab8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/PatchAttribute.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; + +namespace Microsoft.Extensions.Http.AutoClient; + +/// +/// Defines an API PATCH method. +/// +/// +/// Marks a method within an interface annotated with as an API PATCH method. +/// +/// The return type of an API method must be a Task<T>. +/// If T is a and the dependency returns "text/plain" content type, the result will be the raw content of the response. Otherwise, it will be deserialized from JSON. +/// If T is of type , the result will be the actual response message without further processing. +/// +/// If you provide an extra parameter to the method, you should use it between curly braces in the URL to make it an URL parameter. For example: /api/users/{userId}. +/// +/// +/// +/// [AutoClient("MyClient")] +/// interface IMyDependencyClient +/// { +/// [Patch("/api/users/{userId}")] +/// Task<User> UpdateUserAsync(string userId, CancellationToken cancellationToken); +/// } +/// +/// +[Experimental] +[AttributeUsage(AttributeTargets.Method)] +public sealed class PatchAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The path of the request. + public PatchAttribute(string path) + { + Path = path; + } + + /// + /// Gets the path of the request. + /// + public string Path { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/PostAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/PostAttribute.cs new file mode 100644 index 0000000000..a42a3f6bb1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/PostAttribute.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; + +namespace Microsoft.Extensions.Http.AutoClient; + +/// +/// Defines an API POST method. +/// +/// +/// Marks a method within an interface annotated with as an API POST method. +/// +/// The return type of an API method must be a Task<T>. +/// If T is a and the dependency returns "text/plain" content type, the result will be the raw content of the response. Otherwise, it will be deserialized from JSON. +/// If T is of type , the result will be the actual response message without further processing. +/// +/// If you provide an extra parameter to the method, you should use it between curly braces in the URL to make it an URL parameter. For example: /api/users/{userId}. +/// +/// +/// +/// [AutoClient("MyClient")] +/// interface IMyDependencyClient +/// { +/// [Post("/api/users/{userId}")] +/// Task<User> AddUserAsync(string userId, CancellationToken cancellationToken); +/// } +/// +/// +[Experimental] +[AttributeUsage(AttributeTargets.Method)] +public sealed class PostAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The path of the request. + public PostAttribute(string path) + { + Path = path; + } + + /// + /// Gets the path of the request. + /// + public string Path { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/PutAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/PutAttribute.cs new file mode 100644 index 0000000000..c7de67fa33 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/Methods/PutAttribute.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; + +namespace Microsoft.Extensions.Http.AutoClient; + +/// +/// Defines an API PUT method. +/// +/// +/// Marks a method within an interface annotated with as an API PUT method. +/// +/// The return type of an API method must be a Task<T>. +/// If T is a and the dependency returns "text/plain" content type, the result will be the raw content of the response. Otherwise, it will be deserialized from JSON. +/// If T is of type , the result will be the actual response message without further processing. +/// +/// If you provide an extra parameter to the method, you should use it between curly braces in the URL to make it an URL parameter. For example: /api/users/{userId}. +/// +/// +/// +/// [AutoClient("MyClient")] +/// interface IMyDependencyClient +/// { +/// [Put("/api/users/{userId}")] +/// Task<User> InsertUserAsync(string userId, CancellationToken cancellationToken); +/// } +/// +/// +[Experimental] +[AttributeUsage(AttributeTargets.Method)] +public sealed class PutAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The path of the request. + public PutAttribute(string path) + { + Path = path; + } + + /// + /// Gets the path of the request. + /// + public string Path { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/Microsoft.Extensions.Http.AutoClient.csproj b/src/Libraries/Microsoft.Extensions.Http.AutoClient/Microsoft.Extensions.Http.AutoClient.csproj new file mode 100644 index 0000000000..5c5b7eca3b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/Microsoft.Extensions.Http.AutoClient.csproj @@ -0,0 +1,38 @@ + + + Microsoft.Extensions.Http.AutoClient + Makes it easy to automatically create efficient and easy to use client code to invoke REST HTTP APIs. + Fundamentals + + + + true + true + + + + normal + 97 + 100 + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/QueryAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/QueryAttribute.cs new file mode 100644 index 0000000000..0b9b9da47d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/QueryAttribute.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Http.AutoClient; + +/// +/// Defines a query string to be used in the API request. +/// +/// +/// Marks a method parameter as a query string for the request. +/// +/// +/// +/// [AutoClient("MyClient")] +/// interface IMyDependencyClient +/// { +/// [Get("/api/users")] +/// Task<string> GetUsersAsync([Query] string userName, [Query("id")] string userId, CancellationToken cancellationToken = default); +/// } +/// +/// +[Experimental] +[AttributeUsage(AttributeTargets.Parameter)] +[Conditional("CODE_GENERATION_ATTRIBUTES")] +public sealed class QueryAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// This overloaded uses the name of the associated method parameter as the query string key. + /// + public QueryAttribute() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The query key to use in the request. + public QueryAttribute(string key) + { + Key = key; + } + + /// + /// Gets the query key, if set. + /// + public string? Key { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/RequestNameAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/RequestNameAttribute.cs new file mode 100644 index 0000000000..e3fbc2a2e8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/RequestNameAttribute.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Http.AutoClient; + +/// +/// Overrides the request name of a REST API method. +/// +/// +/// When this attribute is used on a REST API method, it overrides the request name of that method. +/// If this attribute is not provided, the request name is obtained from the method name. If the method name ends in 'Async', the request name will exclude that. +/// For example, if the method is called GetUsersAsync the request name, by default, will be GetUsers. +/// +/// +/// +/// [AutoClient("MyClient")] +/// interface IMyDependencyClient +/// { +/// [Get("/api/users")] +/// [RequestName("ObtainUsers")] +/// Task<string> GetUsersAsync(CancellationToken cancellationToken = default); +/// } +/// +/// +[Experimental] +[AttributeUsage(AttributeTargets.Method)] +[Conditional("CODE_GENERATION_ATTRIBUTES")] +public sealed class RequestNameAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The name to use for this request within telemetry. + public RequestNameAttribute(string value) + { + Value = value; + } + + /// + /// Gets the value to be used as request name in telemetry. + /// + public string Value { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/StaticHeaderAttribute.cs b/src/Libraries/Microsoft.Extensions.Http.AutoClient/StaticHeaderAttribute.cs new file mode 100644 index 0000000000..774d971626 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/StaticHeaderAttribute.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Http.AutoClient; + +/// +/// Defines a static header to be sent on every API request. +/// +/// +/// Injects a static header to be sent with every request. When this attribute is applied +/// to an interface, then it impacts every method described by the interface. Otherwise, it only +/// applies to the method where it is applied. +/// +/// +/// +/// [AutoClient("MyClient")] +/// [StaticHeader("X-MyHeader", "MyHeaderValue")] +/// interface IMyDependencyClient +/// { +/// [Get("/api/users")] +/// [StaticHeader("X-GetUsersHeader", "Value")] +/// public Task<Users> GetUsers(CancellationToken cancellationToken = default); +/// } +/// +/// +[Experimental] +[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Method, AllowMultiple = true)] +[Conditional("CODE_GENERATION_ATTRIBUTES")] +public sealed class StaticHeaderAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the header. + /// The value of the header. + public StaticHeaderAttribute(string header, string value) + { + Header = header; + Value = value; + } + + /// + /// Gets the name of the header. + /// + public string Header { get; } + + /// + /// Gets the value of the header. + /// + public string Value { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/buildTransitive/Microsoft.Extensions.Http.AutoClient.props b/src/Libraries/Microsoft.Extensions.Http.AutoClient/buildTransitive/Microsoft.Extensions.Http.AutoClient.props new file mode 100644 index 0000000000..7bc91e4438 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/buildTransitive/Microsoft.Extensions.Http.AutoClient.props @@ -0,0 +1,2 @@ + + diff --git a/src/Libraries/Microsoft.Extensions.Http.AutoClient/buildTransitive/Microsoft.Extensions.Http.AutoClient.targets b/src/Libraries/Microsoft.Extensions.Http.AutoClient/buildTransitive/Microsoft.Extensions.Http.AutoClient.targets new file mode 100644 index 0000000000..ceadfacb28 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.AutoClient/buildTransitive/Microsoft.Extensions.Http.AutoClient.targets @@ -0,0 +1,3 @@ + + + diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/HttpClientFaultInjectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/HttpClientFaultInjectionExtensions.cs new file mode 100644 index 0000000000..f94df64d78 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/HttpClientFaultInjectionExtensions.cs @@ -0,0 +1,222 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Http.Resilience.FaultInjection.Internal; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.FaultInjection; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection; + +/// +/// Provides extension methods for Fault-Injection library specifically for HttpClient usages. +/// +public static class HttpClientFaultInjectionExtensions +{ + /// + /// Registers default implementations for , and ; + /// adds fault-injection policies to all . + /// + /// The services collection. + /// + /// The so that additional calls can be chained. + /// + /// + /// All parameters cannot be null. + /// + public static IServiceCollection AddHttpClientFaultInjection(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + Action action = builder => builder.Configure(); + return services.AddHttpClientFaultInjection(action); + } + + /// + /// Configures and registers default implementations for , + /// and ; + /// adds fault-injection policies to all . + /// + /// The services collection. + /// The configuration section to bind to . + /// + /// The so that additional calls can be chained. + /// + /// + /// All parameters cannot be null. + /// + public static IServiceCollection AddHttpClientFaultInjection(this IServiceCollection services, + IConfiguration section) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(section); + + Action action = builder => builder.Configure(section); + return services.AddHttpClientFaultInjection(action); + } + + /// + /// Calls the given action to configure options with and registers default implementations for + /// , and ; + /// adds fault-injection policies to all . + /// + /// The services collection. + /// Action to configure options with . + /// + /// The so that additional calls can be chained. + /// + /// + /// All parameters cannot be null. + /// + /// + /// If the default instance of is used, this method also adds a + /// chaos policy handler to all registered with its name as the identifier. + /// Additional chaos policy handlers with different identifier names can be added using . + /// + public static IServiceCollection AddHttpClientFaultInjection(this IServiceCollection services, + Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + var builder = new HttpFaultInjectionOptionsBuilder(services); + configure.Invoke(builder); + + _ = services.AddFaultInjection(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + // Adds fault-injection for all Http Clients + _ = services.ConfigureAll(options => + { + options.HttpMessageHandlerBuilderActions.Add(builder => + { + var chaosPolicyFactory = builder.Services.GetRequiredService(); + var httpClientChaosPolicyFactory = builder.Services.GetRequiredService(); + var chaosPolicy = httpClientChaosPolicyFactory.CreateHttpResponsePolicy() + .WrapAsync(chaosPolicyFactory.CreateExceptionPolicy()) + .WrapAsync(chaosPolicyFactory.CreateLatencyPolicy()); + + builder.AdditionalHandlers.Add( + ActivatorUtilities.CreateInstance(builder.Services, + builder.Name!)); + builder.AdditionalHandlers.Add(new PolicyHttpMessageHandler(chaosPolicy)); + }); + }); + + return services; + } + + /// + /// Adds a chaos policy handler identified by the chaos policy options group name to the given . + /// + /// The . + /// The chaos policy options group name. + /// + /// The so that additional calls can be chained. + /// + public static IHttpClientBuilder AddFaultInjectionPolicyHandler(this IHttpClientBuilder httpClientBuilder, + string chaosPolicyOptionsGroupName) + { + _ = Throw.IfNull(httpClientBuilder); + _ = Throw.IfNullOrEmpty(chaosPolicyOptionsGroupName); + + _ = httpClientBuilder.AddHttpMessageHandler(services => + { + return ActivatorUtilities.CreateInstance(services, + chaosPolicyOptionsGroupName); + }); + + return AddChaosMessageHandler(httpClientBuilder); + } + + /// + /// Adds a chaos policy handler to the given + /// using weight assignments denoted in to determine which chaos policy options group to + /// use at each run of fault-injection. + /// + /// The . + /// Function to configure . + /// + /// The so that additional calls can be chained. + /// + [Experimental] + public static IHttpClientBuilder AddWeightedFaultInjectionPolicyHandlers(this IHttpClientBuilder httpClientBuilder, + Action weightAssignmentsConfig) + { + _ = Throw.IfNull(httpClientBuilder); + _ = Throw.IfNull(weightAssignmentsConfig); + + _ = httpClientBuilder.Services.Configure(httpClientBuilder.Name, weightAssignmentsConfig); + _ = httpClientBuilder.AddHttpMessageHandler(services => + { + var weightAssignmentsOptions = + services.GetRequiredService>(); + return ActivatorUtilities.CreateInstance(services, + httpClientBuilder.Name, weightAssignmentsOptions); + }); + + return httpClientBuilder.AddChaosMessageHandler(); + } + + /// + /// Adds a chaos policy handler to the given + /// using weight assignments denoted in to determine which chaos policy options group to + /// use at each run of fault-injection. + /// + /// The . + /// The configuration section to bind to . + /// + /// The so that additional calls can be chained. + /// + [Experimental] + [DynamicDependency( + DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, + typeof(FaultPolicyWeightAssignmentsOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed by [DynamicDependency]")] + public static IHttpClientBuilder AddWeightedFaultInjectionPolicyHandlers(this IHttpClientBuilder httpClientBuilder, + IConfigurationSection weightAssignmentsConfigSection) + { + _ = Throw.IfNull(httpClientBuilder); + _ = Throw.IfNull(weightAssignmentsConfigSection); + + _ = httpClientBuilder.Services.Configure(httpClientBuilder.Name, + weightAssignmentsConfigSection); + _ = httpClientBuilder.AddHttpMessageHandler(services => + { + var weightAssignmentsOptions = + services.GetRequiredService>(); + return ActivatorUtilities.CreateInstance(services, + httpClientBuilder.Name, weightAssignmentsOptions); + }); + + return httpClientBuilder.AddChaosMessageHandler(); + } + + private static IHttpClientBuilder AddChaosMessageHandler(this IHttpClientBuilder httpClientBuilder) + { + _ = httpClientBuilder + .AddResilienceHandler("chaos") + .AddPolicy((pipelineBuilder, services) => + { + var chaosPolicyFactory = services.GetRequiredService(); + var httpClientChaosPolicyFactory = services.GetRequiredService(); + _ = pipelineBuilder + .AddPolicy(httpClientChaosPolicyFactory.CreateHttpResponsePolicy()) + .AddPolicy(chaosPolicyFactory.CreateExceptionPolicy()) + .AddPolicy(chaosPolicyFactory.CreateLatencyPolicy()); + }); + + return httpClientBuilder; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/HttpFaultInjectionOptionsBuilder.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/HttpFaultInjectionOptionsBuilder.cs new file mode 100644 index 0000000000..2fbdaa77b8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/HttpFaultInjectionOptionsBuilder.cs @@ -0,0 +1,112 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.FaultInjection.Internal; +using Microsoft.Extensions.Resilience.FaultInjection; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection; + +/// +/// Builder class that inherits to provide options configuration methods for +/// , and . +/// +public class HttpFaultInjectionOptionsBuilder +{ + private readonly FaultInjectionOptionsBuilder _faultInjectionOptionsBuilder; + private readonly IServiceCollection _services; + + /// + /// Initializes a new instance of the class. + /// + /// The services collection. + public HttpFaultInjectionOptionsBuilder(IServiceCollection services) + { + _services = Throw.IfNull(services); + _faultInjectionOptionsBuilder = new FaultInjectionOptionsBuilder(_services); + } + + /// + /// Configures default . + /// + /// + /// The builder object itself so that additional calls can be chained. + /// + public HttpFaultInjectionOptionsBuilder Configure() + { + _ = _faultInjectionOptionsBuilder.Configure(); + return this; + } + + /// + /// Configures through + /// the provided . + /// + /// + /// The configuration section to bind to . + /// + /// The builder object itself so that additional calls can be chained. + /// + /// All parameters cannot be null. + /// + public HttpFaultInjectionOptionsBuilder Configure(IConfiguration section) + { + _ = _faultInjectionOptionsBuilder.Configure(section); + return this; + } + + /// + /// Configures through + /// the provided configure. + /// + /// + /// The function to be registered to configure . + /// + /// The builder object itself so that additional calls can be chained. + /// + /// All parameters cannot be null. + /// + public HttpFaultInjectionOptionsBuilder Configure(Action configureOptions) + { + _ = _faultInjectionOptionsBuilder.Configure(configureOptions); + return this; + } + + /// + /// Add an exception instance to . + /// + /// The identifier for the exception instance to be added. + /// The exception instance to be added. + /// The builder object itself so that additional calls can be chained. + /// + /// The exception cannot be null. + /// + /// + /// The key must not be an empty string or null. + /// + public HttpFaultInjectionOptionsBuilder AddException(string key, Exception exception) + { + _ = _faultInjectionOptionsBuilder.AddException(key, exception); + return this; + } + + /// + /// Adds a with the provided . + /// + /// The identifier for the options instance to be added. + /// The http content. + /// The builder object itself so that additional calls can be chained. + public HttpFaultInjectionOptionsBuilder AddHttpContent(string key, HttpContent content) + { + _ = Throw.IfNull(content); + _ = Throw.IfNullOrWhitespace(key); + + _ = _services.Configure(key, o => o.HttpContent = content); + + return this; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/IHttpClientChaosPolicyFactory.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/IHttpClientChaosPolicyFactory.cs new file mode 100644 index 0000000000..9ee3b2d23c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/IHttpClientChaosPolicyFactory.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Polly; +using Polly.Contrib.Simmy.Outcomes; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection; + +/// +/// Factory for http response chaos policy creation. +/// +public interface IHttpClientChaosPolicyFactory +{ + /// + /// Creates an async http response fault injection policy with delegate functions + /// to fetch fault injection settings from . + /// + /// + /// An http response fault injection policy, + /// an instance of . + /// + public AsyncInjectOutcomePolicy CreateHttpResponsePolicy(); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/FaultInjectionContextMessageHandler.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/FaultInjectionContextMessageHandler.cs new file mode 100644 index 0000000000..c417c91353 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/FaultInjectionContextMessageHandler.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.FaultInjection; +using Polly; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Internal; + +internal sealed class FaultInjectionContextMessageHandler : DelegatingHandler +{ + private readonly string _chaosPolicyOptionsGroupName; + + public FaultInjectionContextMessageHandler(string chaosPolicyOptionsGroupName) + { + _chaosPolicyOptionsGroupName = chaosPolicyOptionsGroupName; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var context = request.GetPolicyExecutionContext(); + + if (context == null) + { + context = new Context(); + request.SetPolicyExecutionContext(context); + } + + _ = context + .WithCallingRequestMessage(request) + .WithFaultInjection(_chaosPolicyOptionsGroupName); + + return base.SendAsync(request, cancellationToken); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/FaultInjectionEventMeterDimensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/FaultInjectionEventMeterDimensions.cs new file mode 100644 index 0000000000..73168d5b8f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/FaultInjectionEventMeterDimensions.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Internal; + +/// +/// FaultInjection event metric counter key names. +/// +internal static class FaultInjectionEventMeterDimensions +{ + /// + /// Client using fault injection library. + /// + public const string FaultInjectionGroupName = "FaultInjectionGroupName"; + + /// + /// Type of fault injected, e.g, latency, exception, etc. + /// + public const string FaultType = "FaultType"; + + /// + /// Value corresponding to injected fault, e.g., NotFonudException, 200ms. + /// + public const string InjectedValue = "InjectedValue"; + + /// + /// The http content key. + /// + public const string HttpContentKey = "HttpContentKey"; +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/FaultInjectionTelemetryHandler.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/FaultInjectionTelemetryHandler.cs new file mode 100644 index 0000000000..c3939b0331 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/FaultInjectionTelemetryHandler.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Internal; + +internal static class FaultInjectionTelemetryHandler +{ + private const string NotApplicable = "N/A"; + + public static void LogAndMeter( + ILogger logger, HttpClientFaultInjectionMetricCounter counter, string groupName, string faultType, string injectedValue, string? httpContentKey) + { + httpContentKey ??= NotApplicable; + + Log.LogInjection(logger, groupName, faultType, injectedValue, httpContentKey); + counter.Add(1, groupName, faultType, injectedValue, httpContentKey); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/FaultInjectionWeightAssignmentContextMessageHandler.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/FaultInjectionWeightAssignmentContextMessageHandler.cs new file mode 100644 index 0000000000..a9b5726804 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/FaultInjectionWeightAssignmentContextMessageHandler.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.FaultInjection; +using Polly; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Internal; + +internal sealed class FaultInjectionWeightAssignmentContextMessageHandler : DelegatingHandler +{ + private readonly FaultPolicyWeightAssignmentsOptions _weightAssignmentOptions; + + public FaultInjectionWeightAssignmentContextMessageHandler(string httpClientName, IOptionsMonitor weightAssignmentOptions) + { + _weightAssignmentOptions = weightAssignmentOptions.Get(httpClientName); + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var context = request.GetPolicyExecutionContext(); + + if (context == null) + { + context = new Context(); + request.SetPolicyExecutionContext(context); + } + + _ = context + .WithCallingRequestMessage(request) + .WithFaultInjection(_weightAssignmentOptions); + + return base.SendAsync(request, cancellationToken); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/HttpClientChaosPolicyFactory.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/HttpClientChaosPolicyFactory.cs new file mode 100644 index 0000000000..4947e1b213 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/HttpClientChaosPolicyFactory.cs @@ -0,0 +1,138 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Resilience.FaultInjection; +using Microsoft.Extensions.Telemetry.Metering; +using Polly; +using Polly.Contrib.Simmy; +using Polly.Contrib.Simmy.Outcomes; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Internal; + +/// +/// Default implementation of . +/// +internal sealed class HttpClientChaosPolicyFactory : IHttpClientChaosPolicyFactory +{ + private const string FaultTypeHttpStatus = "HttpStatus"; + + private readonly Task _enabled = Task.FromResult(true); + private readonly Task _notEnabled = Task.FromResult(false); + private readonly Task _noInjectionRate = Task.FromResult(0); + + private readonly ILogger _logger; + private readonly HttpClientFaultInjectionMetricCounter _counter; + private readonly IFaultInjectionOptionsProvider _optionsProvider; + private readonly IHttpContentOptionsRegistry _httpContentOptionsRegistry; + private readonly Func> _getHttpResponseMessageAsync; + private readonly Func> _getEnabledAsync; + private readonly Func> _getInjectionRateAsync; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The meter. + /// The provider of . + /// The registry that contains registered http content options. + public HttpClientChaosPolicyFactory( + ILogger logger, Meter meter, + IFaultInjectionOptionsProvider optionsProvider, IHttpContentOptionsRegistry httpContentOptionsRegistry) + { + _logger = logger; + _counter = Metric.CreateHttpClientFaultInjectionMetricCounter(meter); + _optionsProvider = optionsProvider; + _httpContentOptionsRegistry = httpContentOptionsRegistry; + _getHttpResponseMessageAsync = GetHttpResponseMessageAsync; + _getEnabledAsync = GetEnabledAsync; + _getInjectionRateAsync = GetInjectionRateAsync; + } + + /// + public AsyncInjectOutcomePolicy CreateHttpResponsePolicy() + { + return MonkeyPolicy.InjectResultAsync(with => + with.Result(_getHttpResponseMessageAsync) + .InjectionRate(_getInjectionRateAsync) + .EnabledWhen(_getEnabledAsync)); + } + + /// + /// Fault provider task for . + /// + /// + /// This task only gets executed when HttpResponseInjectionPolicyOptions is defined at the options group and is enabled, + /// as defined in . + /// See how faults are injected at . + /// + internal Task GetHttpResponseMessageAsync(Context context, CancellationToken ct) + { + var groupName = context.GetFaultInjectionGroupName()!; + _ = _optionsProvider.TryGetChaosPolicyOptionsGroup(groupName, out var optionsGroup); + + var statusCode = optionsGroup!.HttpResponseInjectionPolicyOptions!.StatusCode; + var httpContentKey = optionsGroup!.HttpResponseInjectionPolicyOptions.HttpContentKey; + var httpContent = httpContentKey == null ? null : _httpContentOptionsRegistry.GetHttpContent(httpContentKey); + + var response = new HttpResponseMessage(statusCode); + if (httpContent != null) + { + response.Content = httpContent; + } + + response.RequestMessage = context.GetCallingRequestMessage(); + + FaultInjectionTelemetryHandler.LogAndMeter( + _logger, _counter, groupName, + FaultTypeHttpStatus, statusCode.ToInvariantString(), httpContentKey); + + return Task.FromResult(response); + } + + /// + /// Task for checking if fault-injection is enabled from the 's associated chaos policy options. + /// + internal Task GetEnabledAsync(Context context, CancellationToken ct) + { + var groupName = context.GetFaultInjectionGroupName(); + if (groupName == null) + { + return _notEnabled; + } + + _ = _optionsProvider.TryGetChaosPolicyOptionsGroup(groupName, out var optionsGroup); + if (optionsGroup?.HttpResponseInjectionPolicyOptions?.Enabled ?? false) + { + return _enabled; + } + + return _notEnabled; + } + + /// + /// Task for checking the injection rate from the 's associated chaos policy options. + /// + internal Task GetInjectionRateAsync(Context context, CancellationToken ct) + { + var groupName = context.GetFaultInjectionGroupName(); + if (groupName == null) + { + return _noInjectionRate; + } + + _ = _optionsProvider.TryGetChaosPolicyOptionsGroup(groupName, out var optionsGroup); + var opt = optionsGroup?.HttpResponseInjectionPolicyOptions; + if (opt == null) + { + return _noInjectionRate; + } + + return Task.FromResult(opt.FaultInjectionRate); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/HttpContentOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/HttpContentOptions.cs new file mode 100644 index 0000000000..1451f18726 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/HttpContentOptions.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Internal; + +internal sealed class HttpContentOptions +{ + public HttpContent? HttpContent { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/HttpContentOptionsRegistry.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/HttpContentOptionsRegistry.cs new file mode 100644 index 0000000000..bc372b4e4d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/HttpContentOptionsRegistry.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Internal; + +internal sealed class HttpContentOptionsRegistry : IHttpContentOptionsRegistry +{ + private readonly IOptionsMonitor _options; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The instance to retrieve from. + /// + public HttpContentOptionsRegistry(IOptionsMonitor options) + { + _options = options; + } + + /// + public HttpContent? GetHttpContent(string key) + { + return _options.Get(key).HttpContent; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/IHttpContentOptionsRegistry.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/IHttpContentOptionsRegistry.cs new file mode 100644 index 0000000000..51bb9c6c7e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/IHttpContentOptionsRegistry.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Internal; + +/// +/// The interface of a registry class implementation for +/// registration and retrieval. +/// +internal interface IHttpContentOptionsRegistry +{ + /// + /// Get an instance of from a registered by key. + /// + /// The identifier. + /// + /// An instance of from the registered + /// instance identified by the given key. + /// Returns null if the provided key is null or not found. + /// + public HttpContent? GetHttpContent(string key); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/Log.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/Log.cs new file mode 100644 index 0000000000..9302272c2f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/Log.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Internal; + +internal static partial class Log +{ + [LogMethod(0, LogLevel.Information, + "Fault-injection group name: {groupName}. " + + "Fault type: {faultType}. " + + "Injected value: {injectedValue}. " + + "Http content key: {httpContentKey}. ")] + public static partial void LogInjection( + ILogger logger, + string groupName, + string faultType, + string injectedValue, + string httpContentKey); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/Metric.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/Metric.cs new file mode 100644 index 0000000000..0d57b213b2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/Metric.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Metrics; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Internal; + +internal static partial class Metric +{ + [Microsoft.Extensions.Telemetry.Metering.Counter( + FaultInjectionEventMeterDimensions.FaultInjectionGroupName, + FaultInjectionEventMeterDimensions.FaultType, + FaultInjectionEventMeterDimensions.InjectedValue, + FaultInjectionEventMeterDimensions.HttpContentKey, + Name = @"R9\Resilience\FaultInjection\HttpClient\InjectedFaults")] + public static partial HttpClientFaultInjectionMetricCounter CreateHttpClientFaultInjectionMetricCounter(Meter meter); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/PolicyContextExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/PolicyContextExtensions.cs new file mode 100644 index 0000000000..325c2d85a6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/PolicyContextExtensions.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection; + +/// +/// Provides extension methods for . +/// +[Experimental] +public static class PolicyContextExtensions +{ + private const string CallingRequestMessage = "CallingRequestMessage"; + + /// + /// Associates the given instance to the . + /// + /// The context instance. + /// The calling request. + /// + /// The so that additional calls can be chained. + /// + public static Context WithCallingRequestMessage(this Context context, HttpRequestMessage request) + { + _ = Throw.IfNull(context); + _ = Throw.IfNull(request); + + context[CallingRequestMessage] = request; + + return context; + } + + internal static HttpRequestMessage? GetCallingRequestMessage(this Context context) + { + _ = Throw.IfNull(context); + + if (context.TryGetValue(CallingRequestMessage, out var contextObj)) + { + if (contextObj is HttpRequestMessage request) + { + return request; + } + } + + return null; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HedgingEndpointOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HedgingEndpointOptions.cs new file mode 100644 index 0000000000..85f7ab563e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HedgingEndpointOptions.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Options for resilient pipeline of policies assigned to a particular endpoint. It is using three chained layers in this order (from the outermost to the innermost): +/// Bulkhead -> Circuit Breaker -> Attempt Timeout. +/// +public class HedgingEndpointOptions +{ + private static readonly TimeSpan _timeoutInterval = TimeSpan.FromSeconds(10); + + /// + /// Gets or sets the bulkhead options for the endpoint. + /// + /// + /// By default it is initialized with a unique instance of using default properties values. + /// + [Required] + [ValidateObjectMembers] + public HttpBulkheadPolicyOptions BulkheadOptions { get; set; } = new(); + + /// + /// Gets or sets the circuit breaker options for the endpoint. + /// + /// + /// By default it is initialized with a unique instance of using default properties values. + /// + [Required] + [ValidateObjectMembers] + public HttpCircuitBreakerPolicyOptions CircuitBreakerOptions { get; set; } = new(); + + /// + /// Gets or sets the options for the timeout policy applied per each request attempt. + /// + /// + /// By default it is initialized with a unique instance of + /// using a custom of 10 seconds. + /// + [Required] + [ValidateObjectMembers] + public HttpTimeoutPolicyOptions TimeoutOptions { get; set; } = new() + { + TimeoutInterval = _timeoutInterval, + }; +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HedgingHttpClientBuilderExtensions.Standard.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HedgingHttpClientBuilderExtensions.Standard.cs new file mode 100644 index 0000000000..0ed2018440 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HedgingHttpClientBuilderExtensions.Standard.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Resilience.Internal.Routing; +using Microsoft.Extensions.Http.Resilience.Internal.Validators; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Resilience; + +public static partial class HedgingHttpClientBuilderExtensions +{ + private const string StandardHandlerPostfix = "standard-hedging"; + private const string StandardInnerHandlerPostfix = "standard-hedging-endpoint"; + + /// + /// Adds a standard hedging handler which wraps the execution of the request with a standard hedging mechanism. + /// + /// The HTTP client builder. + /// Configures the routing strategy associated with this handler. + /// + /// A builder that can be used to configure the standard hedging behavior. + /// + /// + /// The standard hedging uses a pipeline pool of circuit breakers to ensure that unhealthy endpoints are not hedged against. + /// By default, the selection from pool is based on the URL Authority (scheme + host + port). + /// + /// It is recommended that you configure the way the pipelines are selected by calling 'SelectPipelineByAuthority' extensions on top of returned . + /// + /// See for more details about the policies inside the pipeline. + /// + public static IStandardHedgingHandlerBuilder AddStandardHedgingHandler(this IHttpClientBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + var hedgingBuilder = builder.AddStandardHedgingHandler(); + + configure(hedgingBuilder.RoutingStrategyBuilder); + + return hedgingBuilder; + } + + /// + /// Adds a standard hedging handler which wraps the execution of the request with a standard hedging mechanism. + /// + /// The HTTP client builder. + /// + /// A builder that can be used to configure the standard hedging behavior. + /// + /// + /// The standard hedging uses a pipeline pool of circuit breakers to ensure that unhealthy endpoints are not hedged against. + /// By default, the selection from pool is based on the URL Authority (scheme + host + port). + /// + /// It is recommended that you configure the way the pipelines are selected by calling 'SelectPipelineByAuthority' extensions on top of returned . + /// + /// See for more details about the policies inside the pipeline. + /// + public static IStandardHedgingHandlerBuilder AddStandardHedgingHandler(this IHttpClientBuilder builder) + { + _ = Throw.IfNull(builder); + + var optionsName = builder.Name; + var routingBuilder = new RoutingStrategyBuilder(builder.Name, builder.Services); + + _ = builder.Services.AddValidatedOptions(optionsName); + _ = builder.Services.AddValidatedOptions(optionsName); + _ = builder.Services.AddRequestCloner(); + + // configure outer handler + var outerHandler = builder.AddResilienceHandler(StandardHandlerPostfix); + _ = outerHandler.AddRoutingPolicy(serviceProvider => serviceProvider.GetRoutingFactory(routingBuilder.Name)); + _ = outerHandler.AddRequestMessageSnapshotPolicy(); + _ = outerHandler.AddPolicy((builder, serviceProvider) => + { + var options = GetOptions(serviceProvider); + var hedgedTaskProvider = CreateHedgedTaskProvider(outerHandler.PipelineName); + + _ = builder + .AddTimeoutPolicy(StandardHedgingPolicyNames.TotalRequestTimeout, options.TotalRequestTimeoutOptions) + .AddHedgingPolicy(StandardHedgingPolicyNames.Hedging, hedgedTaskProvider, options.HedgingOptions); + }); + + // configure inner handler + var innerBuilder = builder.AddResilienceHandler(StandardInnerHandlerPostfix); + _ = innerBuilder.SelectPipelineByAuthority(new DataClassification("FIXME", 1)); + _ = innerBuilder.AddPolicy((builder, serviceProvider) => + { + var options = GetOptions(serviceProvider).EndpointOptions; + + _ = builder + .AddBulkheadPolicy(StandardHedgingPolicyNames.Bulkhead, options.BulkheadOptions) + .AddCircuitBreakerPolicy(StandardHedgingPolicyNames.CircuitBreaker, options.CircuitBreakerOptions) + .AddTimeoutPolicy(StandardHedgingPolicyNames.AttemptTimeout, options.TimeoutOptions); + }); + + return new StandardHedgingHandlerBuilder(builder.Name, builder.Services, routingBuilder, innerBuilder); + + HttpStandardHedgingResilienceOptions GetOptions(IServiceProvider serviceProvider) + => serviceProvider.GetRequiredService>().Get(optionsName); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HedgingHttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HedgingHttpClientBuilderExtensions.cs new file mode 100644 index 0000000000..63208dc0eb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HedgingHttpClientBuilderExtensions.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Resilience; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Extension methods for configuring message handlers as part of +/// the message handler pipeline. +/// +public static partial class HedgingHttpClientBuilderExtensions +{ + internal static HedgedTaskProvider CreateHedgedTaskProvider(string pipelineName) + { + var invokerProvider = Internal.ContextExtensions.CreateMessageInvokerProvider(pipelineName); + var routingStrategyProvider = HedgingContextExtensions.CreateRoutingStrategyProvider(pipelineName); + var snapshotProvider = HedgingContextExtensions.CreateRequestMessageSnapshotProvider(pipelineName); + + return (HedgingTaskProviderArguments args, out Task? result) => + { + // retrieve active routing strategy that was attached by RoutingPolicy + var strategy = routingStrategyProvider(args.Context)!; + if (!strategy.TryGetNextRoute(out var route)) + { + result = null; + + // Stryker disable once Boolean + return false; + } + + var snapshot = snapshotProvider(args.Context)!; + var request = snapshot.Create(); + request.RequestUri = request.RequestUri!.ReplaceHost(route); + var invoker = invokerProvider(args.Context)!; + + // send cloned request to a inner delegating handler + result = invoker.SendAsync(request, args.CancellationToken); + + return true; + }; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpClientHedgingResiliencePredicates.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpClientHedgingResiliencePredicates.cs new file mode 100644 index 0000000000..62c3102b11 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpClientHedgingResiliencePredicates.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Diagnostics; +using Polly.CircuitBreaker; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Static predicates used within the current package. +/// +public static class HttpClientHedgingResiliencePredicates +{ + /// + /// Determines whether an exception should be treated by hedging as a transient failure. + /// + public static readonly Predicate IsTransientHttpException = exception => + { + _ = Throw.IfNull(exception); + + return exception switch + { + BrokenCircuitException => true, + _ when HttpClientResiliencePredicates.IsTransientHttpException(exception) => true, + _ => false, + }; + }; +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpHedgingPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpHedgingPolicyOptions.cs new file mode 100644 index 0000000000..d619e0fff6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpHedgingPolicyOptions.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Implementation of the for results. +/// +public class HttpHedgingPolicyOptions : HedgingPolicyOptions +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// By default the options is set to handle only transient failures, + /// i.e. timeouts, 5xx responses and exceptions. + /// + public HttpHedgingPolicyOptions() + { + ShouldHandleResultAsError = HttpClientResiliencePredicates.IsTransientHttpFailure; + ShouldHandleException = HttpClientHedgingResiliencePredicates.IsTransientHttpException; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpStandardHedgingResilienceOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpStandardHedgingResilienceOptions.cs new file mode 100644 index 0000000000..b18a420d06 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpStandardHedgingResilienceOptions.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Options for resilient pipeline of policies for usage in hedging HTTP scenarios. It is using 5 chained layers in this order (from the outermost to the innermost): +/// Total Request Timeout -> Hedging -> Bulkhead (per endpoint) -> Circuit Breaker (per endpoint) -> Attempt Timeout (per endpoint). +/// +/// /// +/// The configuration of each policy is initialized with the default options per type. The request goes through these policies: +/// +/// 1. Total request timeout policy applies an overall timeout to the execution, ensuring that the request including hedging attempts does not exceed the configured limit. +/// 2. The hedging policy executes the requests against multiple endpoints in case the dependency is slow or returns a transient error. +/// 3. The bulkhead policy limits the maximum number of concurrent requests being send to the dependency. +/// 4. The circuit breaker blocks the execution if too many direct failures or timeouts are detected. +/// 5. The attempt timeout policy limits each request attempt duration and throws if its exceeded. +/// +/// The last three policies are assigned to each individual endpoint. The selection of endpoint can be customized by +/// or +/// extensions. +/// +/// By default, the endpoint is selected by authority (scheme + host + port). +/// +public class HttpStandardHedgingResilienceOptions +{ + /// + /// Gets or sets the timeout policy options for the total timeout applied on the request execution. + /// + /// + /// By default it is initialized with a unique instance of + /// using default properties values. + /// + [Required] + [ValidateObjectMembers] + public HttpTimeoutPolicyOptions TotalRequestTimeoutOptions { get; set; } = new(); + + /// + /// Gets or sets the hedging policy options. + /// + /// + /// By default it is initialized with a unique instance of using default properties values. + /// + [Required] + [ValidateObjectMembers] + public HttpHedgingPolicyOptions HedgingOptions { get; set; } = new(); + + /// + /// Gets or sets the hedging endpoint options. + /// + /// + /// By default it is initialized with a unique instance of using default properties values. + /// + [Required] + [ValidateObjectMembers] + public HedgingEndpointOptions EndpointOptions { get; set; } = new HedgingEndpointOptions(); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/IStandardHedgingHandlerBuilder.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/IStandardHedgingHandlerBuilder.cs new file mode 100644 index 0000000000..066508b55b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/IStandardHedgingHandlerBuilder.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Defines the builder used to configure the standard hedging handler. +/// +public interface IStandardHedgingHandlerBuilder +{ + /// + /// Gets the name of standard hedging handler being configured. + /// + string Name { get; } + + /// + /// Gets the service collection. + /// + IServiceCollection Services { get; } + + /// + /// Gets the builder for the routing strategy. + /// + IRoutingStrategyBuilder RoutingStrategyBuilder { get; } + + /// + /// Gets for endpoint pipeline. + /// + /// This property is for internal use only. + [EditorBrowsable(EditorBrowsableState.Never)] + IHttpResiliencePipelineBuilder EndpointResiliencePipelineBuilder { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HedgingConstants.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HedgingConstants.cs new file mode 100644 index 0000000000..8bd9f6fc92 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HedgingConstants.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +internal static class HedgingConstants +{ + public const string DeprecatedMessage = "Deprecated since 1.23.0 and will be removed in 1.32.0. " + + "Use standard hedging instead. If something prevents you from switching to standard hedging contact R9 team with your scenario " + + "and we can either extend the standard hedging or postpone the deletion of this API and making it official."; +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HedgingContextExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HedgingContextExtensions.cs new file mode 100644 index 0000000000..f237ef1555 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HedgingContextExtensions.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +internal static class HedgingContextExtensions +{ + private const string RoutingStrategyKey = "Hedging.RoutingStrategy"; + private const string SnapshotKey = "Hedging.RequestMessageSnapshot"; + + internal static Func CreateRequestMessageSnapshotProvider(string pipelineName) + { + _ = Throw.IfNullOrEmpty(pipelineName); + + var key = $"{SnapshotKey}-{pipelineName}"; + + return (context) => + { + if (context.TryGetValue(key, out var val)) + { + return (IHttpRequestMessageSnapshot)val; + } + + return null; + }; + } + + internal static Action CreateRequestMessageSnapshotSetter(string pipelineName) + { + var key = $"{SnapshotKey}-{pipelineName}"; + + return (context, snapshot) => context[key] = snapshot; + } + + internal static Func CreateRoutingStrategyProvider(string pipelineName) + { + _ = Throw.IfNullOrEmpty(pipelineName); + + var key = $"{RoutingStrategyKey}-{pipelineName}"; + + return (context) => + { + if (context.TryGetValue(key, out var val)) + { + return (IRequestRoutingStrategy)val; + } + + return null; + }; + } + + internal static Action CreateRoutingStrategySetter(string pipelineName) + { + var key = $"{RoutingStrategyKey}-{pipelineName}"; + + return (context, invoker) => context[key] = invoker; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HttpResiliencePipelineBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HttpResiliencePipelineBuilderExtensions.cs new file mode 100644 index 0000000000..5ec1cecd32 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HttpResiliencePipelineBuilderExtensions.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.Internal.Routing; +using Microsoft.Extensions.Resilience.Internal; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +internal static class HttpResiliencePipelineBuilderExtensions +{ + public static IHttpResiliencePipelineBuilder AddRequestMessageSnapshotPolicy(this IHttpResiliencePipelineBuilder builder) + { + var pipelineName = builder.PipelineName; + + _ = builder.AddPolicy((builder, serviceProvider) => builder.AddPolicy(ActivatorUtilities.CreateInstance(serviceProvider, pipelineName))); + + return builder; + } + + public static IHttpResiliencePipelineBuilder AddRoutingPolicy( + this IHttpResiliencePipelineBuilder builder, + Func factory) + { + var pipelineName = builder.PipelineName; + + _ = builder.AddPolicy((builder, serviceProvider) => builder.AddPolicy(new RoutingPolicy(pipelineName, factory(serviceProvider)))); + + return builder; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/IRandomizer.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/IRandomizer.cs new file mode 100644 index 0000000000..392b56cf1f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/IRandomizer.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +/// +/// Providers thread safe random support for this package. +/// +internal interface IRandomizer +{ + /// + /// Gets the next random double. + /// + /// The max value. + /// The next double. + double NextDouble(double maxValue); + + /// + /// Gets the next random int. + /// + /// The max value. + /// The next int. + int NextInt(int maxValue); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Randomizer.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Randomizer.cs new file mode 100644 index 0000000000..d5ad29836f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Randomizer.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +#pragma warning disable CA5394 // Do not use insecure randomness +#pragma warning disable CPR138 // Random class instances should be shared as statics. + +internal sealed class Randomizer : IRandomizer +{ + private static readonly ThreadLocal _randomInstance = new(() => new Random()); + + public double NextDouble(double maxValue) => _randomInstance.Value!.NextDouble() * maxValue; + + public int NextInt(int maxValue) => _randomInstance.Value!.Next(maxValue); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/RequestMessageSnapshotPolicy.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/RequestMessageSnapshotPolicy.cs new file mode 100644 index 0000000000..b1b9fe8355 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/RequestMessageSnapshotPolicy.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Polly; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +/// +/// This policy creates a snapshot of before executing the hedging to prevent race conditions when cloning and modifying the message at the same time. +/// This way, all hedged requests will have an unique instance of the message available from snapshot without the need to access the original one for cloning. +/// +internal sealed class RequestMessageSnapshotPolicy : AsyncPolicy +{ + private readonly IRequestClonerInternal _requestCloner; + private readonly Func _requestProvider; + private readonly Action _snapshotSetter; + + public RequestMessageSnapshotPolicy(string pipelineName, IRequestClonerInternal requestCloner) + { + _requestCloner = requestCloner; + _requestProvider = ContextExtensions.CreateRequestMessageProvider(pipelineName); + _snapshotSetter = HedgingContextExtensions.CreateRequestMessageSnapshotSetter(pipelineName); + } + + protected override async Task ImplementationAsync( + Func> action, + Context context, + CancellationToken cancellationToken, + bool continueOnCapturedContext) + { + using var snapshot = _requestCloner.CreateSnapshot(_requestProvider(context)!); + _snapshotSetter(context, snapshot); + + return await action(context, cancellationToken).ConfigureAwait(continueOnCapturedContext); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/DefaultRoutingStrategyFactory.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/DefaultRoutingStrategyFactory.cs new file mode 100644 index 0000000000..844add2036 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/DefaultRoutingStrategyFactory.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Routing; + +internal sealed class DefaultRoutingStrategyFactory : IRequestRoutingStrategyFactory + where TRoutingStrategy : IRequestRoutingStrategy +{ + private readonly string _clientId; + private readonly IServiceProvider _serviceProvider; + private readonly ObjectFactory _factory; + + public DefaultRoutingStrategyFactory(string clientId, IServiceProvider serviceProvider) + { + _clientId = clientId; + _serviceProvider = serviceProvider; + _factory = ActivatorUtilities.CreateFactory(typeof(TRoutingStrategy), new[] { typeof(string) }); + } + + public IRequestRoutingStrategy CreateRoutingStrategy() => (IRequestRoutingStrategy)_factory(_serviceProvider, new object[] { _clientId }); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/IPooledRequestRoutingStrategyFactory.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/IPooledRequestRoutingStrategyFactory.cs new file mode 100644 index 0000000000..93c869fcc6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/IPooledRequestRoutingStrategyFactory.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Http.Resilience.Internal.Routing; + +/// +internal interface IPooledRequestRoutingStrategyFactory : IRequestRoutingStrategyFactory +{ + /// + /// Returns the strategy instance to the pool. + /// + /// The strategy instance. + void ReturnRoutingStrategy(IRequestRoutingStrategy strategy); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/OrderedGroups/OrderedGroupsRoutingOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/OrderedGroups/OrderedGroupsRoutingOptionsValidator.cs new file mode 100644 index 0000000000..f69a0cd722 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/OrderedGroups/OrderedGroupsRoutingOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Routing; + +[OptionsValidator] +internal sealed partial class OrderedGroupsRoutingOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/OrderedGroups/OrderedGroupsRoutingStrategy.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/OrderedGroups/OrderedGroupsRoutingStrategy.cs new file mode 100644 index 0000000000..953c9ec387 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/OrderedGroups/OrderedGroupsRoutingStrategy.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Routing; + +internal sealed class OrderedGroupsRoutingStrategy : IRequestRoutingStrategy, IResettable +{ + private readonly IRandomizer _randomizer; + + private int _lastUsedIndex; + private IList? _groups; + + public OrderedGroupsRoutingStrategy(IRandomizer randomizer) + { + _randomizer = randomizer; + } + + public void Initialize(IList groups) + { + _ = TryReset(); + + _groups = groups; + } + + public bool TryGetNextRoute([NotNullWhen(true)] out Uri? nextRoute) + { + if (_groups == null) + { + Throw.InvalidOperationException("The routing strategy is not initialized."); + } + + if (TryGetNextGroup(out var group)) + { + nextRoute = group!.Endpoints.SelectByWeight(e => e.Weight, _randomizer!).Uri!; + return true; + } + + nextRoute = null; + return false; + } + + public bool TryReset() + { + _groups = null; + _lastUsedIndex = 0; + return true; + } + + private bool TryGetNextGroup(out EndpointGroup? nextGroup) + { + if (_lastUsedIndex >= _groups!.Count) + { + nextGroup = null; + return false; + } + + nextGroup = _groups[_lastUsedIndex]; + _lastUsedIndex++; + return true; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/OrderedGroups/OrderedGroupsRoutingStrategyFactory.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/OrderedGroups/OrderedGroupsRoutingStrategyFactory.cs new file mode 100644 index 0000000000..ab67e6d59d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/OrderedGroups/OrderedGroupsRoutingStrategyFactory.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Routing; + +internal sealed class OrderedGroupsRoutingStrategyFactory : PooledRoutingStrategyFactory +{ + public OrderedGroupsRoutingStrategyFactory(string clientId, ObjectPool pool, IOptionsMonitor optionsMonitor) + : base(clientId, pool, optionsMonitor) + { + } + + protected override void Initialize(OrderedGroupsRoutingStrategy strategy, OrderedGroupsRoutingOptions options) => strategy.Initialize(options.Groups); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/PooledRoutingStrategyFactory.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/PooledRoutingStrategyFactory.cs new file mode 100644 index 0000000000..e50673de49 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/PooledRoutingStrategyFactory.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Routing; + +internal abstract class PooledRoutingStrategyFactory : IPooledRequestRoutingStrategyFactory + where T : class, IRequestRoutingStrategy, IResettable +{ + private readonly ObjectPool _pool; + private TOptions _options; + + protected PooledRoutingStrategyFactory(string clientId, ObjectPool pool, IOptionsMonitor optionsMonitor) + { + _pool = pool; + _options = optionsMonitor.Get(clientId); + + _ = optionsMonitor.OnChange((options, name) => + { + if (name == clientId) + { + _options = options; + } + }); + } + + public IRequestRoutingStrategy CreateRoutingStrategy() + { + var strategy = _pool.Get(); + + Initialize(strategy, _options); + + return strategy; + } + + public void ReturnRoutingStrategy(IRequestRoutingStrategy strategy) + { + _ = Throw.IfNull(strategy); + + _pool.Return((T)strategy); + } + + protected abstract void Initialize(T strategy, TOptions options); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/RoutingHelper.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/RoutingHelper.cs new file mode 100644 index 0000000000..589df47167 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/RoutingHelper.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Routing; + +internal static class RoutingHelper +{ + public static T SelectByWeight(this IList endpoints, Func weightProvider, IRandomizer randomizer) + { + var accumulatedProbability = 0d; + var weightSum = 0d; + + for (int i = 0; i < endpoints.Count; i++) + { + weightSum += weightProvider(endpoints[i]); + } + + var randomPercentageValue = randomizer.NextDouble(weightSum); + for (int i = 0; i < endpoints.Count; i++) + { + var endpoint = endpoints[i]; + var weight = weightProvider(endpoint); + + if (randomPercentageValue <= weight + accumulatedProbability) + { + return endpoint; + } + + accumulatedProbability += weight; + } + + throw new InvalidOperationException($"The item cannot be selected because the weights are not correctly calculated."); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/RoutingPolicy.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/RoutingPolicy.cs new file mode 100644 index 0000000000..879fc0ffe9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/RoutingPolicy.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Routing; + +/// +/// Adds routing support to an inner policy. +/// +internal sealed class RoutingPolicy : AsyncPolicy +{ + private readonly IRequestRoutingStrategyFactory _factory; + private readonly Func _requestProvider; + private readonly Action _routingStrategySetter; + + public RoutingPolicy(string pipelineName, IRequestRoutingStrategyFactory factory) + { + _factory = factory; + _requestProvider = ContextExtensions.CreateRequestMessageProvider(pipelineName); + _routingStrategySetter = HedgingContextExtensions.CreateRoutingStrategySetter(pipelineName); + } + + protected override async Task ImplementationAsync( + Func> action, + Context context, + CancellationToken cancellationToken, + bool continueOnCapturedContext) + { + var strategy = _factory.CreateRoutingStrategy(); + + // if there are not routes we cannot continue + if (!strategy.TryGetNextRoute(out var route)) + { + Throw.InvalidOperationException("The routing strategy did not provide any route URL on the first attempt."); + } + + _routingStrategySetter(context, strategy); + + var request = _requestProvider(context)!; + + // for primary request, use retrieved route + request.RequestUri = request.RequestUri!.ReplaceHost(route!); + + try + { + return await action(context, cancellationToken).ConfigureAwait(false); + } + finally + { + if (_factory is IPooledRequestRoutingStrategyFactory pooled) + { + pooled.ReturnRoutingStrategy(strategy); + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/RoutingStrategyBuilder.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/RoutingStrategyBuilder.cs new file mode 100644 index 0000000000..3b0cd9c7c1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/RoutingStrategyBuilder.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Routing; + +internal sealed class RoutingStrategyBuilder : IRoutingStrategyBuilder +{ + public RoutingStrategyBuilder(string name, IServiceCollection services) + { + Name = name; + Services = services; + } + + public string Name { get; } + + public IServiceCollection Services { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/WeightedGroups/WeightedGroupsRoutingOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/WeightedGroups/WeightedGroupsRoutingOptionsValidator.cs new file mode 100644 index 0000000000..34659cd2bc --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/WeightedGroups/WeightedGroupsRoutingOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Routing; + +[OptionsValidator] +internal sealed partial class WeightedGroupsRoutingOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/WeightedGroups/WeightedGroupsRoutingStrategy.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/WeightedGroups/WeightedGroupsRoutingStrategy.cs new file mode 100644 index 0000000000..e770ea3d35 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/WeightedGroups/WeightedGroupsRoutingStrategy.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Routing; + +internal sealed class WeightedGroupsRoutingStrategy : IRequestRoutingStrategy, IResettable +{ + private readonly IRandomizer _randomizer; + private readonly List _groups; + private bool _initialGroupPicked; + private WeightedGroupSelectionMode _mode; + private bool _initialized; + + public WeightedGroupsRoutingStrategy(IRandomizer randomizer) + { + _randomizer = randomizer; + _groups = new List(); + } + + public void Initialize(IEnumerable groups, WeightedGroupSelectionMode mode) + { + _ = TryReset(); + + _initialized = true; + _mode = mode; + _groups.AddRange(groups); + } + + public bool TryReset() + { + _initialized = false; + _mode = WeightedGroupSelectionMode.EveryAttempt; + _groups.Clear(); + _initialGroupPicked = false; + return true; + } + + public bool TryGetNextRoute([NotNullWhen(true)] out Uri? nextRoute) + { + if (!_initialized) + { + Throw.InvalidOperationException("The routing strategy is not initialized."); + } + + if (TryGetNextGroup(out var group)) + { + nextRoute = group!.Endpoints.SelectByWeight(e => e.Weight, _randomizer).Uri!; + return true; + } + + nextRoute = null; + return false; + } + + private bool TryGetNextGroup(out WeightedEndpointGroup? nextGroup) + { + if (_groups.Count == 0) + { + nextGroup = null; + return false; + } + + nextGroup = PickGroup(); + _ = _groups.Remove(nextGroup); + return true; + } + + private WeightedEndpointGroup PickGroup() + { + if (!_initialGroupPicked) + { + _initialGroupPicked = true; + return _groups.SelectByWeight(g => g.Weight, _randomizer); + } + + if (_mode == WeightedGroupSelectionMode.InitialAttempt) + { + return _groups[0]; + } + else + { + return _groups.SelectByWeight(g => g.Weight, _randomizer); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/WeightedGroups/WeightedGroupsRoutingStrategyFactory.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/WeightedGroups/WeightedGroupsRoutingStrategyFactory.cs new file mode 100644 index 0000000000..85da1e12ab --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Routing/WeightedGroups/WeightedGroupsRoutingStrategyFactory.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Routing; + +internal sealed class WeightedGroupsRoutingStrategyFactory : PooledRoutingStrategyFactory +{ + public WeightedGroupsRoutingStrategyFactory(string clientId, ObjectPool pool, IOptionsMonitor optionsMonitor) + : base(clientId, pool, optionsMonitor) + { + } + + protected override void Initialize(WeightedGroupsRoutingStrategy strategy, WeightedGroupsRoutingOptions options) => strategy.Initialize(options.Groups, options.SelectionMode); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/StandardHedgingHandlerBuilder.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/StandardHedgingHandlerBuilder.cs new file mode 100644 index 0000000000..f11a3b762a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/StandardHedgingHandlerBuilder.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Routing; + +internal sealed class StandardHedgingHandlerBuilder : IStandardHedgingHandlerBuilder +{ + public StandardHedgingHandlerBuilder(string name, IServiceCollection services, IRoutingStrategyBuilder routingStrategyBuilder, IHttpResiliencePipelineBuilder endpointResiliencePipelineBuilder) + { + Name = name; + Services = services; + RoutingStrategyBuilder = routingStrategyBuilder; + EndpointResiliencePipelineBuilder = endpointResiliencePipelineBuilder; + } + + public string Name { get; } + + public IServiceCollection Services { get; } + + public IRoutingStrategyBuilder RoutingStrategyBuilder { get; } + + public IHttpResiliencePipelineBuilder EndpointResiliencePipelineBuilder { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/StandardHedgingPolicyNames.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/StandardHedgingPolicyNames.cs new file mode 100644 index 0000000000..c37b827772 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/StandardHedgingPolicyNames.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Http.Resilience.Internal.Validators; + +internal static class StandardHedgingPolicyNames +{ + public const string CircuitBreaker = "StandardHedging-CircuitBreaker"; + + public const string Bulkhead = "StandardHedging-Bulkhead"; + + public const string Hedging = "StandardHedging-Hedging"; + + public const string TotalRequestTimeout = "StandardHedging-TotalRequestTimeout"; + + public const string AttemptTimeout = "StandardHedging-AttemptTimeout"; +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Validators/HttpStandardHedgingResilienceOptionsCustomValidator.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Validators/HttpStandardHedgingResilienceOptionsCustomValidator.cs new file mode 100644 index 0000000000..f5370aade5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Validators/HttpStandardHedgingResilienceOptionsCustomValidator.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Validators; + +internal sealed class HttpStandardHedgingResilienceOptionsCustomValidator : IValidateOptions +{ + private const int CircuitBreakerTimeoutMultiplier = 2; + private readonly INamedServiceProvider _namedServiceProvider; + + public HttpStandardHedgingResilienceOptionsCustomValidator(INamedServiceProvider namedServiceProvider) + { + _namedServiceProvider = namedServiceProvider; + } + + public ValidateOptionsResult Validate(string? name, HttpStandardHedgingResilienceOptions options) + { + var builder = new ValidateOptionsResultBuilder(); + + if (_namedServiceProvider.GetService(name!) is null) + { + builder.AddError($"The hedging routing is not configured for '{name}' HTTP client."); + } + + if (options.EndpointOptions.TimeoutOptions.TimeoutInterval > options.TotalRequestTimeoutOptions.TimeoutInterval) + { + builder.AddError($"Total request timeout policy must have a greater timeout than the attempt timeout policy. " + + $"Total Request Timeout: {options.TotalRequestTimeoutOptions.TimeoutInterval.TotalSeconds}s, " + + $"Attempt Timeout: {options.EndpointOptions.TimeoutOptions.TimeoutInterval.TotalSeconds}s"); + } + + var timeout = TimeSpan.FromMilliseconds(options.EndpointOptions.TimeoutOptions.TimeoutInterval.TotalMilliseconds * CircuitBreakerTimeoutMultiplier); + if (options.EndpointOptions.CircuitBreakerOptions.SamplingDuration < timeout) + { + builder.AddError("The sampling duration of circuit breaker policy needs to be at least double of " + + $"an attempt timeout policy’s timeout interval, in order to be effective. " + + $"Sampling Duration: {options.EndpointOptions.CircuitBreakerOptions.SamplingDuration.TotalSeconds}s," + + $"Attempt Timeout: {options.EndpointOptions.TimeoutOptions.TimeoutInterval.TotalSeconds}s"); + } + + // if generator is specified we cannot calculate the max hedging delay + if (options.HedgingOptions.HedgingDelayGenerator == null) + { + var maxHedgingDelay = TimeSpan.FromMilliseconds((options.HedgingOptions.MaxHedgedAttempts - 1) * options.HedgingOptions.HedgingDelay.TotalMilliseconds); + + // Stryker disable once Equality + if (maxHedgingDelay > options.TotalRequestTimeoutOptions.TimeoutInterval) + { + builder.AddError($"The cumulative delay of the hedging policy is larger than total request timeout interval. " + + $"Total Request Timeout: {options.TotalRequestTimeoutOptions.TimeoutInterval.TotalSeconds}s, " + + $"Cumulative Hedging Delay: {maxHedgingDelay.TotalSeconds}s"); + } + } + + return builder.Build(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Validators/HttpStandardHedgingResilienceOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Validators/HttpStandardHedgingResilienceOptionsValidator.cs new file mode 100644 index 0000000000..d9c81573d8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Validators/HttpStandardHedgingResilienceOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Validators; + +[OptionsValidator] +internal sealed partial class HttpStandardHedgingResilienceOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/Endpoint.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/Endpoint.cs new file mode 100644 index 0000000000..27eede3b87 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/Endpoint.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Extensions.Http.Resilience; + +#pragma warning disable IDE0032 // Use auto property + +/// +/// Represents an URI based endpoint. +/// +public class Endpoint +{ + private Uri? _uri; + + /// + /// Gets or sets the URL of the endpoint. + /// + /// + /// Only schema, domain name and, port will be used, rest of the URL is constructed from request URL. + /// + [Required] + public Uri? Uri + { + get => _uri; + set => _uri = value; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/EndpointGroup.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/EndpointGroup.cs new file mode 100644 index 0000000000..12a43a3959 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/EndpointGroup.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Represents a collection of . +/// +public class EndpointGroup +{ + /// + /// Gets or sets the endpoints in this endpoint group. + /// + /// + /// By default the endpoints are initialized with an empty list. + /// The client must define the endpoint for each endpoint group. + /// At least one endpoint must be defined on each endpoint group in order to performed hedged requests. + /// +#pragma warning disable CA2227 // Collection properties should be read only +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + [Microsoft.Shared.Data.Validation.Length(1)] + [ValidateEnumeratedItems] + public IList Endpoints { get; set; } = new List(); +#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +#pragma warning restore CA2227 // Collection properties should be read only +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/IRequestRoutingStrategy.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/IRequestRoutingStrategy.cs new file mode 100644 index 0000000000..b40c4a6c2f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/IRequestRoutingStrategy.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Defines a strategy for retrieval of route URLs, +/// used to route one request across a set of different endpoints. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public interface IRequestRoutingStrategy +{ + /// + /// Gets the next route Uri. + /// + /// Holds next route value, or . + /// if next route available, otherwise. + [EditorBrowsable(EditorBrowsableState.Never)] + bool TryGetNextRoute([NotNullWhen(true)] out Uri? nextRoute); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/IRequestRoutingStrategyFactory.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/IRequestRoutingStrategyFactory.cs new file mode 100644 index 0000000000..54517521e5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/IRequestRoutingStrategyFactory.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Defines a factory for creation of request routing strategies. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public interface IRequestRoutingStrategyFactory +{ + /// + /// Creates a new instance of . + /// + /// The RequestRoutingStragegy for providing the routes. + [EditorBrowsable(EditorBrowsableState.Never)] + IRequestRoutingStrategy CreateRoutingStrategy(); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/IRoutingStrategyBuilder.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/IRoutingStrategyBuilder.cs new file mode 100644 index 0000000000..0292ad70d5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/IRoutingStrategyBuilder.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Builder for configuring the routing strategies associated with hedging handler. +/// +public interface IRoutingStrategyBuilder +{ + /// + /// Gets the routing strategy name being configured. + /// + string Name { get; } + + /// + /// Gets the service collection. + /// + IServiceCollection Services { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/OrderedGroupsRoutingOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/OrderedGroupsRoutingOptions.cs new file mode 100644 index 0000000000..0b99daabf7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/OrderedGroupsRoutingOptions.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Represents the options for collection of endpoint groups that have fixed order. +/// +/// +/// This strategy picks the endpoint groups in he same order as they are specified in the collection. +/// +public class OrderedGroupsRoutingOptions +{ + /// + /// Gets or sets the collection of ordered endpoints groups. + /// + [Required] + [Microsoft.Shared.Data.Validation.Length(1)] + [ValidateEnumeratedItems] +#pragma warning disable CA2227 // Collection properties should be read only +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + public IList Groups { get; set; } = new List(); +#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +#pragma warning restore CA2227 // Collection properties should be read only +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/RoutingStrategyBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/RoutingStrategyBuilderExtensions.cs new file mode 100644 index 0000000000..122d057733 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/RoutingStrategyBuilderExtensions.cs @@ -0,0 +1,164 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.DependencyInjection.Pools; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Resilience.Internal.Routing; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Resilience; + +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + +/// +/// Extension for . +/// +public static class RoutingStrategyBuilderExtensions +{ + /// + /// Configures ordered groups routing using . + /// + /// The routing builder. + /// The section that the will bind against. + /// + /// The same routing builder instance. + /// + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(OrderedGroupsRoutingOptions))] + public static IRoutingStrategyBuilder ConfigureOrderedGroups(this IRoutingStrategyBuilder builder, IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + + _ = builder.Services.AddPool(); + + return builder.ConfigureRoutingStrategy(options => options.Bind(section)); + } + + /// + /// Configures ordered groups routing using . + /// + /// The routing builder. + /// The callback that configures . + /// + /// The same routing builder instance. + /// + public static IRoutingStrategyBuilder ConfigureOrderedGroups(this IRoutingStrategyBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + return builder.ConfigureOrderedGroups((options, _) => configure(options)); + } + + /// + /// Configures ordered groups routing using . + /// + /// The routing builder. + /// The callback that configures . + /// + /// The same routing builder instance. + /// + [Experimental] + public static IRoutingStrategyBuilder ConfigureOrderedGroups(this IRoutingStrategyBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + _ = builder.Services.AddPool(); + + return builder.ConfigureRoutingStrategy(options => options.Configure(configure)); + } + + /// + /// Configures weighted groups routing using . + /// + /// The routing builder. + /// The section that the will bind against. + /// + /// The same routing builder instance. + /// + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(WeightedGroupsRoutingOptions))] + public static IRoutingStrategyBuilder ConfigureWeightedGroups(this IRoutingStrategyBuilder builder, IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + + _ = builder.Services.AddPool(); + + return builder.ConfigureRoutingStrategy(options => options.Bind(section)); + } + + /// + /// Configures weighted groups routing using . + /// + /// The routing builder. + /// The callback that configures . + /// + /// The same routing builder instance. + /// + public static IRoutingStrategyBuilder ConfigureWeightedGroups(this IRoutingStrategyBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + return builder.ConfigureWeightedGroups((options, _) => configure(options)); + } + + /// + /// Configures weighted groups routing using . + /// + /// The routing builder. + /// The callback that configures . + /// + /// The same routing builder instance. + /// + [Experimental] + public static IRoutingStrategyBuilder ConfigureWeightedGroups(this IRoutingStrategyBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + _ = builder.Services.AddPool(); + + return builder.ConfigureRoutingStrategy(options => options.Configure(configure)); + } + + internal static IRequestRoutingStrategyFactory GetRoutingFactory(this IServiceProvider serviceProvider, string routingName) + { + return serviceProvider.GetRequiredService>().GetRequiredService(routingName); + } + + internal static IRoutingStrategyBuilder ConfigureRoutingStrategy( + this IRoutingStrategyBuilder builder, + Action> configure) + where TRoutingStrategyFactory : class, IRequestRoutingStrategyFactory + where TRoutingStrategyOptions : class + where TRoutingStrategyOptionsValidator : class, IValidateOptions + { + builder.Services.TryAddSingleton(); + + var optionsBuilder = builder.Services.AddValidatedOptions(builder.Name); + configure(optionsBuilder); + + return builder.ConfigureRoutingStrategy(serviceProvider => + { + return (IRequestRoutingStrategyFactory)ActivatorUtilities.CreateInstance(serviceProvider, typeof(TRoutingStrategyFactory), builder.Name); + }); + } + + internal static IRoutingStrategyBuilder ConfigureRoutingStrategy(this IRoutingStrategyBuilder builder, Func factory) + { + builder.Services.TryAddSingleton(); + + _ = builder.Services.AddNamedSingleton(builder.Name, factory); + + return builder; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/WeightedEndpoint.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/WeightedEndpoint.cs new file mode 100644 index 0000000000..30cfc2da83 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/WeightedEndpoint.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Represents an URI based endpoint with a weight assigned. +/// +public class WeightedEndpoint +{ + private const int MinWeight = 1; + private const int MaxWeight = 64000; + private const int DefaultWeight = MaxWeight / 2; + + /// + /// Gets or sets the URL of the endpoint. + /// + /// + /// Only schema, domain name and, port is used, rest of the URL is constructed from request URL. + /// + [Required] + public Uri? Uri { get; set; } + + /// + /// Gets or sets the weight of the endpoint. + /// + /// + /// Default value is 32000. + /// + [Range(MinWeight, MaxWeight)] + public int Weight { get; set; } = DefaultWeight; +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/WeightedEndpointGroup.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/WeightedEndpointGroup.cs new file mode 100644 index 0000000000..c276f71e2a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/WeightedEndpointGroup.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Represents a collection of with a weight assigned. +/// +public class WeightedEndpointGroup : EndpointGroup +{ + private const int MinWeight = 1; + private const int MaxWeight = 64000; + private const int DefaultWeight = MaxWeight / 2; + + /// + /// Gets or sets the weight of the group. + /// + /// + /// Default value is 32000. + /// + [Range(MinWeight, MaxWeight)] + public int Weight { get; set; } = DefaultWeight; +} + diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/WeightedGroupSelectionMode.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/WeightedGroupSelectionMode.cs new file mode 100644 index 0000000000..3eb83935d0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/WeightedGroupSelectionMode.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Represents the selection mode used in . +/// +public enum WeightedGroupSelectionMode +{ + /// + /// In this selection mode the weight is used for every pick of . + /// + EveryAttempt, + + /// + /// In this selection mode the weight is only used to pick initial . + /// Remaining groups are picked in order, starting from the first, finishing with last and skipping already picked group. + /// + InitialAttempt +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/WeightedGroupsRoutingOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/WeightedGroupsRoutingOptions.cs new file mode 100644 index 0000000000..7ce868e494 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Routing/WeightedGroupsRoutingOptions.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Represents the options for collection of endpoint groups that have a weight assigned. +/// +/// +/// This strategy picks the first endpoint group based on its weight and then selects the remaining groups in order, +/// starting from the first one and omitting the one that was already selected. +/// +public class WeightedGroupsRoutingOptions +{ + /// + /// Gets or sets the selection mode that determines the behavior of underlying routing strategy. + /// + public WeightedGroupSelectionMode SelectionMode { get; set; } = WeightedGroupSelectionMode.EveryAttempt; + + /// + /// Gets or sets the collection of weighted endpoints groups. + /// + [Required] + [Microsoft.Shared.Data.Validation.Length(1)] + [ValidateEnumeratedItems] +#pragma warning disable CA2227 // Collection properties should be read only +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + public IList Groups { get; set; } = new List(); +#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +#pragma warning restore CA2227 // Collection properties should be read only +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/StandardHedgingHandlerBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/StandardHedgingHandlerBuilderExtensions.cs new file mode 100644 index 0000000000..101d40b144 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/StandardHedgingHandlerBuilderExtensions.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Extensions for . +/// +public static class StandardHedgingHandlerBuilderExtensions +{ + /// + /// Configures the for the standard hedging pipeline. + /// + /// The pipeline builder. + /// The section that the options will bind against. + /// The same builder instance. + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(HttpStandardHedgingResilienceOptions))] + public static IStandardHedgingHandlerBuilder Configure(this IStandardHedgingHandlerBuilder builder, IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + var options = Throw.IfNull(section.Get()); + + _ = builder.Services.Configure( + builder.Name, + section, +#if NET6_0_OR_GREATER + o => o.ErrorOnUnknownConfiguration = true); +#else + _ => { }); +#endif + + return builder; + } + + /// + /// Configures the for the standard hedging pipeline. + /// + /// The pipeline builder. + /// The configure method. + /// The same builder instance. +#pragma warning disable S3872 // Parameter names should not duplicate the names of their methods + public static IStandardHedgingHandlerBuilder Configure(this IStandardHedgingHandlerBuilder builder, Action configure) +#pragma warning restore S3872 // Parameter names should not duplicate the names of their methods + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + return builder.Configure((options, _) => configure(options)); + } + + /// + /// Configures the for the standard hedging pipeline. + /// + /// The pipeline builder. + /// The configure method. + /// The same builder instance. +#pragma warning disable S3872 // Parameter names should not duplicate the names of their methods + [Experimental] + public static IStandardHedgingHandlerBuilder Configure(this IStandardHedgingHandlerBuilder builder, Action configure) +#pragma warning restore S3872 // Parameter names should not duplicate the names of their methods + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + _ = builder.Services.AddOptions(builder.Name).Configure(configure); + + return builder; + } + + /// + /// Instructs the underlying pipeline builder to select the pipeline instance by redacted authority (scheme + host + port). + /// + /// The builder instance. + /// The data class associated with the authority. + /// The same builder instance. + /// The authority is redacted using retrieved for . + public static IStandardHedgingHandlerBuilder SelectPipelineByAuthority(this IStandardHedgingHandlerBuilder builder, DataClassification classification) + { + _ = Throw.IfNull(builder); + + _ = builder.EndpointResiliencePipelineBuilder.SelectPipelineByAuthority(classification); + + return builder; + } + + /// + /// Instructs the underlying pipeline builder to select the pipeline instance by custom selector. + /// + /// The builder instance. + /// The factory that returns selector. + /// The same builder instance. + /// The pipeline key is used in metrics and logs, do not return any sensitive value. + public static IStandardHedgingHandlerBuilder SelectPipelineBy(this IStandardHedgingHandlerBuilder builder, Func selectorFactory) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(selectorFactory); + + _ = builder.EndpointResiliencePipelineBuilder.SelectPipelineBy(selectorFactory); + + return builder; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj b/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj new file mode 100644 index 0000000000..9a815196b7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj @@ -0,0 +1,47 @@ + + + Microsoft.Extensions.Http.Resilience + Resilience mechanisms for HTTP Client. + Resilience + + + + true + true + true + true + true + true + false + false + false + + + + normal + 100 + 100 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpBulkheadPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpBulkheadPolicyOptions.cs new file mode 100644 index 0000000000..d522e58fc0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpBulkheadPolicyOptions.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Implementation of the for HTTP scenarios. +/// +public class HttpBulkheadPolicyOptions : BulkheadPolicyOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpCircuitBreakerPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpCircuitBreakerPolicyOptions.cs new file mode 100644 index 0000000000..6f316216d7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpCircuitBreakerPolicyOptions.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Implementation of the for results. +/// +public class HttpCircuitBreakerPolicyOptions : CircuitBreakerPolicyOptions +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// By default the options is set to handle only transient failures, + /// i.e. timeouts, 5xx responses and exceptions. + /// + public HttpCircuitBreakerPolicyOptions() + { + ShouldHandleResultAsError = result => HttpClientResiliencePredicates.IsTransientHttpFailure(result); + ShouldHandleException = exp => HttpClientResiliencePredicates.IsTransientHttpException(exp); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpClientResilienceGenerators.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpClientResilienceGenerators.cs new file mode 100644 index 0000000000..6f014c349c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpClientResilienceGenerators.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Static generators used within the current package. +/// +public static class HttpClientResilienceGenerators +{ + /// + /// Gets the generator that is able to generate delay based on the "Retry-After" response header. + /// + public static readonly Func, TimeSpan> HandleRetryAfterHeader = RetryAfterHelper.Generator; +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpClientResiliencePredicates.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpClientResiliencePredicates.cs new file mode 100644 index 0000000000..b50e721edc --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpClientResiliencePredicates.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Net.Http; +using Microsoft.Shared.Diagnostics; +using Polly.Timeout; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Static predicates used within the current package. +/// +public static class HttpClientResiliencePredicates +{ + /// + /// Determines whether an exception should be treated by policies as a transient failure. + /// + public static readonly Predicate IsTransientHttpException = exception => + { + _ = Throw.IfNull(exception); + + return exception is HttpRequestException || + exception is TimeoutRejectedException; + }; + + /// + /// Determines whether a response contains a transient failure. + /// + /// The current handling implementation uses approach proposed by Polly: + /// . + /// + public static readonly Predicate IsTransientHttpFailure = response => + { + _ = Throw.IfNull(response); + + var statusCode = (int)response.StatusCode; + + return statusCode >= InternalServerErrorCode || + response.StatusCode == HttpStatusCode.RequestTimeout || + statusCode == TooManyRequests; + + }; + + private const int InternalServerErrorCode = (int)HttpStatusCode.InternalServerError; + + private const int TooManyRequests = 429; +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpFallbackPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpFallbackPolicyOptions.cs new file mode 100644 index 0000000000..7150ce7cf2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpFallbackPolicyOptions.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Implementation of the for results. +/// +public class HttpFallbackPolicyOptions : FallbackPolicyOptions +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// By default the options is set to handle only transient failures, + /// i.e. timeouts, 5xx responses and exceptions. + /// + public HttpFallbackPolicyOptions() + { + ShouldHandleResultAsError = result => HttpClientResiliencePredicates.IsTransientHttpFailure(result); + ShouldHandleException = exp => HttpClientResiliencePredicates.IsTransientHttpException(exp); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRetryPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRetryPolicyOptions.cs new file mode 100644 index 0000000000..f32c5aa5c6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRetryPolicyOptions.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Implementation of the for results. +/// +public class HttpRetryPolicyOptions : RetryPolicyOptions +{ + private bool _shouldRetryAfterHeader; + + /// + /// Gets or sets a value indicating whether should retry after header. + /// + /// + /// By default the property is set to false. + /// If the property is set to true, then the DelayGenerator will maximize + /// based on the RetryAfter header rules, otherwise it will remain null. + /// + public bool ShouldRetryAfterHeader + { + get => _shouldRetryAfterHeader; + set + { + _shouldRetryAfterHeader = value; + RetryDelayGenerator = _shouldRetryAfterHeader ? HttpClientResilienceGenerators.HandleRetryAfterHeader : null; + } + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// By default the options is set to handle only transient failures, + /// i.e. timeouts, 5xx responses and exceptions. + /// + public HttpRetryPolicyOptions() + { + ShouldHandleResultAsError = result => HttpClientResiliencePredicates.IsTransientHttpFailure(result); + ShouldHandleException = exp => HttpClientResiliencePredicates.IsTransientHttpException(exp); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpTimeoutPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpTimeoutPolicyOptions.cs new file mode 100644 index 0000000000..ee9d1071a8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpTimeoutPolicyOptions.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Implementation of the for HTTP scenarios. +/// +public class HttpTimeoutPolicyOptions : TimeoutPolicyOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/Internal/HttpPolicyFactoryServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/Internal/HttpPolicyFactoryServiceCollectionExtensions.cs new file mode 100644 index 0000000000..cf80be6222 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/Internal/HttpPolicyFactoryServiceCollectionExtensions.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.ExceptionSummarization; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Resilience; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Text; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +/// +/// Extension class for the Service Collection DI container. +/// +[ExcludeFromCodeCoverage] +internal static class HttpPolicyFactoryServiceCollectionExtensions +{ + private static readonly ServiceDescriptor _serviceDescriptor = ServiceDescriptor.Singleton(); + + /// + /// Configures the failure result dimensions that will be emitted for Http failures, by exploring the inner exceptions and their properties. + /// + /// The services. + /// The input . + public static IServiceCollection ConfigureHttpFailureResultContext(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + // don't add any new service if this method is called multiple times + if (services.Contains(_serviceDescriptor)) + { + return services; + } + + services.Add(_serviceDescriptor); + + return services + .AddExceptionSummarizer(b => b.AddHttpProvider()) + .ConfigureFailureResultContext((response) => + { + if (response != null) + { + var statusCodeName = response.StatusCode.ToInvariantString(); + if (string.IsNullOrEmpty(statusCodeName) || char.IsDigit(statusCodeName[0])) + { + statusCodeName = TelemetryConstants.Unknown; + } + + return FailureResultContext.Create(failureReason: ((int)response.StatusCode).ToInvariantString(), additionalInformation: statusCodeName); + } + + return FailureResultContext.Create(); + }); + } + + private sealed class Marker + { + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/Internal/RetryAfterHelper.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/Internal/RetryAfterHelper.cs new file mode 100644 index 0000000000..865fedd406 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/Internal/RetryAfterHelper.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +internal static class RetryAfterHelper +{ + public static TimeSpan Generator(RetryDelayArguments args) + { + if (args.Result?.Result is HttpResponseMessage response) + { + return ParseRetryAfterHeader(response, TimeProvider.System); + } + + return TimeSpan.Zero; + } + + /// + /// Parses Retry-After value from the relevant HTTP response header. + /// If not found then it will return . + /// + /// HTTP response message. + /// Current time provider for conversion of absolute values. + /// The delay according to the Retry-After header. + /// . + internal static TimeSpan ParseRetryAfterHeader(HttpResponseMessage httpResponse, TimeProvider timeProvider) + { + var headers = httpResponse?.Headers; + if (headers?.RetryAfter != null) + { + if (headers.RetryAfter.Date.HasValue) + { + // An absolute point in time + return headers.RetryAfter.Date.Value - timeProvider.GetUtcNow(); + } + else if (headers.RetryAfter.Delta.HasValue) + { + // A relative number of seconds + return headers.RetryAfter.Delta.Value; + } + } + + return TimeSpan.Zero; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/FallbackClientHandlerOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/FallbackClientHandlerOptions.cs new file mode 100644 index 0000000000..35bbe94f96 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/FallbackClientHandlerOptions.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Class for the fallback options definition. +/// +[Experimental(HttpClientBuilderExtensions.FallbackExperimentalMessage)] +public class FallbackClientHandlerOptions +{ + /// + /// Gets or sets the base fallback URI. + /// + [Required] + public Uri? BaseFallbackUri { get; set; } + + /// + /// Gets or sets the fallback policy options. + /// + [ValidateObjectMembers] + public HttpFallbackPolicyOptions FallbackPolicyOptions { get; set; } = new(); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.Fallback.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.Fallback.cs new file mode 100644 index 0000000000..70638245d8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.Fallback.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Resilience; + +public static partial class HttpClientBuilderExtensions +{ + internal const string FallbackExperimentalMessage = "The current form of this API is experimental. " + + "A direct replacement for it will be provided in the follow up versions of the SDK. " + + "If you're a new adopter, consider using the Hedging handler. If you are already using the API, stay tuned for the next release's features."; + + /// + /// Adds a fallback handler that wraps the execution of the request with a fallback mechanism, + /// ensuring that the request is retried against a secondary endpoint. + /// + /// The HTTP client builder. + /// The configure callback. + /// + /// An that can be used to configure the client. + /// + [Experimental(FallbackExperimentalMessage)] + public static IHttpClientBuilder AddFallbackHandler(this IHttpClientBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + return builder.AddFallbackHandlerInternal(null, configure); + } + + /// + /// Adds a fallback handler that wraps the execution of the request with a fallback mechanism, + /// ensuring that the request is retried against a secondary endpoint. + /// + /// The HTTP client builder. + /// The section that the will bind against. + /// + /// An that can be used to configure the client. + /// + [Experimental(FallbackExperimentalMessage)] + public static IHttpClientBuilder AddFallbackHandler(this IHttpClientBuilder builder, IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + + return builder.AddFallbackHandlerInternal(section, null); + } + + /// + /// Adds a fallback handler that wraps the execution of the request with a fallback mechanism, + /// ensuring that the request is retried against a secondary endpoint. + /// + /// The HTTP client builder. + /// The section that the will bind against. + /// The configure callback. + /// + /// An that can be used to configure the client. + /// + [Experimental(FallbackExperimentalMessage)] + public static IHttpClientBuilder AddFallbackHandler(this IHttpClientBuilder builder, IConfigurationSection section, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + _ = Throw.IfNull(configure); + + return builder.AddFallbackHandlerInternal(section, configure); + } + + private static IHttpClientBuilder AddFallbackHandlerInternal(this IHttpClientBuilder builder, IConfigurationSection? section, Action? configure) + { + FallbackHelper.AddFallbackPolicy( + builder.AddResilienceHandler(FallbackHelper.HandlerPostfix), + optionsName: builder.Name, + options => options.Configure(section, configure)); + + return builder; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.Resilience.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.Resilience.cs new file mode 100644 index 0000000000..c8abdf825d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.Resilience.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Resilience; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Http.Resilience; + +public static partial class HttpClientBuilderExtensions +{ + /// + /// Adds a that uses a named inline resilience pipeline configured by returned . + /// + /// The builder instance. + /// The custom identifier for the pipeline, used in the name of the pipeline. + /// The HTTP pipeline builder instance. + /// + /// The final pipeline name is combination of and . + /// Use pipeline identifier if your HTTP client contains multiple resilience handlers. + /// + public static IHttpResiliencePipelineBuilder AddResilienceHandler(this IHttpClientBuilder builder, string pipelineIdentifier) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(pipelineIdentifier); + + var pipelineBuilder = builder.AddHttpResiliencePipeline(pipelineIdentifier); + + _ = builder.AddHttpMessageHandler(serviceProvider => + { + var selector = CreatePipelineSelector(serviceProvider, pipelineBuilder.PipelineName); + return new ResilienceHandler(pipelineBuilder.PipelineName, selector); + }); + + return pipelineBuilder; + } + + private static Func> CreatePipelineSelector(IServiceProvider serviceProvider, string pipelineName) + { + var resilienceProvider = serviceProvider.GetRequiredService(); + var pipelineKeyProvider = serviceProvider.GetPipelineKeyProvider(pipelineName); + + if (pipelineKeyProvider == null) + { + var pipeline = resilienceProvider.GetPipeline(pipelineName); + return _ => pipeline; + } + else + { + TouchPipelineKey(pipelineKeyProvider); + + return request => + { + var pipelineKey = pipelineKeyProvider.GetPipelineKey(request); + return resilienceProvider.GetPipeline(pipelineName, pipelineKey); + }; + } + } + + private static void TouchPipelineKey(IPipelineKeyProvider provider) + { + // this piece of code eagerly checks that the pipeline key provider is correctly configured + // combined with HttpClient auto-activation we can detect any issues on startup + if (provider is ByAuthorityPipelineKeyProvider) + { +#pragma warning disable S1075 // URIs should not be hardcoded - this URL is not used for any real request, nor in any telemetry + using var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:123"); +#pragma warning restore S1075 // URIs should not be hardcoded + _ = provider.GetPipelineKey(request); + } + } + + private static HttpResiliencePipelineBuilder AddHttpResiliencePipeline(this IHttpClientBuilder builder, string pipelineIdentifier) + { + _ = builder.Services.ConfigureHttpFailureResultContext(); + var pipelineName = PipelineNameHelper.GetPipelineName(builder.Name, pipelineIdentifier); + var pipelineBuilder = builder.Services.AddResiliencePipeline(pipelineName); + + return new HttpResiliencePipelineBuilder(pipelineBuilder); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.StandardResilience.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.StandardResilience.cs new file mode 100644 index 0000000000..856b0297f9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.StandardResilience.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Resilience.Internal.Validators; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Resilience; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Resilience; + +public static partial class HttpClientBuilderExtensions +{ + private const string StandardIdentifier = "standard"; + + /// + /// Adds a that uses a standard resilience pipeline with default options to send the requests and handle any transient errors. + /// The pipeline combines multiple policies that are configured based on HTTP-specific options with recommended defaults. + /// + /// The builder instance. + /// The section that the options will bind against. + /// The HTTP pipeline builder instance. + /// + /// See for more details about the individual policies configured by this method. + /// + public static IHttpStandardResiliencePipelineBuilder AddStandardResilienceHandler(this IHttpClientBuilder builder, IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + + return builder.AddStandardResilienceHandler().Configure(section); + } + + /// + /// Adds a that uses a standard resilience pipeline with default options to send the requests and handle any transient errors. + /// The pipeline combines multiple policies that are configured based on HTTP-specific options with recommended defaults. + /// + /// The builder instance. + /// The action that configures the resilience options. + /// The HTTP pipeline builder instance. + /// + /// See for more details about the individual policies configured by this method. + /// + public static IHttpStandardResiliencePipelineBuilder AddStandardResilienceHandler(this IHttpClientBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + return builder.AddStandardResilienceHandler().Configure(configure); + } + + /// + /// Adds a that uses a standard resilience pipeline with default + /// to send the requests and handle any transient errors. + /// The pipeline combines multiple policies that are configured based on HTTP-specific options with recommended defaults. + /// + /// The builder instance. + /// The HTTP pipeline builder instance. + /// + /// See for more details about the individual policies configured by this method. + /// + public static IHttpStandardResiliencePipelineBuilder AddStandardResilienceHandler(this IHttpClientBuilder builder) + { + _ = Throw.IfNull(builder); + + _ = builder.Services.ConfigureHttpFailureResultContext(); + + return new HttpStandardResiliencePipelineBuilder(builder.AddResilienceHandler(StandardIdentifier).AddStandardPipeline()); + } + + private static HttpResiliencePipelineBuilder AddStandardPipeline(this IResiliencePipelineBuilder builder) + { + var resilienceBuilder = + builder.AddPolicy( + builder.PipelineName, + options => { }, + (builder, options, _) => + builder + .AddBulkheadPolicy(StandardPolicyNames.Bulkhead, options.BulkheadOptions) + .AddTimeoutPolicy(StandardPolicyNames.TotalRequestTimeout, options.TotalRequestTimeoutOptions) + .AddRetryPolicy(StandardPolicyNames.Retry, options.RetryOptions) + .AddCircuitBreakerPolicy(StandardPolicyNames.CircuitBreaker, options.CircuitBreakerOptions) + .AddTimeoutPolicy(StandardPolicyNames.AttemptTimeout, options.AttemptTimeoutOptions)); + + _ = builder.Services.AddValidatedOptions(builder.PipelineName); + + return new HttpResiliencePipelineBuilder(resilienceBuilder); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpResiliencePipelineBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpResiliencePipelineBuilderExtensions.cs new file mode 100644 index 0000000000..b03721c219 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpResiliencePipelineBuilderExtensions.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Extensions for . +/// +public static class HttpResiliencePipelineBuilderExtensions +{ + /// + /// Instructs the underlying pipeline builder to select the pipeline instance by redacted authority (scheme + host + port). + /// + /// The builder instance. + /// The data class associated with the authority. + /// The same builder instance. + /// The authority is redacted using retrieved for . + public static IHttpResiliencePipelineBuilder SelectPipelineByAuthority(this IHttpResiliencePipelineBuilder builder, DataClassification classification) + { + _ = Throw.IfNull(builder); + + PipelineKeyProviderHelper.SelectPipelineByAuthority(builder.Services, builder.PipelineName, classification); + + return builder; + } + + /// + /// Instructs the underlying pipeline builder to select the pipeline instance by custom selector. + /// + /// The builder instance. + /// The factory that returns selector. + /// The same builder instance. + /// The pipeline key is used in metrics and logs, do not return any sensitive value. + public static IHttpResiliencePipelineBuilder SelectPipelineBy(this IHttpResiliencePipelineBuilder builder, Func selectorFactory) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(selectorFactory); + + PipelineKeyProviderHelper.SelectPipelineBy(builder.Services, builder.PipelineName, selectorFactory); + + return builder; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpStandardResilienceOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpStandardResilienceOptions.cs new file mode 100644 index 0000000000..968ae1a39d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpStandardResilienceOptions.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Options for resilient pipeline of policies for usage in HTTP scenarios. It is using five chained layers in this order (from the outermost to the innermost): +/// Bulkhead -> Total Request Timeout -> Retry -> Circuit Breaker -> Attempt Timeout. +/// +/// /// +/// The configuration of each policy is initialized with the default options per type. The request goes through these policies: +/// 1. Total request timeout policy applies an overall timeout to the execution, ensuring that the request including hedging attempts does not exceed the configured limit. +/// 2. The retry policy retries the request in case the dependency is slow or returns a transient error. +/// 3. The bulkhead policy limits the maximum number of concurrent requests being send to the dependency. +/// 4. The circuit breaker blocks the execution if too many direct failures or timeouts are detected. +/// 5. The attempt timeout policy limits each request attempt duration and throws if its exceeded. +/// +public class HttpStandardResilienceOptions +{ + private static readonly TimeSpan _attemptTimeoutInterval = TimeSpan.FromSeconds(10); + + /// + /// Gets or sets the bulkhead options. + /// + /// + /// By default it is initialized with a unique instance of using default properties values. + /// + [Required] + [ValidateObjectMembers] + public HttpBulkheadPolicyOptions BulkheadOptions { get; set; } = new(); + + /// + /// Gets or sets the timeout policy options for the total timeout applied on the request's execution. + /// + /// + /// By default it is initialized with a unique instance of + /// using default properties values. + /// + [Required] + [ValidateObjectMembers] + public HttpTimeoutPolicyOptions TotalRequestTimeoutOptions { get; set; } = new(); + + /// + /// Gets or sets the retry policy Options. + /// + /// + /// By default it is initialized with a unique instance of using default properties values. + /// + [Required] + [ValidateObjectMembers] + public HttpRetryPolicyOptions RetryOptions { get; set; } = new(); + + /// + /// Gets or sets the circuit breaker options. + /// + /// + /// By default it is initialized with a unique instance of using default properties values. + /// + [Required] + [ValidateObjectMembers] + public HttpCircuitBreakerPolicyOptions CircuitBreakerOptions { get; set; } = new(); + + /// + /// Gets or sets the options for the timeout policy applied per each request attempt. + /// + /// + /// By default it is initialized with a unique instance of + /// using custom of 10 seconds. + /// + [Required] + [ValidateObjectMembers] + public HttpTimeoutPolicyOptions AttemptTimeoutOptions { get; set; } = new() + { + TimeoutInterval = _attemptTimeoutInterval, + }; +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpStandardResiliencePipelineBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpStandardResiliencePipelineBuilderExtensions.cs new file mode 100644 index 0000000000..25e15a6dd2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpStandardResiliencePipelineBuilderExtensions.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Extensions for . +/// +public static class HttpStandardResiliencePipelineBuilderExtensions +{ + /// + /// Configures the for the standard pipeline. + /// + /// The pipeline builder. + /// The section that the options will bind against. + /// The same builder instance. + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(HttpStandardResilienceOptions))] + public static IHttpStandardResiliencePipelineBuilder Configure(this IHttpStandardResiliencePipelineBuilder builder, IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + var options = Throw.IfNull(section.Get()); + + _ = builder.Services.Configure( + builder.PipelineName, + section, +#if NET6_0_OR_GREATER + o => o.ErrorOnUnknownConfiguration = true); +#else + _ => { }); +#endif + return builder; + } + + /// + /// Configures the for the standard pipeline. + /// + /// The pipeline builder. + /// The configure method. + /// The same builder instance. +#pragma warning disable S3872 // Parameter names should not duplicate the names of their methods + public static IHttpStandardResiliencePipelineBuilder Configure(this IHttpStandardResiliencePipelineBuilder builder, Action configure) +#pragma warning restore S3872 // Parameter names should not duplicate the names of their methods + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + return builder.Configure((options, _) => configure(options)); + } + + /// + /// Configures the for the standard pipeline. + /// + /// The pipeline builder. + /// The configure method. + /// The same builder instance. +#pragma warning disable S3872 // Parameter names should not duplicate the names of their methods + [Experimental] + public static IHttpStandardResiliencePipelineBuilder Configure(this IHttpStandardResiliencePipelineBuilder builder, Action configure) +#pragma warning restore S3872 // Parameter names should not duplicate the names of their methods + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + _ = builder.Services.AddOptions(builder.PipelineName).Configure(configure); + + return builder; + } + + /// + /// Instructs the underlying pipeline builder to select the pipeline instance by redacted authority (scheme + host + port). + /// + /// The builder instance. + /// The data class associated with the authority. + /// The same builder instance. + /// The authority is redacted using retrieved for . + public static IHttpStandardResiliencePipelineBuilder SelectPipelineByAuthority(this IHttpStandardResiliencePipelineBuilder builder, DataClassification classification) + { + _ = Throw.IfNull(builder); + + PipelineKeyProviderHelper.SelectPipelineByAuthority(builder.Services, builder.PipelineName, classification); + + return builder; + } + + /// + /// Instructs the underlying pipeline builder to select the pipeline instance by custom selector. + /// + /// The builder instance. + /// The factory that returns selector. + /// The same builder instance. + /// The pipeline key is used in metrics and logs, do not return any sensitive value. + public static IHttpStandardResiliencePipelineBuilder SelectPipelineBy(this IHttpStandardResiliencePipelineBuilder builder, Func selectorFactory) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(selectorFactory); + + PipelineKeyProviderHelper.SelectPipelineBy(builder.Services, builder.PipelineName, selectorFactory); + + return builder; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpResiliencePipelineBuilder.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpResiliencePipelineBuilder.cs new file mode 100644 index 0000000000..73de8b166a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpResiliencePipelineBuilder.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.Extensions.Resilience; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// The builder for configuring the HTTP client resilience pipeline. +/// +#pragma warning disable S4023 // Interfaces should not be empty +public interface IHttpResiliencePipelineBuilder : IResiliencePipelineBuilder +#pragma warning restore S4023 // Interfaces should not be empty +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpStandardResiliencePipelineBuilder.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpStandardResiliencePipelineBuilder.cs new file mode 100644 index 0000000000..dc4ad8807c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpStandardResiliencePipelineBuilder.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// The builder for the standard HTTP pipeline. +/// +public interface IHttpStandardResiliencePipelineBuilder +{ + /// + /// Gets the name of the pipeline configured by this builder. + /// + string PipelineName { get; } + + /// + /// Gets the application service collection. + /// + IServiceCollection Services { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByAuthorityPipelineKeyProvider.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByAuthorityPipelineKeyProvider.cs new file mode 100644 index 0000000000..e9f7539506 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByAuthorityPipelineKeyProvider.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Net.Http; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +internal sealed class ByAuthorityPipelineKeyProvider : IPipelineKeyProvider +{ + private readonly Redactor _redactor; + private readonly ConcurrentDictionary<(string scheme, string host, int port), string> _cache = new(); + + public ByAuthorityPipelineKeyProvider(Redactor redactor, DataClassification _) + { + _redactor = redactor; + } + + public string GetPipelineKey(HttpRequestMessage requestMessage) + { + var url = requestMessage.RequestUri ?? throw new InvalidOperationException("The request message must have a URL specified."); + + var key = (url.Scheme, url.Host, url.Port); + + // We could use GetOrAdd for simplification but that would force us to allocate the lambda for every call. + if (_cache.TryGetValue(key, out var pipelineKey)) + { + return pipelineKey; + } + + pipelineKey = url.GetLeftPart(UriPartial.Authority); + pipelineKey = _redactor.Redact(pipelineKey); + + if (string.IsNullOrEmpty(pipelineKey)) + { + Throw.InvalidOperationException( + $"The redacted pipeline is an empty string and cannot be used for pipeline selection. Is redaction correctly configured?"); + } + + // sometimes this can be called twice (multiple concurrent requests), but we don't care + _cache[key] = pipelineKey!; + + return pipelineKey!; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByCustomSelectorPipelineKeyProvider.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByCustomSelectorPipelineKeyProvider.cs new file mode 100644 index 0000000000..3e17755ab0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByCustomSelectorPipelineKeyProvider.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +internal sealed class ByCustomSelectorPipelineKeyProvider : IPipelineKeyProvider +{ + private readonly PipelineKeySelector _selector; + + public ByCustomSelectorPipelineKeyProvider(PipelineKeySelector selector) + { + _selector = selector; + } + + public string GetPipelineKey(HttpRequestMessage requestMessage) + { + return _selector(requestMessage); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ContextExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ContextExtensions.cs new file mode 100644 index 0000000000..678463a1c9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ContextExtensions.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Telemetry; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +/// +/// Various extensions for . +/// +internal static class ContextExtensions +{ + private const string RequestMessageKey = "Resilience.ContextExtensions.Request"; + + private const string MessageInvokerKey = "Resilience.ContextExtensions.MessageInvoker"; + + /// + /// Sets the request metadata to the context. + /// + /// The context. + /// The request. + public static void SetRequestMetadata(this Context context, HttpRequestMessage request) + { + _ = Throw.IfNull(request); + _ = Throw.IfNull(context); + + if (!context.ContainsKey(TelemetryConstants.RequestMetadataKey) && request.GetRequestMetadata() is RequestMetadata requestMetadata) + { + context[TelemetryConstants.RequestMetadataKey] = requestMetadata; + } + } + + /// + /// Gets the assigned to the context. + /// + /// A . + public static Func CreateMessageInvokerProvider(string pipelineName) + { + _ = Throw.IfNullOrEmpty(pipelineName); + + var key = $"{MessageInvokerKey}-{pipelineName}"; + + return (context) => + { + if (context.TryGetValue(key, out var val)) + { + return ((Lazy)val).Value; + } + + return null; + }; + } + + /// + /// Gets the assigned to the context. + /// + /// A . + public static Func CreateRequestMessageProvider(string pipelineName) + { + _ = Throw.IfNullOrEmpty(pipelineName); + + var key = $"{RequestMessageKey}-{pipelineName}"; + + return (context) => + { + if (context.TryGetValue(key, out var val)) + { + return (HttpRequestMessage)val; + } + + return null; + }; + } + + internal static Action> CreateMessageInvokerSetter(string pipelineName) + { + var key = $"{MessageInvokerKey}-{pipelineName}"; + + return (context, invoker) => context[key] = invoker; + } + + internal static Action CreateRequestMessageSetter(string pipelineName) + { + var key = $"{RequestMessageKey}-{pipelineName}"; + + return (context, invoker) => context[key] = invoker; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/DefaultRequestCloner.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/DefaultRequestCloner.cs new file mode 100644 index 0000000000..433b0394b3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/DefaultRequestCloner.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +/// +/// Default implementation of interface for cloning requests. +/// +/// +/// The request content is only copied, not deeply cloned. +/// If the request is cloned outside the middlewares, the content must be cloned as well. +/// +internal sealed class DefaultRequestCloner : IRequestClonerInternal +{ + public IHttpRequestMessageSnapshot CreateSnapshot(HttpRequestMessage request) + { + _ = Throw.IfNull(request); + + return new Snapshot(request); + } + + private sealed class Snapshot : IHttpRequestMessageSnapshot + { + private static readonly ObjectPool>>> _headersPool = PoolFactory.CreateListPool>>(); + private static readonly ObjectPool>> _propertiesPool = PoolFactory.CreateListPool>(); + + private readonly HttpMethod _method; + private readonly Uri? _requestUri; + private readonly Version _version; + private readonly HttpContent? _content; + private readonly List>> _headers; + private readonly List> _properties; + + public Snapshot(HttpRequestMessage request) + { + if (request.Content is StreamContent) + { + Throw.InvalidOperationException($"{nameof(StreamContent)} content cannot by cloned using the {nameof(DefaultRequestCloner)}."); + } + + _method = request.Method; + _version = request.Version; + _requestUri = request.RequestUri; + _content = request.Content; + + // headers + _headers = _headersPool.Get(); + _headers.AddRange(request.Headers); + + // props + _properties = _propertiesPool.Get(); +#if NET5_0_OR_GREATER + _properties.AddRange(request.Options); +#else + _properties.AddRange(request.Properties); +#endif + } + + public HttpRequestMessage Create() + { + var clone = new HttpRequestMessage(_method, _requestUri) + { + Content = _content, + Version = _version + }; + +#if NET5_0_OR_GREATER + foreach (var prop in _properties) + { + _ = clone.Options.TryAdd(prop.Key, prop.Value); + } +#else + foreach (var prop in _properties) + { + clone.Properties.Add(prop); + } +#endif + foreach (KeyValuePair> header in _headers) + { + _ = clone.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + return clone; + } + + public void Dispose() + { + _propertiesPool.Return(_properties); + _headersPool.Return(_headers); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/FallbackHelper.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/FallbackHelper.cs new file mode 100644 index 0000000000..df650c91d9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/FallbackHelper.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.Internal.Validators; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience; +using Microsoft.Extensions.Resilience.Internal; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +internal static class FallbackHelper +{ + public const string HandlerPostfix = "fallback-handler"; + + public static void AddFallbackPolicy(IHttpResiliencePipelineBuilder builder, string optionsName, Action> configure) + { + var pipelineName = builder.PipelineName; + + _ = builder.Services.AddRequestCloner(); + _ = builder.AddPolicy( + optionsName, + options => configure(options), + (builder, options, serviceProvider) => builder.AddFallbackPolicy( + "DefaultFallbackPolicy", + CreateProvider(serviceProvider, pipelineName, optionsName), + options.FallbackPolicyOptions)); + } + + private static FallbackScenarioTaskProvider CreateProvider(IServiceProvider serviceProvider, string pipelineName, string optionsName) + { + var cloner = serviceProvider.GetRequiredService(); + var monitor = serviceProvider.GetRequiredService>(); + var invokerProvider = ContextExtensions.CreateMessageInvokerProvider(pipelineName); + var requestProvider = ContextExtensions.CreateRequestMessageProvider(pipelineName); + + return args => + { + var request = requestProvider(args.Context)!; + var invoker = invokerProvider(args.Context)!; + var fallbackUrl = monitor.Get(optionsName).BaseFallbackUri!; + + // Request is cloned as the private property "_sendStatus" of the initial HttpRequestMessage + // is set to 1 (i.e. sent) during the execution of the SendAsync method. + using var snapshot = cloner.CreateSnapshot(request); + var newRequest = snapshot.Create().ReplaceHost(fallbackUrl); + + return invoker.SendAsync(newRequest, args.CancellationToken); + }; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpRequestMessageExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpRequestMessageExtensions.cs new file mode 100644 index 0000000000..7a3c1b8ed3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpRequestMessageExtensions.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +/// +/// Extension methods for the . +/// +internal static class HttpRequestMessageExtensions +{ + /// + /// Replaces the base URI of an . + /// + /// The request. + /// The updated URI. + /// Incoming with new . + public static HttpRequestMessage ReplaceHost(this HttpRequestMessage request, Uri updatedUri) + { + var replacedUri = request.RequestUri!.ReplaceHost(updatedUri); + request.RequestUri = replacedUri; + + return request; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpResiliencePipelineBuilder.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpResiliencePipelineBuilder.cs new file mode 100644 index 0000000000..273bbba178 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpResiliencePipelineBuilder.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Resilience; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +internal sealed class HttpResiliencePipelineBuilder : IHttpResiliencePipelineBuilder +{ + public HttpResiliencePipelineBuilder(IResiliencePipelineBuilder builder) + { + PipelineName = builder.PipelineName; + Services = builder.Services; + } + + public string PipelineName { get; } + + public IServiceCollection Services { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpStandardResiliencePipelineBuilder.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpStandardResiliencePipelineBuilder.cs new file mode 100644 index 0000000000..04b4a9e2f2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpStandardResiliencePipelineBuilder.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +internal sealed class HttpStandardResiliencePipelineBuilder : IHttpStandardResiliencePipelineBuilder +{ + public HttpStandardResiliencePipelineBuilder(IHttpResiliencePipelineBuilder builder) + { + PipelineName = builder.PipelineName; + Services = builder.Services; + } + + public string PipelineName { get; } + + public IServiceCollection Services { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IHttpRequestMessageSnapshot.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IHttpRequestMessageSnapshot.cs new file mode 100644 index 0000000000..d9be95049e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IHttpRequestMessageSnapshot.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +/// +/// The snapshot of created by . +/// +internal interface IHttpRequestMessageSnapshot : IDisposable +{ + /// + /// Creates a new instance of from the snapshot. + /// + /// A instance. + HttpRequestMessage Create(); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IPipelineKeyProvider.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IPipelineKeyProvider.cs new file mode 100644 index 0000000000..8fd9414c19 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IPipelineKeyProvider.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +/// +/// The provider that returns the pipeline key from the request message. +/// +internal interface IPipelineKeyProvider +{ + /// + /// Returns the pipeline key from the request message. + /// + /// The request message. + /// The pipeline key. + string GetPipelineKey(HttpRequestMessage requestMessage); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IRequestClonerInternal.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IRequestClonerInternal.cs new file mode 100644 index 0000000000..f56e2f85d3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IRequestClonerInternal.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +/// +/// Internal interface for cloning an instance. +/// +internal interface IRequestClonerInternal +{ + /// + /// Creates a snapshot of that can be then used for cloning. + /// + /// The request message. + /// The snapshot instance. + IHttpRequestMessageSnapshot CreateSnapshot(HttpRequestMessage request); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/PipelineKeyProviderHelper.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/PipelineKeyProviderHelper.cs new file mode 100644 index 0000000000..e27aa83efd --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/PipelineKeyProviderHelper.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +internal static class PipelineKeyProviderHelper +{ + public static void SelectPipelineByAuthority(IServiceCollection services, string pipelineName, DataClassification classification) + { + UsePipelineKeyProvider(services, pipelineName, serviceProvider => + { + var redactor = serviceProvider.GetRequiredService().GetRedactor(classification); + + return ActivatorUtilities.CreateInstance(serviceProvider, redactor, classification); + }); + } + + public static void SelectPipelineBy(IServiceCollection services, string pipelineName, Func selectorFactory) + { + UsePipelineKeyProvider(services, pipelineName, serviceProvider => + { + var selector = selectorFactory(serviceProvider); + + return ActivatorUtilities.CreateInstance(serviceProvider, selector); + }); + } + + public static IPipelineKeyProvider? GetPipelineKeyProvider(this IServiceProvider provider, string pipelineName) + { + return provider.GetService>()?.GetService(pipelineName); + } + + private static void UsePipelineKeyProvider(IServiceCollection services, string pipelineName, Func factory) + { + _ = services.AddNamedSingleton(pipelineName, factory); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/PipelineNameHelper.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/PipelineNameHelper.cs new file mode 100644 index 0000000000..8ca87a434c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/PipelineNameHelper.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +internal static class PipelineNameHelper +{ + public static string GetPipelineName(string httpClientName, string pipelineIdentifier) + { + return $"{httpClientName}-{pipelineIdentifier}"; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ResilienceHandler.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ResilienceHandler.cs new file mode 100644 index 0000000000..513a72d764 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ResilienceHandler.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Polly; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +/// +/// Base class for resilience handler, i.e. handlers that use resilience pipelines to send the requests. +/// +internal sealed class ResilienceHandler : PolicyHttpMessageHandler +{ + private readonly Lazy _invoker; + private readonly Action> _invokerSetter; + private readonly Action _requestSetter; + + public ResilienceHandler(string pipelineName, Func> policySelector) + : base(policySelector) + { + // Stryker disable once boolean : no means to test this + _invoker = new Lazy(() => new HttpMessageInvoker(InnerHandler!), true); + _invokerSetter = ContextExtensions.CreateMessageInvokerSetter(pipelineName); + _requestSetter = ContextExtensions.CreateRequestMessageSetter(pipelineName); + } + + /// + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var created = false; + if (request.GetPolicyExecutionContext() is not Context context) + { + context = new Context(); + request.SetPolicyExecutionContext(context); + created = true; + } + + // set common properties to the context + context.SetRequestMetadata(request); + _invokerSetter(context, _invoker); + _requestSetter(context, request); + + try + { + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } + finally + { + if (created) + { + request.SetPolicyExecutionContext(null); + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..1000cbb295 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ServiceCollectionExtensions.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +/// +/// Resilience based extensions for . +/// +internal static class ServiceCollectionExtensions +{ + /// + /// Adds a implementation of to services. + /// + /// The services. + /// The same services instances. + public static IServiceCollection AddRequestCloner(this IServiceCollection services) + { + services.TryAddSingleton(); + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StandardPolicyNames.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StandardPolicyNames.cs new file mode 100644 index 0000000000..1f6b0707a1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StandardPolicyNames.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +internal static class StandardPolicyNames +{ + public const string CircuitBreaker = "Standard-CircuitBreaker"; + + public const string Bulkhead = "Standard-Bulkhead"; + + public const string Retry = "Standard-Retry"; + + public const string TotalRequestTimeout = "Standard-TotalRequestTimeout"; + + public const string AttemptTimeout = "Standard-AttemptTimeout"; +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/UriExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/UriExtensions.cs new file mode 100644 index 0000000000..9238389654 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/UriExtensions.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +/// +/// Extensions for Uri class to replace host. +/// +internal static class UriExtensions +{ +#if !NETCOREAPP3_1_OR_GREATER + private static readonly char[] _questionMark = new[] { '?' }; +#endif + + /// + /// Replaces host of with the host from . + /// + /// Uri with old host. + /// Uri with new host. + /// with host from . + public static Uri ReplaceHost(this Uri currentUri, Uri updatedUri) + { + _ = Throw.IfNull(currentUri); + _ = Throw.IfNull(updatedUri); + + var builder = new UriBuilder(updatedUri) + { + Path = currentUri.LocalPath, + + // UriBuilder always prepends with a question mark when setting the Query property. +#if NETCOREAPP3_1_OR_GREATER + Query = currentUri.Query.TrimStart('?') +#else + Query = currentUri.Query.TrimStart(_questionMark) +#endif + }; + + return builder.Uri; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/FallbackClientHandlerOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/FallbackClientHandlerOptionsValidator.cs new file mode 100644 index 0000000000..3862acdf04 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/FallbackClientHandlerOptionsValidator.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Validators; + +[Experimental("Required for Experimental public API since 1.21.0. Internal use only.")] +internal sealed class FallbackClientHandlerOptionsValidator : IValidateOptions +{ + private const string NoPathAndQuery = "/"; + + public ValidateOptionsResult Validate(string? name, FallbackClientHandlerOptions options) + { + var builder = new ValidateOptionsResultBuilder(); + + if (options.FallbackPolicyOptions is null) + { + builder.AddError("must be configured to define a fallback policy.", nameof(options.FallbackPolicyOptions)); + } + + var fallbackUri = options.BaseFallbackUri; + + if (fallbackUri is null) + { + builder.AddError("must be configured.", nameof(options.BaseFallbackUri)); + } + else + { + if (!fallbackUri.IsAbsoluteUri) + { + builder.AddError("must be an absolute uri.", nameof(options.BaseFallbackUri)); + } + else if (fallbackUri.PathAndQuery != NoPathAndQuery) + { + builder.AddError("must be a base uri, hence it may contain only the schema, host and port.", nameof(options.BaseFallbackUri)); + } + } + + return builder.Build(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpCircuitBreakerPolicyOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpCircuitBreakerPolicyOptionsValidator.cs new file mode 100644 index 0000000000..c928ce0052 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpCircuitBreakerPolicyOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Validators; + +[OptionsValidator] +internal sealed partial class HttpCircuitBreakerPolicyOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpFallbackPolicyOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpFallbackPolicyOptionsValidator.cs new file mode 100644 index 0000000000..f240da0181 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpFallbackPolicyOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Validators; + +[OptionsValidator] +internal sealed partial class HttpFallbackPolicyOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpRetryPolicyOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpRetryPolicyOptionsValidator.cs new file mode 100644 index 0000000000..d4bed96dcd --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpRetryPolicyOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Validators; + +[OptionsValidator] +internal sealed partial class HttpRetryPolicyOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpStandardResilienceOptionsCustomValidator.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpStandardResilienceOptionsCustomValidator.cs new file mode 100644 index 0000000000..f88c189cef --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpStandardResilienceOptionsCustomValidator.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Validators; + +internal sealed class HttpStandardResilienceOptionsCustomValidator : IValidateOptions +{ + private const int CircuitBreakerTimeoutMultiplier = 2; + + public ValidateOptionsResult Validate(string? name, HttpStandardResilienceOptions options) + { + var builder = new ValidateOptionsResultBuilder(); + + if (options.AttemptTimeoutOptions.TimeoutInterval > options.TotalRequestTimeoutOptions.TimeoutInterval) + { + builder.AddError($"Total request timeout policy must have a greater timeout than the attempt timeout policy. " + + $"Total Request Timeout: {options.TotalRequestTimeoutOptions.TimeoutInterval.TotalSeconds}s, " + + $"Attempt Timeout: {options.AttemptTimeoutOptions.TimeoutInterval.TotalSeconds}s"); + } + + if (options.CircuitBreakerOptions.SamplingDuration < TimeSpan.FromMilliseconds(options.AttemptTimeoutOptions.TimeoutInterval.TotalMilliseconds * CircuitBreakerTimeoutMultiplier)) + { + builder.AddError("The sampling duration of circuit breaker policy needs to be at least double of " + + $"an attempt timeout policy’s timeout interval, in order to be effective. " + + $"Sampling Duration: {options.CircuitBreakerOptions.SamplingDuration.TotalSeconds}s," + + $"Attempt Timeout: {options.AttemptTimeoutOptions.TimeoutInterval.TotalSeconds}s"); + } + + if (options.RetryOptions.RetryCount != RetryPolicyOptions.InfiniteRetry) + { + TimeSpan retrySum = options.RetryOptions.GetRetryPolicyDelaySum(); + + if (retrySum > options.TotalRequestTimeoutOptions.TimeoutInterval) + { + builder.AddError($"The cumulative delay of the retry policy cannot be larger than total request timeout policy interval. " + + $"Cumulative Delay: {retrySum.TotalSeconds}s," + + $"Total Request Timeout: {options.TotalRequestTimeoutOptions.TimeoutInterval.TotalSeconds}s"); + } + } + + return builder.Build(); + + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpStandardResilienceOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpStandardResilienceOptionsValidator.cs new file mode 100644 index 0000000000..d07c5564bf --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpStandardResilienceOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Validators; + +[OptionsValidator] +internal sealed partial class HttpStandardResilienceOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/ValidationHelper.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/ValidationHelper.cs new file mode 100644 index 0000000000..2ea2bd439e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/ValidationHelper.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using Microsoft.Extensions.Resilience; +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Http.Resilience.Internal.Validators; + +internal static class ValidationHelper +{ + public static TimeSpan GetRetryPolicyDelaySum(this RetryPolicyOptions retryPolicyOptions) + { + return retryPolicyOptions.BackoffType == BackoffType.ExponentialWithJitter ? + retryPolicyOptions.GetExponentialWithJitterDeterministicDelay() : + retryPolicyOptions.GetDelays().Aggregate((accumulated, current) => accumulated + current); + } + + /// + /// Calculates the upper-bound cumulated delay of the given retry policy options by using the algorithm defined in + /// https://github.com/Polly-Contrib/Polly.Contrib.WaitAndRetry/blob/master/src/Polly.Contrib.WaitAndRetry/Backoff.DecorrelatedJitterV2.cs, + /// with the randomized jitter factor (a value between 0 and 1) replaced with 1. + /// + /// The retry policy options. + /// The calculated upper-bound cumulated delay of the retry policy. + public static TimeSpan GetExponentialWithJitterDeterministicDelay(this RetryPolicyOptions options) + { + var totalDelay = TimeSpan.Zero; + + const double Factor = 4.0; + const double ScalingFactor = 1 / 1.4d; + var maxTimeSpanDouble = TimeSpan.MaxValue.Ticks - 1000; + var targetTicksFirstDelay = options.BaseDelay.Ticks; + + var prev = 0.0; + for (int i = 0; EvaluateRetry(i, options.RetryCount); i++) + { + var t = i + 1.0; + var next = Math.Pow(2, t) * Math.Tanh(Math.Sqrt(Factor * t)); + + var formulaIntrinsicValue = next - prev; + var diff = (long)Math.Min(formulaIntrinsicValue * ScalingFactor * targetTicksFirstDelay, maxTimeSpanDouble); + + try + { + totalDelay += TimeSpan.FromTicks(diff); + } + catch (OverflowException) + { + return TimeSpan.FromTicks(maxTimeSpanDouble); + } + + prev = next; + } + + return totalDelay; + } + + private static bool EvaluateRetry(int retry, int maxRetryCount) => retry < maxRetryCount || maxRetryCount == RetryPolicyOptions.InfiniteRetry; +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/PipelineKeySelector.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/PipelineKeySelector.cs new file mode 100644 index 0000000000..7e3f5997aa --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/PipelineKeySelector.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// A function that returns a pipeline key extracted from the request message. +/// +/// The request message that the pipeline key is extracted from. +/// A pipeline key. +/// +/// The pipeline key is used by metrics and telemetry. Make sure it does not contain any sensitive data. +/// +public delegate string PipelineKeySelector(HttpRequestMessage requestMessage); + diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/HttpClientLatencyTelemetryExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/HttpClientLatencyTelemetryExtensions.cs new file mode 100644 index 0000000000..4b92a7eeb3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/HttpClientLatencyTelemetryExtensions.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Telemetry.Latency.Internal; +using Microsoft.Extensions.Http.Telemetry.Logging; +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Telemetry.Latency; + +/// +/// Extension methods to add http client latency telemetry. +/// +public static class HttpClientLatencyTelemetryExtensions +{ + /// + /// Adds a to collect latency information and enrich outgoing request log for all http clients. + /// + /// + /// This extension configures latency information collection globally for all http clients. + /// + /// The . + /// + /// instance for chaining. + /// + public static IServiceCollection AddDefaultHttpClientLatencyTelemetry(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + _ = services.RegisterCheckpointNames(HttpCheckpoints.Checkpoints); + _ = services.AddOptions(); + _ = services.AddActivatedSingleton(); + _ = services.AddActivatedSingleton(); + _ = services.AddTransient(); + _ = services.AddHttpClientLogEnricher(); + + return services.ConfigureAll( + httpClientOptions => + { + httpClientOptions + .HttpMessageHandlerBuilderActions.Add(httpMessageHandlerBuilder => + { + var handler = httpMessageHandlerBuilder.Services.GetRequiredService(); + httpMessageHandlerBuilder.AdditionalHandlers.Add(handler); + }); + }); + } + + /// + /// Adds a to collect latency information and enrich outgoing request log for all http clients. + /// + /// + /// This extension configures outgoing request logs auto collection globally for all http clients. + /// + /// The . + /// The to use for configuring . + /// + /// instance for chaining. + /// + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(LoggingOptions))] + [UnconditionalSuppressMessage("Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed with [DynamicDependency]")] + public static IServiceCollection AddDefaultHttpClientLatencyTelemetry(this IServiceCollection services, IConfigurationSection section) + { + _ = Throw.IfNull(section); + + _ = services + .Configure(section); + + return services.AddDefaultHttpClientLatencyTelemetry(); + } + + /// + /// Adds a to collect latency information and enrich outgoing request log for all http clients. + /// + /// + /// This extension configures outgoing request logs auto collection globally for all http clients. + /// + /// The . + /// The delegate to configure with. + /// + /// instance for chaining. + /// + public static IServiceCollection AddDefaultHttpClientLatencyTelemetry(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(configure); + + _ = services + .Configure(configure); + + return services.AddDefaultHttpClientLatencyTelemetry(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/HttpClientLatencyTelemetryOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/HttpClientLatencyTelemetryOptions.cs new file mode 100644 index 0000000000..8488de6ad7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/HttpClientLatencyTelemetryOptions.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; + +namespace Microsoft.Extensions.Http.Telemetry.Latency; + +/// +/// Options to configure the http client latency telemetry. +/// +public class HttpClientLatencyTelemetryOptions +{ + /// + /// Gets or sets a value indicating whether to collect detailed latency breakdown of call. + /// + /// + /// Detailed breakdowns add checkpoints for HTTP operations such as connection open, request headers sent etc. + /// Defaults to . + /// + public bool EnableDetailedLatencyBreadkdown { get; set; } = true; +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpCheckpoints.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpCheckpoints.cs new file mode 100644 index 0000000000..e5390aa61d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpCheckpoints.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Http.Telemetry.Latency.Internal; + +internal static class HttpCheckpoints +{ + public const string SocketConnectStart = "cons"; + public const string SocketConnectEnd = "cone"; + public const string ConnectionEstablished = "cones"; + public const string RequestLeftQueue = "rlq"; + + public const string NameResolutionStart = "dnss"; + public const string NameResolutionEnd = "dnse"; + + public const string RequestHeadersStart = "reqhs"; + public const string RequestHeadersEnd = "reqhe"; + + public const string RequestContentStart = "reqcs"; + public const string RequestContentEnd = "reqce"; + + public const string ResponseHeadersStart = "reshs"; + public const string ResponseHeadersEnd = "reshe"; + + public const string ResponseContentStart = "rescs"; + public const string ResponseContentEnd = "resce"; + + public const string HandlerRequestStart = "handreqs"; + public const string EnricherInvoked = "enrin"; + + public static readonly string[] Checkpoints = new[] + { + SocketConnectStart, + SocketConnectEnd, + ConnectionEstablished, + RequestLeftQueue, + + NameResolutionStart, + NameResolutionEnd, + + RequestHeadersStart, + RequestHeadersEnd, + + RequestContentStart, + RequestContentEnd, + + ResponseHeadersStart, + ResponseHeadersEnd, + + ResponseContentStart, + ResponseContentEnd, + + HandlerRequestStart, + EnricherInvoked + }; +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpClientLatencyContext.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpClientLatencyContext.cs new file mode 100644 index 0000000000..0d6fb7218a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpClientLatencyContext.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Microsoft.Extensions.Telemetry.Latency; + +namespace Microsoft.Extensions.Http.Telemetry.Latency.Internal; + +internal sealed class HttpClientLatencyContext +{ + private readonly AsyncLocal _latencyContext = new(); + + public ILatencyContext? Get() + { + return _latencyContext.Value; + } + + public void Set(ILatencyContext context) + { + _latencyContext.Value = context; + } + + public void Unset() + { + _latencyContext.Value = null; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpClientLatencyLogEnricher.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpClientLatencyLogEnricher.cs new file mode 100644 index 0000000000..d706406023 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpClientLatencyLogEnricher.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using Microsoft.Extensions.Http.Telemetry.Logging; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Http.Telemetry.Latency.Internal; + +/// +/// The enrciher appends checkpoints for the outgoing http request. +/// It also logs the server name from the response header to correlate logs between client and server. +/// +internal sealed class HttpClientLatencyLogEnricher : IHttpClientLogEnricher +{ + private static readonly ObjectPool _builderPool = PoolFactory.SharedStringBuilderPool; + private readonly HttpClientLatencyContext _latencyContext; + + private readonly CheckpointToken _enricherInvoked; + + public HttpClientLatencyLogEnricher(HttpClientLatencyContext latencyContext, ILatencyContextTokenIssuer tokenIssuer) + { + _latencyContext = latencyContext; + _enricherInvoked = tokenIssuer.GetCheckpointToken(HttpCheckpoints.EnricherInvoked); + } + + public void Enrich(IEnrichmentPropertyBag enrichmentBag, HttpRequestMessage? request = null, HttpResponseMessage? response = null) + { + if (response != null) + { + var lc = _latencyContext.Get(); + lc?.AddCheckpoint(_enricherInvoked); + + StringBuilder stringBuilder = _builderPool.Get(); + + // Add serverName, checkpoints to outgoing http logs. + AppendServerName(response.Headers, stringBuilder); + _ = stringBuilder.Append(','); + + if (lc != null) + { + AppendCheckpoints(lc, stringBuilder); + } + + enrichmentBag.Add("latencyInfo", stringBuilder.ToString()); + + _builderPool.Return(stringBuilder); + } + } + + private static void AppendServerName(HttpHeaders headers, StringBuilder stringBuilder) + { + if (headers.TryGetValues(TelemetryConstants.ServerApplicationNameHeader, out var values)) + { + _ = stringBuilder.Append(values!.First()); + } + } + + private static void AppendCheckpoints(ILatencyContext latencyContext, StringBuilder stringBuilder) + { + var latencyData = latencyContext.LatencyData; + for (int i = 0; i < latencyData.Checkpoints.Length; i++) + { + _ = stringBuilder.Append(latencyData.Checkpoints[i].Name); + _ = stringBuilder.Append('/'); + } + + _ = stringBuilder.Append(','); + for (int i = 0; i < latencyData.Checkpoints.Length; i++) + { + var ms = ((double)latencyData.Checkpoints[i].Elapsed / latencyData.Checkpoints[i].Frequency) * 1000; + _ = stringBuilder.Append(ms); + _ = stringBuilder.Append('/'); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpLatencyTelemetryHandler.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpLatencyTelemetryHandler.cs new file mode 100644 index 0000000000..37e5f077fc --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpLatencyTelemetryHandler.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AmbientMetadata; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Telemetry.Latency.Internal; + +/// +/// This delegating handler creates a for the request if it has not been created for the request. +/// It also adds client name to outgoing request header which helps with correlating client and server telemetry. +/// +internal sealed class HttpLatencyTelemetryHandler : DelegatingHandler +{ + private readonly HttpRequestLatencyListener _latencyListener; + private readonly ILatencyContextProvider _latencyContextProvider; + private readonly CheckpointToken _handlerStart; + private readonly string _applicationName; + + public HttpLatencyTelemetryHandler(HttpRequestLatencyListener latencyListener, ILatencyContextTokenIssuer tokenIssuer, ILatencyContextProvider latencyContextProvider, + IOptions options, IOptions appMetdata) + { + var appMetadata = Throw.IfMemberNull(appMetdata, appMetdata.Value); + var telemetryOptions = Throw.IfMemberNull(options, options.Value); + + _latencyListener = latencyListener; + _latencyContextProvider = latencyContextProvider; + _handlerStart = tokenIssuer.GetCheckpointToken(HttpCheckpoints.HandlerRequestStart); + _applicationName = appMetdata.Value.ApplicationName; + + if (telemetryOptions.EnableDetailedLatencyBreadkdown) + { + _latencyListener.Enable(); + } + } + + protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + using var context = _latencyContextProvider.CreateContext(); + context.AddCheckpoint(_handlerStart); + _latencyListener.LatencyContext.Set(context); + + request.Headers.Add(TelemetryConstants.ClientApplicationNameHeader, _applicationName); + + var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + + _latencyListener.LatencyContext.Unset(); + + return response; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpRequestLatencyListener.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpRequestLatencyListener.cs new file mode 100644 index 0000000000..2eebc3cba2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Latency/Internal/HttpRequestLatencyListener.cs @@ -0,0 +1,163 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Threading; +using Microsoft.Extensions.Telemetry.Latency; + +namespace Microsoft.Extensions.Http.Telemetry.Latency.Internal; + +internal sealed class HttpRequestLatencyListener : EventListener +{ + private const string SocketProviderName = "System.Net.Sockets"; + private const string HttpProviderName = "System.Net.Http"; + private const string NameResolutionProivderName = "System.Net.NameResolution"; + + private readonly ConcurrentDictionary _eventSources = new() + { + [SocketProviderName] = null, + [HttpProviderName] = null, + [NameResolutionProivderName] = null + }; + + internal HttpClientLatencyContext LatencyContext { get; } + + private readonly EventToCheckpointToken _eventToCheckpointToken; + + private int _enabled; + + internal bool Enabled => _enabled == 1; + + public HttpRequestLatencyListener(HttpClientLatencyContext latencyContext, ILatencyContextTokenIssuer tokenIssuer) + { + LatencyContext = latencyContext; + _eventToCheckpointToken = new(tokenIssuer); + } + + public void Enable() + { + if (Interlocked.CompareExchange(ref _enabled, 1, 0) == 0) + { + foreach (var eventSource in _eventSources) + { + if (eventSource.Value != null) + { + EnableEventSource(eventSource.Value); + } + } + } + } + + internal void OnEventWritten(string eventSourceName, string? eventName) + { + // If event of interest, add a checkpoint for it. + CheckpointToken? token = _eventToCheckpointToken.GetCheckpointToken(eventSourceName, eventName); + if (token.HasValue) + { + LatencyContext.Get()?.AddCheckpoint(token.Value); + } + } + + internal void OnEventSourceCreated(string eventSourceName, EventSource eventSource) + { + if (_eventSources.ContainsKey(eventSourceName)) + { + _eventSources[eventSourceName] = eventSource; + EnableEventSource(eventSource); + } + } + + protected override void OnEventSourceCreated(EventSource eventSource) + { + OnEventSourceCreated(eventSource.Name, eventSource); + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + OnEventWritten(eventData.EventSource.Name, eventData.EventName); + } + + private void EnableEventSource(EventSource eventSource) + { + if (Enabled && !eventSource.IsEnabled()) + { + EnableEvents(eventSource, EventLevel.Informational); + } + } + + private sealed class EventToCheckpointToken + { + private static readonly Dictionary _socketMap = new() + { + { "ConnectStart", HttpCheckpoints.SocketConnectStart }, + { "ConnectStop", HttpCheckpoints.SocketConnectEnd } + }; + + private static readonly Dictionary _nameResolutionMap = new() + { + { "ResolutionStart", HttpCheckpoints.NameResolutionStart }, + { "ResolutionStop", HttpCheckpoints.NameResolutionEnd } + }; + + private static readonly Dictionary _httpMap = new() + { + { "ConnectionEstablished", HttpCheckpoints.ConnectionEstablished }, + { "RequestLeftQueue", HttpCheckpoints.RequestLeftQueue }, + { "RequestHeadersStart", HttpCheckpoints.RequestHeadersStart }, + { "RequestHeadersStop", HttpCheckpoints.RequestHeadersEnd }, + { "RequestContentStart", HttpCheckpoints.RequestContentStart }, + { "RequestContentStop", HttpCheckpoints.RequestContentEnd }, + { "ResponseHeadersStart", HttpCheckpoints.ResponseHeadersStart }, + { "ResponseHeadersStop", HttpCheckpoints.ResponseHeadersEnd }, + { "ResponseContentStart", HttpCheckpoints.ResponseContentStart }, + { "ResponseContentStop", HttpCheckpoints.ResponseContentEnd } + }; + + private readonly FrozenDictionary> _eventToTokenMap; + + public EventToCheckpointToken(ILatencyContextTokenIssuer tokenIssuer) + { + Dictionary socket = new(); + foreach (string key in _socketMap.Keys) + { + socket[key] = tokenIssuer.GetCheckpointToken(_socketMap[key]); + } + + Dictionary nameResolution = new(); + foreach (string key in _nameResolutionMap.Keys) + { + nameResolution[key] = tokenIssuer.GetCheckpointToken(_nameResolutionMap[key]); + } + + Dictionary http = new(); + foreach (string key in _httpMap.Keys) + { + http[key] = tokenIssuer.GetCheckpointToken(_httpMap[key]); + } + + _eventToTokenMap = new Dictionary> + { + { SocketProviderName, socket.ToFrozenDictionary(StringComparer.Ordinal, optimizeForReading: true) }, + { NameResolutionProivderName, nameResolution.ToFrozenDictionary(StringComparer.Ordinal, optimizeForReading: true) }, + { HttpProviderName, http.ToFrozenDictionary(StringComparer.Ordinal, optimizeForReading: true) } + }.ToFrozenDictionary(StringComparer.Ordinal, optimizeForReading: true); + } + + public CheckpointToken? GetCheckpointToken(string eventSourceName, string? eventName) + { + if (eventName != null && _eventToTokenMap.TryGetValue(eventSourceName, out var events)) + { + if (events.TryGetValue(eventName, out var token)) + { + return token; + } + } + + return null; + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/HttpClientLoggingDimensions.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/HttpClientLoggingDimensions.cs new file mode 100644 index 0000000000..59a149c1d4 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/HttpClientLoggingDimensions.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Http.Telemetry.Logging; + +/// +/// Constants used for HTTP client logging dimensions. +/// +public static class HttpClientLoggingDimensions +{ + /// + /// HTTP Request duration. + /// + public const string Duration = "duration"; + + /// + /// HTTP Host. + /// + public const string Host = "httpHost"; + + /// + /// HTTP Method. + /// + public const string Method = "httpMethod"; + + /// + /// HTTP Path. + /// + public const string Path = "httpPath"; + + /// + /// HTTP Request Body. + /// + public const string RequestBody = "httpRequestBody"; + + /// + /// HTTP Response Body. + /// + public const string ResponseBody = "httpResponseBody"; + + /// + /// HTTP Request Headers prefix. + /// + public const string RequestHeaderPrefix = "httpRequestHeader_"; + + /// + /// HTTP Response Headers prefix. + /// + public const string ResponseHeaderPrefix = "httpResponseHeader_"; + + /// + /// HTTP Status Code. + /// + public const string StatusCode = "httpStatusCode"; + + /// + /// Gets a list of all dimension names. + /// + /// A read-only of all dimension names. + public static IReadOnlyList DimensionNames { get; } = + Array.AsReadOnly(new[] + { + Duration, + Host, + Method, + Path, + RequestBody, + RequestHeaderPrefix, + ResponseBody, + ResponseHeaderPrefix, + StatusCode + }); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/HttpClientLoggingExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/HttpClientLoggingExtensions.cs new file mode 100644 index 0000000000..e1cdedae6e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/HttpClientLoggingExtensions.cs @@ -0,0 +1,264 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Http.Telemetry.Logging.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Telemetry.Logging; + +/// +/// Extension methods to register HTTP client logging feature. +/// +public static class HttpClientLoggingExtensions +{ + internal static readonly string HandlerAddedTwiceExceptionMessage = + $"{typeof(HttpLoggingHandler)} was already added either to all HttpClientBuilder's or to the current instance of {typeof(IHttpClientBuilder)}."; + + /// + /// Adds a to collect and emit logs for outgoing requests for all http clients. + /// + /// + /// This extension configures outgoing request logs auto collection globally for all http clients. + /// + /// The . + /// + /// instance for chaining. + /// + public static IServiceCollection AddDefaultHttpClientLogging(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + _ = services + .AddHttpRouteProcessor() + .AddHttpHeadersRedactor() + .AddOutgoingRequestContext() + .RemoveAll(); + + services.TryAddActivatedSingleton(); + services.TryAddActivatedSingleton(); + + return services.ConfigureAll( + httpClientOptions => + { + httpClientOptions + .HttpMessageHandlerBuilderActions.Add(httpMessageHandlerBuilder => + { + var logger = httpMessageHandlerBuilder.Services.GetRequiredService>(); + var httpRequestReader = httpMessageHandlerBuilder.Services.GetRequiredService(); + var enrichers = httpMessageHandlerBuilder.Services.GetServices(); + var loggingOptions = httpMessageHandlerBuilder.Services.GetRequiredService>(); + + if (httpMessageHandlerBuilder.AdditionalHandlers.Any(handler => handler is HttpLoggingHandler)) + { + Throw.InvalidOperationException(HandlerAddedTwiceExceptionMessage); + } + + httpMessageHandlerBuilder.AdditionalHandlers.Add(new HttpLoggingHandler(logger, httpRequestReader, enrichers, loggingOptions)); + }); + }); + } + + /// + /// Adds a to collect and emit logs for outgoing requests for all http clients. + /// + /// + /// This extension configures outgoing request logs auto collection globally for all http clients. + /// + /// The . + /// The to use for configuring . + /// + /// instance for chaining. + /// + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(LoggingOptions))] + [UnconditionalSuppressMessage("Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed with [DynamicDependency]")] + public static IServiceCollection AddDefaultHttpClientLogging(this IServiceCollection services, IConfigurationSection section) + { + _ = Throw.IfNull(section); + + _ = services + .AddValidatedOptions() + .Bind(section); + + return services.AddDefaultHttpClientLogging(); + } + + /// + /// Adds a to collect and emit logs for outgoing requests for all http clients. + /// + /// + /// This extension configures outgoing request logs auto collection globally for all http clients. + /// + /// The . + /// The delegate to configure with. + /// + /// instance for chaining. + /// + public static IServiceCollection AddDefaultHttpClientLogging(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(configure); + + _ = services + .AddValidatedOptions() + .Configure(configure); + + return services.AddDefaultHttpClientLogging(); + } + + /// + /// Registers HTTP client logging components into . + /// + /// The . + /// + /// An that can be used to configure the client. + /// + /// Argument is . + public static IHttpClientBuilder AddHttpClientLogging(this IHttpClientBuilder builder) + { + _ = Throw.IfNull(builder); + + _ = builder.Services + .AddValidatedOptions(builder.Name); + + _ = builder.Services + .AddHttpRouteProcessor() + .AddHttpHeadersRedactor() + .AddOutgoingRequestContext() + .RemoveAll(); + + builder.Services.TryAddActivatedSingleton(); + builder.Services.TryAddActivatedSingleton(); + + _ = builder.ConfigureHttpMessageHandlerBuilder(b => + { + if (b.AdditionalHandlers.Any(handler => handler is HttpLoggingHandler)) + { + Throw.InvalidOperationException(HandlerAddedTwiceExceptionMessage); + } + }); + + return builder.AddHttpMessageHandler(ConfigureHandler(builder)); + } + + /// + /// Registers HTTP client logging components into . + /// + /// The . + /// The to use for configuring . + /// + /// An that can be used to configure the client. + /// + /// One of the arguments is . + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(LoggingOptions))] + [UnconditionalSuppressMessage("Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed with [DynamicDependency]")] + public static IHttpClientBuilder AddHttpClientLogging(this IHttpClientBuilder builder, IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + + _ = builder.Services + .AddValidatedOptions(builder.Name) + .Bind(section); + + _ = builder.Services + .AddHttpRouteProcessor() + .AddHttpHeadersRedactor() + .AddOutgoingRequestContext() + .RemoveAll(); + + builder.Services.TryAddActivatedSingleton(); + builder.Services.TryAddActivatedSingleton(); + + return builder.AddHttpMessageHandler(ConfigureHandler(builder)); + } + + /// + /// Registers HTTP client logging components into . + /// + /// The . + /// The delegate to configure with. + /// + /// An that can be used to configure the client. + /// + /// One of the arguments is . + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(LoggingOptions))] + [UnconditionalSuppressMessage("Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed with [DynamicDependency]")] + public static IHttpClientBuilder AddHttpClientLogging(this IHttpClientBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + _ = builder.Services + .AddValidatedOptions(builder.Name) + .Configure(configure); + + _ = builder.Services + .AddHttpRouteProcessor() + .AddHttpHeadersRedactor() + .AddOutgoingRequestContext() + .RemoveAll(); + + builder.Services.TryAddActivatedSingleton(); + builder.Services.TryAddActivatedSingleton(); + + return builder.AddHttpMessageHandler(ConfigureHandler(builder)); + } + + /// + /// Adds an enricher instance of to the to enrich HTTP client logs. + /// + /// Type of enricher. + /// The to add the instance of to. + /// The so that additional calls can be chained. + public static IServiceCollection AddHttpClientLogEnricher(this IServiceCollection services) + where T : class, IHttpClientLogEnricher + { + _ = Throw.IfNull(services); + + _ = services.AddActivatedSingleton(); + + return services; + } + + /// + /// Configures DI registration so that a named instance of gets injected into . + /// + /// The . + /// + /// An that can be used to configure the client. + /// + private static Func ConfigureHandler(IHttpClientBuilder builder) + { + return serviceProvider => + { + var loggingOptions = Microsoft.Extensions.Options.Options.Create(serviceProvider + .GetRequiredService>().Get(builder.Name)); + + return ActivatorUtilities.CreateInstance( + serviceProvider, + ActivatorUtilities.CreateInstance( + serviceProvider, + ActivatorUtilities.CreateInstance( + serviceProvider, + loggingOptions), + loggingOptions), + loggingOptions); + }; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/IHttpClientLogEnricher.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/IHttpClientLogEnricher.cs new file mode 100644 index 0000000000..eeac58221a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/IHttpClientLogEnricher.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Http.Telemetry.Logging; + +/// +/// Interface for implementing log enrichers for HTTP client requests. +/// +public interface IHttpClientLogEnricher +{ + /// + /// Enrich HTTP client request logs. + /// + /// Property bag to add enriched properties to. + /// object associated with the outgoing HTTP request. + /// object associated with the outgoing HTTP request. + void Enrich(IEnrichmentPropertyBag enrichmentBag, HttpRequestMessage? request = null, HttpResponseMessage? response = null); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/Constants.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/Constants.cs new file mode 100644 index 0000000000..5b69c84f51 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/Constants.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Internal; + +internal static class Constants +{ + public const string NoContent = "[no-content-type]"; + public const string UnreadableContent = "[unreadable-content-type]"; + public const string ReadCancelled = "[read-cancelled]"; +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpHeadersReader.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpHeadersReader.cs new file mode 100644 index 0000000000..de46e9d36f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpHeadersReader.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Internal; + +internal sealed class HttpHeadersReader : IHttpHeadersReader +{ + private readonly FrozenDictionary _requestHeaders; + private readonly FrozenDictionary _responseHeaders; + private readonly IHttpHeadersRedactor _redactor; + + public HttpHeadersReader(IOptions options, IHttpHeadersRedactor redactor) + { + _ = Throw.IfMemberNull(options, options.Value); + + _redactor = redactor; + + _requestHeaders = options.Value.RequestHeadersDataClasses.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase, optimizeForReading: true); + _responseHeaders = options.Value.ResponseHeadersDataClasses.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase, optimizeForReading: true); + } + + public void ReadRequestHeaders(HttpRequestMessage request, List>? destination) + { + if (destination is null) + { + return; + } + + ReadHeaders(request.Headers, _requestHeaders, destination); + } + + public void ReadResponseHeaders(HttpResponseMessage response, List>? destination) + { + if (destination is null) + { + return; + } + + ReadHeaders(response.Headers, _responseHeaders, destination); + } + + private void ReadHeaders(HttpHeaders requestHeaders, FrozenDictionary headersToLog, List> destination) + { + foreach (var kvp in headersToLog) + { + var classification = kvp.Value; + var header = kvp.Key; + + if (requestHeaders.TryGetValues(header, out var values)) + { + destination.Add(new(header, _redactor.Redact(values, classification))); + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpLoggingHandler.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpLoggingHandler.cs new file mode 100644 index 0000000000..27197fb5c0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpLoggingHandler.cs @@ -0,0 +1,173 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Logging; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Internal; + +/// +/// Handler that logs HTTP client requests./>. +/// +internal sealed class HttpLoggingHandler : DelegatingHandler +{ + internal TimeProvider TimeProvider = TimeProvider.System; + + private readonly IHttpClientLogEnricher[] _enrichers; + private readonly ILogger _logger; + private readonly IHttpRequestReader _httpRequestReader; + + private readonly ObjectPool>> _headersPool = + PoolFactory.CreateListPool>(); + + private readonly ObjectPool _logRecordPool = + PoolFactory.CreatePool(new LogRecordPooledObjectPolicy()); + + private readonly bool _logRequestStart; + private readonly bool _logRequestHeaders; + private readonly bool _logResponseHeaders; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// Handler for reading an HTTP request message. + /// HTTP client log enrichers to enrich log records by. + /// An instance of representing HTTP logging options. + public HttpLoggingHandler( + ILogger logger, + IHttpRequestReader httpRequestReader, + IEnumerable enrichers, + IOptions options) + { + _logger = logger; + _httpRequestReader = httpRequestReader; + _enrichers = enrichers.ToArray(); + _ = Throw.IfMemberNull(options, options.Value); + + _logRequestStart = options.Value.LogRequestStart; + _logResponseHeaders = options.Value.ResponseHeadersDataClasses.Count > 0; + _logRequestHeaders = options.Value.RequestHeadersDataClasses.Count > 0; + } + + /// + /// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation. + /// + /// The HTTP request message to send to the server. + /// A cancellation token to cancel operation. + /// + /// The task object representing the asynchronous operation. + /// + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + _ = Throw.IfNull(request); + + var timestamp = TimeProvider.GetTimestamp(); + + var logRecord = _logRecordPool.Get(); + var propertyBag = LogMethodHelper.GetHelper(); + + List>? requestHeadersBuffer = null; + List>? responseHeadersBuffer = null; + + HttpResponseMessage? response = null; + + if (_logRequestHeaders) + { + requestHeadersBuffer = _headersPool.Get(); + } + + try + { + await _httpRequestReader.ReadRequestAsync(logRecord, request, requestHeadersBuffer, cancellationToken).ConfigureAwait(false); + + if (_logRequestStart) + { + Log.OutgoingRequest(_logger, LogLevel.Information, logRecord); + } + + response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + + if (_logResponseHeaders) + { + responseHeadersBuffer = _headersPool.Get(); + } + + await _httpRequestReader.ReadResponseAsync(logRecord, response, responseHeadersBuffer, cancellationToken).ConfigureAwait(false); + + FillLogRecord(logRecord, propertyBag, timestamp, request, response); + Log.OutgoingRequest(_logger, GetLogLevel(logRecord), logRecord); + + return response; + } + catch (Exception exception) + { + FillLogRecord(logRecord, propertyBag, timestamp, request, response); + Log.OutgoingRequestError(_logger, logRecord, exception); + + throw; + } + finally + { + _logRecordPool.Return(logRecord); + LogMethodHelper.ReturnHelper(propertyBag); + + if (responseHeadersBuffer is not null) + { + _headersPool.Return(responseHeadersBuffer); + } + + if (requestHeadersBuffer is not null) + { + _headersPool.Return(requestHeadersBuffer); + } + } + } + + private static LogLevel GetLogLevel(LogRecord logRecord) + { + const int HttpErrorsRangeStart = 400; + const int HttpErrorsRangeEnd = 599; + int statusCode = logRecord.StatusCode!.Value; + + if (statusCode >= HttpErrorsRangeStart && statusCode <= HttpErrorsRangeEnd) + { + return LogLevel.Error; + } + + return LogLevel.Information; + } + + [SuppressMessage("Design", "CA1031:Do not catch general exception types", + Justification = "We intentionally catch all exception types to make Telemetry code resilient to failures.")] + private void FillLogRecord( + LogRecord logRecord, LogMethodHelper propertyBag, long timestamp, + HttpRequestMessage request, HttpResponseMessage? response) + { + foreach (var enricher in _enrichers) + { + try + { + enricher.Enrich(propertyBag, request, response); + } + catch (Exception e) + { + Log.EnrichmentError(_logger, e); + } + } + + logRecord.EnrichmentProperties = propertyBag; + logRecord.Duration = (long)TimeProvider.GetElapsedTime(timestamp, TimeProvider.GetTimestamp()).TotalMilliseconds; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpRequestBodyReader.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpRequestBodyReader.cs new file mode 100644 index 0000000000..bbe8f17424 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpRequestBodyReader.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Frozen; +using System.IO; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +#if NETCOREAPP3_1_OR_GREATER +using Microsoft.Extensions.ObjectPool; +#endif +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; +#if NETCOREAPP3_1_OR_GREATER +using Microsoft.Shared.Pools; +#else +using System.Buffers; +#endif + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Internal; + +internal sealed class HttpRequestBodyReader +{ + /// + /// Exposed for testing purposes. + /// + internal readonly TimeSpan RequestReadTimeout; + +#if NETCOREAPP3_1_OR_GREATER + private static readonly ObjectPool> _bufferWriterPool = BufferWriterPool.SharedBufferWriterPool; +#endif + private readonly FrozenSet _readableRequestContentTypes; + private readonly int _requestReadLimit; + + public HttpRequestBodyReader(IOptions options, IDebuggerState? debugger = null) + { + var requestOptions = Throw.IfMemberNull(options, options.Value); + + _readableRequestContentTypes = requestOptions.RequestBodyContentTypes.ToFrozenSet(StringComparer.OrdinalIgnoreCase, optimizeForReading: true); + debugger ??= DebuggerState.System; + _requestReadLimit = requestOptions.BodySizeLimit; + + RequestReadTimeout = debugger.IsAttached + ? Timeout.InfiniteTimeSpan + : requestOptions.BodyReadTimeout; + } + + public ValueTask ReadAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request.Content == null || request.Method == HttpMethod.Get) + { + return new(string.Empty); + } + + var contentType = request.Content.Headers.ContentType; + if (contentType == null) + { + return new(Constants.NoContent); + } + + if (!_readableRequestContentTypes.Covers(contentType.MediaType)) + { + return new(Constants.UnreadableContent); + } + + return ReadFromStreamWithTimeoutAsync(request, RequestReadTimeout, _requestReadLimit, cancellationToken).Preserve(); + } + + private static async ValueTask ReadFromStreamWithTimeoutAsync(HttpRequestMessage request, + TimeSpan readTimeout, int readSizeLimit, CancellationToken cancellationToken) + { + using var joinedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + joinedTokenSource.CancelAfter(readTimeout); + + try + { + return await ReadFromStreamAsync(request, readSizeLimit, joinedTokenSource.Token).ConfigureAwait(false); + } + + // when readTimeout occurred: + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + return Constants.ReadCancelled; + } + } + + private static async ValueTask ReadFromStreamAsync(HttpRequestMessage request, int readSizeLimit, + CancellationToken cancellationToken) + { +#if NET5_0_OR_GREATER + var streamToReadFrom = await request.Content!.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); +#else + var streamToReadFrom = await request.Content.ReadAsStreamAsync().WaitAsync(cancellationToken).ConfigureAwait(false); +#endif + + var readLimit = Math.Min(readSizeLimit, (int)streamToReadFrom.Length); +#if NETCOREAPP3_1_OR_GREATER + var bufferWriter = _bufferWriterPool.Get(); + try + { + var memory = bufferWriter.GetMemory(readLimit).Slice(0, readLimit); + var charsWritten = await streamToReadFrom.ReadAsync(memory, cancellationToken).ConfigureAwait(false); + + return Encoding.UTF8.GetString(memory[..charsWritten].Span); + } + finally + { + _bufferWriterPool.Return(bufferWriter); + streamToReadFrom.Seek(0, SeekOrigin.Begin); + } + +#else + var buffer = ArrayPool.Shared.Rent(readLimit); + try + { + _ = await streamToReadFrom.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + return Encoding.UTF8.GetString(buffer.AsSpan(0, readLimit).ToArray()); + } + finally + { + ArrayPool.Shared.Return(buffer); + streamToReadFrom.Seek(0, SeekOrigin.Begin); + } +#endif + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpRequestReader.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpRequestReader.cs new file mode 100644 index 0000000000..964e4e356a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpRequestReader.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Internal; + +internal sealed class HttpRequestReader : IHttpRequestReader +{ + private readonly IHttpRouteFormatter _routeFormatter; + private readonly IHttpHeadersReader _httpHeadersReader; + private readonly FrozenDictionary _defaultSensitiveParameters; + + private readonly bool _logRequestBody; + private readonly bool _logResponseBody; + + private readonly bool _logRequestHeaders; + private readonly bool _logResponseHeaders; + + private readonly HttpRouteParameterRedactionMode _routeParameterRedactionMode; + + // These are not registered in DI as handler today is public and we would need to make all of those types public. + // They are not implemented as statics to simplify design and pass less arguments around. + // Also wanted to encapsulate logic of reading each part of the request to simplify handler logic itself. + private readonly HttpRequestBodyReader _httpRequestBodyReader; + private readonly HttpResponseBodyReader _httpResponseBodyReader; + + private readonly OutgoingPathLoggingMode _outgoingPathLogMode; + private readonly IOutgoingRequestContext _requestMetadataContext; + private readonly IDownstreamDependencyMetadataManager? _downstreamDependencyMetadataManager; + + public HttpRequestReader( + IOptions options, + IHttpRouteFormatter routeFormatter, + IHttpHeadersReader httpHeadersReader, + IOutgoingRequestContext requestMetadataContext, + IDownstreamDependencyMetadataManager? downstreamDependencyMetadataManager = null) + { + var optionsValue = Throw.IfMemberNull(options, options.Value); + _routeFormatter = routeFormatter; + _outgoingPathLogMode = Throw.IfOutOfRange(optionsValue.RequestPathLoggingMode); + _httpHeadersReader = httpHeadersReader; + _requestMetadataContext = requestMetadataContext; + _downstreamDependencyMetadataManager = downstreamDependencyMetadataManager; + + _defaultSensitiveParameters = optionsValue.RouteParameterDataClasses.ToFrozenDictionary(StringComparer.Ordinal, optimizeForReading: true); + + if (optionsValue.LogBody) + { + _logRequestBody = optionsValue.RequestBodyContentTypes.Count > 0; + _logResponseBody = optionsValue.ResponseBodyContentTypes.Count > 0; + } + + _logRequestHeaders = optionsValue.RequestHeadersDataClasses.Count > 0; + _logResponseHeaders = optionsValue.ResponseHeadersDataClasses.Count > 0; + + _httpRequestBodyReader = new HttpRequestBodyReader(options); + _httpResponseBodyReader = new HttpResponseBodyReader(options); + + _routeParameterRedactionMode = optionsValue.RequestPathParameterRedactionMode; + } + + public async Task ReadRequestAsync(LogRecord logRecord, HttpRequestMessage request, + List>? requestHeadersBuffer, CancellationToken cancellationToken) + { + if (_logRequestHeaders) + { + _httpHeadersReader.ReadRequestHeaders(request, requestHeadersBuffer); + logRecord.RequestHeaders = requestHeadersBuffer; + } + + if (_logRequestBody) + { + logRecord.RequestBody = await _httpRequestBodyReader.ReadAsync(request, cancellationToken) + .ConfigureAwait(false); + } + + logRecord.Host = request.RequestUri?.Host ?? TelemetryConstants.Unknown; + logRecord.Method = request.Method; + logRecord.Path = GetRedactedPath(request); + } + + public async Task ReadResponseAsync(LogRecord logRecord, HttpResponseMessage response, + List>? responseHeadersBuffer, + CancellationToken cancellationToken) + { + if (_logResponseHeaders) + { + _httpHeadersReader.ReadResponseHeaders(response, responseHeadersBuffer); + logRecord.ResponseHeaders = responseHeadersBuffer; + } + + if (_logResponseBody) + { + logRecord.ResponseBody = await _httpResponseBodyReader.ReadAsync(response, cancellationToken).ConfigureAwait(false); + } + + logRecord.StatusCode = (int)response.StatusCode; + } + + private string GetRedactedPath(HttpRequestMessage request) + { + if (request.RequestUri is null) + { + return TelemetryConstants.Unknown; + } + + if (_routeParameterRedactionMode == HttpRouteParameterRedactionMode.None) + { + return request.RequestUri.AbsolutePath; + } + + var requestMetadata = request.GetRequestMetadata() ?? + _requestMetadataContext.RequestMetadata ?? + _downstreamDependencyMetadataManager?.GetRequestMetadata(request); + + if (requestMetadata == null) + { + return TelemetryConstants.Redacted; + } + + var route = requestMetadata.RequestRoute; + if (route == TelemetryConstants.Unknown) + { + return requestMetadata.RequestName; + } + + return _outgoingPathLogMode switch + { + OutgoingPathLoggingMode.Formatted => _routeFormatter.Format(route, request.RequestUri.AbsolutePath, _routeParameterRedactionMode, _defaultSensitiveParameters), + _ => route + }; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpResponseBodyReader.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpResponseBodyReader.cs new file mode 100644 index 0000000000..e3e8f42e1e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/HttpResponseBodyReader.cs @@ -0,0 +1,147 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Frozen; +using System.IO; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Microsoft.IO; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Internal; + +internal sealed class HttpResponseBodyReader +{ + /// + /// Exposed for testing purposes. + /// + internal readonly TimeSpan ResponseReadTimeout; + + private static readonly ObjectPool> _bufferWriterPool = BufferWriterPool.SharedBufferWriterPool; + private readonly FrozenSet _readableResponseContentTypes; + private readonly int _responseReadLimit; + + private readonly RecyclableMemoryStreamManager _streamManager; + + public HttpResponseBodyReader(IOptions options, IDebuggerState? debugger = null) + { + _ = Throw.IfMemberNull(options, options.Value); + + var responseOptions = options.Value; + _streamManager = new RecyclableMemoryStreamManager(); + _readableResponseContentTypes = responseOptions.ResponseBodyContentTypes.ToFrozenSet(StringComparer.OrdinalIgnoreCase, optimizeForReading: true); + _responseReadLimit = responseOptions.BodySizeLimit; + + debugger ??= DebuggerState.System; + + ResponseReadTimeout = debugger.IsAttached + ? Timeout.InfiniteTimeSpan + : responseOptions.BodyReadTimeout; + } + + public ValueTask ReadAsync(HttpResponseMessage response, CancellationToken cancellationToken) + { + var contentType = response.Content.Headers.ContentType; + if (contentType == null) + { + return new(Constants.NoContent); + } + + if (!_readableResponseContentTypes.Covers(contentType.MediaType!)) + { + return new(Constants.UnreadableContent); + } + + return ReadFromStreamWithTimeoutAsync(response, ResponseReadTimeout, _responseReadLimit, _streamManager, + cancellationToken).Preserve(); + } + + private static async ValueTask ReadFromStreamAsync(HttpResponseMessage response, int readSizeLimit, + RecyclableMemoryStreamManager streamManager, CancellationToken cancellationToken) + { +#if NET5_0_OR_GREATER + var streamToReadFrom = await response.Content!.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); +#else + var streamToReadFrom = await response.Content.ReadAsStreamAsync().WaitAsync(cancellationToken).ConfigureAwait(false); +#endif + + var bufferWriter = _bufferWriterPool.Get(); + var memory = bufferWriter.GetMemory(readSizeLimit).Slice(0, readSizeLimit); +#if !NETCOREAPP3_1_OR_GREATER + byte[] buffer = memory.ToArray(); +#endif + try + { +#if NETCOREAPP3_1_OR_GREATER + var charsWritten = await streamToReadFrom.ReadAsync(memory, cancellationToken).ConfigureAwait(false); + bufferWriter.Advance(charsWritten); + return Encoding.UTF8.GetString(memory.Slice(0, charsWritten).Span); +#else + var charsWritten = await streamToReadFrom.ReadAsync(buffer, 0, readSizeLimit, cancellationToken).ConfigureAwait(false); + bufferWriter.Advance(charsWritten); + return Encoding.UTF8.GetString(buffer.AsMemory(0, charsWritten).ToArray()); +#endif + } + finally + { + if (streamToReadFrom.CanSeek) + { + streamToReadFrom.Seek(0, SeekOrigin.Begin); + } + else + { + var freshStream = streamManager.GetStream(); +#if NETCOREAPP3_1_OR_GREATER + var remainingSpace = memory.Slice(bufferWriter.WrittenCount, memory.Length - bufferWriter.WrittenCount); + var writtenCount = await streamToReadFrom.ReadAsync(remainingSpace, cancellationToken) + .ConfigureAwait(false); + + await freshStream.WriteAsync(memory.Slice(0, writtenCount + bufferWriter.WrittenCount), cancellationToken) + .ConfigureAwait(false); +#else + var writtenCount = await streamToReadFrom.ReadAsync(buffer, bufferWriter.WrittenCount, + buffer.Length - bufferWriter.WrittenCount, cancellationToken).ConfigureAwait(false); + + await freshStream.WriteAsync(buffer, 0, writtenCount + bufferWriter.WrittenCount, cancellationToken).ConfigureAwait(false); +#endif + freshStream.Seek(0, SeekOrigin.Begin); + + var newContent = new StreamContent(freshStream); + + foreach (var header in response.Content.Headers) + { + _ = newContent.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + response.Content = newContent; + } + + _bufferWriterPool.Return(bufferWriter); + } + } + + private static async ValueTask ReadFromStreamWithTimeoutAsync(HttpResponseMessage response, TimeSpan readTimeout, + int readSizeLimit, RecyclableMemoryStreamManager streamManager, CancellationToken cancellationToken) + { + using var joinedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + joinedTokenSource.CancelAfter(readTimeout); + + try + { + return await ReadFromStreamAsync(response, readSizeLimit, streamManager, joinedTokenSource.Token) + .ConfigureAwait(false); + } + + // when readTimeout occurred: + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + return Constants.ReadCancelled; + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/IHttpHeadersReader.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/IHttpHeadersReader.cs new file mode 100644 index 0000000000..075ed91892 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/IHttpHeadersReader.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Net.Http; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Internal; + +/// +/// Methods to read HTTP headers. +/// +internal interface IHttpHeadersReader +{ + /// + /// Read HTTP request headers. + /// + /// An instance of to read headers from. + /// Destination to save read headers to. + void ReadRequestHeaders(HttpRequestMessage request, List>? destination); + + /// + /// Read HTTP response headers. + /// + /// An instance of to read headers from. + /// Destination to save read headers to. + void ReadResponseHeaders(HttpResponseMessage response, List>? destination); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/IHttpRequestReader.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/IHttpRequestReader.cs new file mode 100644 index 0000000000..3f9b00898a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/IHttpRequestReader.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Internal; + +/// +/// Methods to read or . +/// +internal interface IHttpRequestReader +{ + /// + /// Reads . + /// + /// A log record object to fill in. + /// HTTP response. + /// A buffer to read response headers to. + /// A cancellation token to cancel operation. + /// A task representing an async operation. + Task ReadResponseAsync(LogRecord record, HttpResponseMessage response, + List>? responseHeadersBuffer, + CancellationToken cancellationToken); + + /// + /// Reads . + /// + /// A log record object to fill in. + /// HTTP request. + /// A buffer to read request headers to. + /// A cancellation token to cancel operation. + /// A task representing an async operation. + Task ReadRequestAsync(LogRecord record, HttpRequestMessage request, + List>? requestHeadersBuffer, + CancellationToken cancellationToken); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/Log.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/Log.cs new file mode 100644 index 0000000000..3f17b582fb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/Log.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Internal; + +/// +/// Logs , and the exceptions due to errors of request/response. +/// +[SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", Justification = "Workaround because Complex object logging does not support this.")] +[SuppressMessage("Major Code Smell", "S109:Magic numbers should not be used", Justification = "Event ID's.")] +internal static partial class Log +{ + public static void OutgoingRequest(ILogger logger, LogLevel level, LogRecord record) + { + OutgoingRequest(logger, level, 1, nameof(OutgoingRequest), record); + } + + public static void OutgoingRequestError(ILogger logger, LogRecord record, Exception exception) + { + OutgoingRequest(logger, LogLevel.Error, 2, nameof(OutgoingRequestError), record, exception); + } + + [LogMethod(3, LogLevel.Error, "An error occurred while enriching the log record.")] + public static partial void EnrichmentError(ILogger logger, Exception exception); + + // Using the code below to avoid every item in ILogger's logRecord State being prefixed with parameter name. + // To be fixed in R9 later. + private static void OutgoingRequest( + ILogger logger, LogLevel level, int eventId, string eventName, LogRecord record, Exception? exception = null) + { + if (logger.IsEnabled(level)) + { + var collector = record.EnrichmentProperties ?? LogMethodHelper.GetHelper(); + + collector.AddRequestHeaders(record.RequestHeaders); + collector.AddResponseHeaders(record.ResponseHeaders); + collector.Add(HttpClientLoggingDimensions.Host, record.Host); + collector.Add(HttpClientLoggingDimensions.Method, record.Method); + collector.Add(HttpClientLoggingDimensions.Path, record.Path); + collector.Add(HttpClientLoggingDimensions.Duration, record.Duration); + + if (record.StatusCode is not null) + { + collector.Add(HttpClientLoggingDimensions.StatusCode, record.StatusCode); + } + + if (!string.IsNullOrEmpty(record.RequestBody)) + { + collector.Add(HttpClientLoggingDimensions.RequestBody, record.RequestBody); + } + + if (!string.IsNullOrEmpty(record.ResponseBody)) + { + collector.Add(HttpClientLoggingDimensions.ResponseBody, record.ResponseBody); + } + + logger.Log( + level, + new(eventId, eventName), + collector, + exception, + static (_, _) => string.Empty); + + // Stryker disable once all + if (collector != record.EnrichmentProperties) + { + LogMethodHelper.ReturnHelper(collector); + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/LogPropertyCollectorExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/LogPropertyCollectorExtensions.cs new file mode 100644 index 0000000000..5c3c0a2c39 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/LogPropertyCollectorExtensions.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Internal; + +internal static class LogPropertyCollectorExtensions +{ + private static readonly ConcurrentDictionary _requestPrefixedNamesCache = new(); + private static readonly ConcurrentDictionary _responsePrefixedNamesCache = new(); + + public static void AddRequestHeaders(this ILogPropertyCollector props, List>? items) + { + if (items is not null) + { + for (var i = 0; i < items.Count; i++) + { + var key = _requestPrefixedNamesCache.GetOrAdd( + items[i].Key, + static (x, p) => p + x, + HttpClientLoggingDimensions.RequestHeaderPrefix); + props.Add(key, items[i].Value); + } + } + } + + public static void AddResponseHeaders(this ILogPropertyCollector props, List>? items) + { + if (items is not null) + { + for (var i = 0; i < items.Count; i++) + { + var key = _responsePrefixedNamesCache.GetOrAdd( + items[i].Key, + static (x, p) => p + x, + HttpClientLoggingDimensions.ResponseHeaderPrefix); + props.Add(key, items[i].Value); + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/LogRecord.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/LogRecord.cs new file mode 100644 index 0000000000..4c45d4cee0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/LogRecord.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Net.Http; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Internal; + +/// +/// Parsed HTTP information. +/// +internal sealed class LogRecord +{ + /// + /// Gets or sets HTTP host. + /// + public string Host { get; set; } = string.Empty; + + /// + /// Gets or sets HTTP request method. + /// + public HttpMethod? Method { get; set; } + + /// + /// Gets or sets parsed request path. + /// + public string Path { get; set; } = string.Empty; + + /// + /// Gets or sets HTTP request duration in milliseconds. + /// + public long Duration { get; set; } + + /// + /// Gets or sets HTTP response status code. + /// + public int? StatusCode { get; set; } + + /// + /// Gets or sets parsed list of request headers. + /// + public List>? RequestHeaders { get; set; } + + /// + /// Gets or sets parsed list of headers. + /// + public List>? ResponseHeaders { get; set; } + + /// + /// Gets or sets parsed request body. + /// + public string RequestBody { get; set; } = string.Empty; + + /// + /// Gets or sets parsed response body. + /// + public string ResponseBody { get; set; } = string.Empty; + + /// + /// Gets or sets enrichment properties. + /// + public LogMethodHelper? EnrichmentProperties { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/LogRecordPooledObjectPolicy.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/LogRecordPooledObjectPolicy.cs new file mode 100644 index 0000000000..14c8c35725 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/LogRecordPooledObjectPolicy.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Internal; + +internal sealed class LogRecordPooledObjectPolicy : PooledObjectPolicy +{ + public override LogRecord Create() => new(); + + public override bool Return(LogRecord record) + { + record.Host = string.Empty; + record.Method = null; + record.Path = string.Empty; + record.Duration = 0; + record.StatusCode = null; + record.RequestBody = string.Empty; + record.ResponseBody = string.Empty; + record.EnrichmentProperties = null; + record.RequestHeaders = null; + record.ResponseHeaders = null; + return true; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/LoggingOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/LoggingOptionsValidator.cs new file mode 100644 index 0000000000..582d87fd87 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/LoggingOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Internal; + +[OptionsValidator] +internal sealed partial class LoggingOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/MediaTypeCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/MediaTypeCollectionExtensions.cs new file mode 100644 index 0000000000..39821bd1e0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/Internal/MediaTypeCollectionExtensions.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Frozen; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Internal; + +// The list of official media types can be found here https://en.wikipedia.org/wiki/Media_type#cite_note-10 +// It is huge and we need better a way to verify allowed / disallowed media types. +// One approach could be memoization of incoming media types and extract this method into +// Http.Logging so LogHttp could also benefit and we prevent repetition. +internal static class MediaTypeCollectionExtensions +{ + private const string Application = "application"; + private const string Json = "+json"; + private const string Xml = "+xml"; + private const string Text = "text/"; + + public static bool Covers(this FrozenSet collection, string? sample) + { + if (!string.IsNullOrEmpty(sample)) + { + if (collection.Contains(sample!)) + { + return true; + } + + if (sample!.StartsWith(Text, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (sample.StartsWith(Application, StringComparison.OrdinalIgnoreCase) + && (sample.EndsWith(Json, StringComparison.OrdinalIgnoreCase) + || sample.EndsWith(Xml, StringComparison.OrdinalIgnoreCase))) + { + return true; + } + } + + return false; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/LoggingOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/LoggingOptions.cs new file mode 100644 index 0000000000..c369dccdaf --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/LoggingOptions.cs @@ -0,0 +1,131 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Shared.Data.Validation; + +namespace Microsoft.Extensions.Http.Telemetry.Logging; + +/// +/// Options to configure HTTP client requests logging. +/// +public class LoggingOptions +{ + private const int MaxIncomingBodySize = 1_572_864; // 1.5 MB + private const int Millisecond = 1; + private const int Hour = 60000 * 60; // 1 hour + private const int DefaultReadSizeLimit = 32 * 1024; // ≈ 32K + private const OutgoingPathLoggingMode DefaultPathLoggingMode = OutgoingPathLoggingMode.Formatted; + private const HttpRouteParameterRedactionMode DefaultPathParameterRedactionMode = HttpRouteParameterRedactionMode.Strict; + + /// + /// Gets or sets a value indicating whether request will be logged additionally before any further processing. + /// + /// + /// When enabled, two entries will be logged for each incoming request - one for request and one for response, if available. + /// When disabled, only one entry will be logged for each incoming request which includes both request and response data. + /// Default set to . + /// + public bool LogRequestStart { get; set; } + + /// + /// Gets or sets a value indicating whether HTTP request and response body will be logged. + /// + /// + /// Please avoid enabling this options in production environment as it might lead to leaking privacy information. + /// Default set to . + /// + public bool LogBody { get; set; } + + /// + /// Gets or sets a value indicating the maximum number of bytes of the request or response body to read. + /// + /// + /// The number should ideally be below 85K to not be allocated on the large object heap. + /// Default set to ≈ 32K. + /// + [Range(1, MaxIncomingBodySize)] + public int BodySizeLimit { get; set; } = DefaultReadSizeLimit; + + /// + /// Gets or sets a value indicating the maximum amount of time to wait for the request or response body to be read. + /// + /// + /// The number should be above 1 millisecond and below 1 hour. + /// Default set to 1 second. + /// + [TimeSpan(Millisecond, Hour)] + public TimeSpan BodyReadTimeout { get; set; } = TimeSpan.FromSeconds(1); + + /// + /// Gets or sets the list of HTTP request content types which are considered text and thus possible to serialize. + /// + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", + Justification = "Options pattern.")] + [Required] + public ISet RequestBodyContentTypes { get; set; } = new HashSet(); + + /// + /// Gets or sets the list of HTTP response content types which are considered text and thus possible to serialize. + /// + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", + Justification = "Options pattern.")] + [Required] + public ISet ResponseBodyContentTypes { get; set; } = new HashSet(); + + /// + /// Gets or sets the set of HTTP request headers to log and their respective data classes to use for redaction. + /// + /// + /// If empty, no HTTP request headers will be logged. + /// If the data class is , no redaction will be done. + /// Default set to . + /// + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", + Justification = "Options pattern.")] + [Required] + public IDictionary RequestHeadersDataClasses { get; set; } = new Dictionary(); + + /// + /// Gets or sets the set of HTTP response headers to log and their respective data classes to use for redaction. + /// + /// + /// If the data class is , no redaction will be done. + /// If empty, no HTTP response headers will be logged. + /// Default set to . + /// + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", + Justification = "Options pattern.")] + [Required] + public IDictionary ResponseHeadersDataClasses { get; set; } = new Dictionary(); + + /// + /// Gets or sets a value indicating how outgoing HTTP request path should be logged. + /// + /// + /// Default set to . + /// This option is applied only when the option is not set to , + /// otherwise this setting is ignored and unredacted HTTP request path is logged. + /// + public OutgoingPathLoggingMode RequestPathLoggingMode { get; set; } = DefaultPathLoggingMode; + + /// + /// Gets or sets a value indicating how outgoing HTTP request path parameters should be redacted. + /// + /// + /// Default set to . + /// + [Experimental] + public HttpRouteParameterRedactionMode RequestPathParameterRedactionMode { get; set; } = DefaultPathParameterRedactionMode; + + /// + /// Gets the route parameters to redact with their corresponding data classes to apply appropriate redaction. + /// + [Required] + public IDictionary RouteParameterDataClasses { get; } = new Dictionary(); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/OutgoingPathLoggingMode.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/OutgoingPathLoggingMode.cs new file mode 100644 index 0000000000..51e355446c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Logging/OutgoingPathLoggingMode.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Http.Telemetry.Logging; + +/// +/// Strategy to decide how outgoing HTTP path is logged. +/// +public enum OutgoingPathLoggingMode +{ + /// + /// HTTP path is formatted, for example in a form of /foo/bar/redactedUserId. + /// + Formatted, + + /// + /// HTTP path is not formatted, route parameters logged in curly braces, for example in a form of /foo/bar/{userId}. + /// + Structured +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HttpClientMeteringExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HttpClientMeteringExtensions.cs new file mode 100644 index 0000000000..3212b6c54e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HttpClientMeteringExtensions.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Shared.Collections; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Telemetry.Metering; + +/// +/// Extension methods for HttpClient.Metering package. />. +/// +/// +public static class HttpClientMeteringExtensions +{ + /// + /// Adds a to collect and emit metrics for outgoing requests from all http clients. + /// + /// + /// This extension configures outgoing request metrics auto collection globally for all http clients. + /// + /// The . + /// + /// instance for chaining. + /// + public static IServiceCollection AddDefaultHttpClientMetering(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + return services + .RegisterMetering() + .AddOutgoingRequestContext() + .ConfigureAll( + httpClientOptions => + { + httpClientOptions + .HttpMessageHandlerBuilderActions.Add(httpMessageHandlerBuilder => + { + var meter = httpMessageHandlerBuilder.Services.GetRequiredService>(); + var outgoingRequestMetricEnrichers = httpMessageHandlerBuilder.Services.GetService>().EmptyIfNull(); + var requestMetadataContext = httpMessageHandlerBuilder.Services.GetService(); + var downstreamDependencyMetadataManager = httpMessageHandlerBuilder.Services.GetService(); + httpMessageHandlerBuilder.AdditionalHandlers.Add(new HttpMeteringHandler(meter, outgoingRequestMetricEnrichers, requestMetadataContext, downstreamDependencyMetadataManager)); + }); + }); + } + + /// + /// Adds a to collect and emit metrics for outgoing requests. + /// + /// The . + /// + /// An that can be used to configure the client. + /// + public static IHttpClientBuilder AddHttpClientMetering(this IHttpClientBuilder builder) + { + _ = Throw.IfNull(builder); + + _ = builder.Services + .RegisterMetering() + .AddOutgoingRequestContext(); + return builder.AddHttpMessageHandler(services => + { + var meter = services.GetRequiredService>(); + var outgoingRequestMetricEnrichers = services.GetService>().EmptyIfNull(); + var requestMetadataContext = services.GetService(); + var downstreamDependencyMetadataManager = services.GetService(); + + return new HttpMeteringHandler(meter, outgoingRequestMetricEnrichers, requestMetadataContext!, downstreamDependencyMetadataManager); + }); + } + + /// + /// Adds an enricher instance of to the to enrich outgoing request metrics. + /// + /// Type of enricher. + /// The to add the instance of to. + /// The so that additional calls can be chained. + public static IServiceCollection AddOutgoingRequestMetricEnricher(this IServiceCollection services) + where T : class, IOutgoingRequestMetricEnricher + { + _ = Throw.IfNull(services); + + _ = services.AddSingleton(); + + return services; + } + + /// + /// Adds to the to enrich outgoing request metrics. + /// + /// The to add to. + /// The instance of to add to . + /// The so that additional calls can be chained. + public static IServiceCollection AddOutgoingRequestMetricEnricher( + this IServiceCollection services, + IOutgoingRequestMetricEnricher enricher) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(enricher); + + _ = services.AddSingleton(enricher); + + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HttpMeteringHandler.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HttpMeteringHandler.cs new file mode 100644 index 0000000000..abe1dd510a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/HttpMeteringHandler.cs @@ -0,0 +1,186 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.Telemetry.Metering.Internal; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Telemetry; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Http.Telemetry.Metering; + +/// +/// Handler that logs outgoing request duration. +/// +/// +public class HttpMeteringHandler : DelegatingHandler +{ + internal TimeProvider TimeProvider = TimeProvider.System; + + private const int StandardDimensionsCount = 4; + private const int MaxCustomDimensionsCount = 14; + + private static readonly RequestMetadata _fallbackMetadata = new(); + + private readonly Histogram _outgoingRequestMetric; + private readonly IOutgoingRequestMetricEnricher[] _requestEnrichers; + private readonly ObjectPool _propertyBagPool = PoolFactory.CreateResettingPool(); + private readonly IOutgoingRequestContext? _requestMetadataContext; + private readonly IDownstreamDependencyMetadataManager? _downstreamDependencyMetadataManager; + private readonly int _enrichersCount; + + /// + /// Initializes a new instance of the class. + /// + /// The meter. + /// Enumerable of outgoing request metric enrichers. + [Experimental] + public HttpMeteringHandler( + Meter meter, + IEnumerable enrichers) + : this(meter, enrichers, null, null) + { + } + + internal HttpMeteringHandler( + Meter meter, + IEnumerable enrichers, + IOutgoingRequestContext? requestMetadataContext, + IDownstreamDependencyMetadataManager? downstreamDependencyMetadataManager = null) + { + _ = Throw.IfNull(meter); + _ = Throw.IfNull(enrichers); + + _requestEnrichers = enrichers.ToArray(); + int dimensionsCount = StandardDimensionsCount; + + foreach (var enricher in _requestEnrichers) + { + _enrichersCount++; + dimensionsCount += enricher.DimensionNames.Count; + } + + if (dimensionsCount > MaxCustomDimensionsCount + StandardDimensionsCount) + { + Throw.ArgumentOutOfRangeException( + $"Total dimensions added by all outgoing request metric enrichers should be smaller than {MaxCustomDimensionsCount}. Observed count: {dimensionsCount - StandardDimensionsCount}", + nameof(enrichers)); + } + + var dimensionsSet = new HashSet + { + Metric.ReqHost, + Metric.DependencyName, + Metric.ReqName, + Metric.RspResultCode + }; + + for (int i = 0; i < _requestEnrichers.Length; i++) + { + foreach (var dimensionName in _requestEnrichers[i].DimensionNames) + { + if (!dimensionsSet.Add(dimensionName)) + { + Throw.ArgumentException(nameof(enrichers), $"A dimension with name {dimensionName} already exists in one of the registered outgoing request metric enrichers"); + } + } + } + + _outgoingRequestMetric = meter.CreateHistogram(Metric.OutgoingRequestMetricName); + + _requestMetadataContext = requestMetadataContext; + _downstreamDependencyMetadataManager = downstreamDependencyMetadataManager; + } + + internal static string GetHostName(HttpRequestMessage request) => string.IsNullOrWhiteSpace(request.RequestUri?.Host) ? TelemetryConstants.Unknown : request.RequestUri!.Host; + + /// + /// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation. + /// + /// The HTTP request message to send to the server. + /// A cancellation token to cancel operation. + /// + /// The task object representing the asynchronous operation. + /// + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + _ = Throw.IfNull(request); + + var timestamp = TimeProvider.GetTimestamp(); + + try + { + var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + OnRequestEnd(request, timestamp, response.StatusCode); + return response; + } + catch + { + // This will not catch a response that returns 4xx, 5xx, etc. but will only catch when base.SendAsync() fails. + OnRequestEnd(request, timestamp, HttpStatusCode.InternalServerError); + throw; + } + } + + private void OnRequestEnd(HttpRequestMessage request, long timestamp, HttpStatusCode statusCode) + { + var requestMetadata = request.GetRequestMetadata() ?? + _requestMetadataContext?.RequestMetadata ?? + _downstreamDependencyMetadataManager?.GetRequestMetadata(request) ?? + _fallbackMetadata; + var dependencyName = requestMetadata.DependencyName; + var requestName = $"{request.Method} {requestMetadata.GetRequestName()}"; + var hostName = GetHostName(request); + var duration = (long)TimeProvider.GetElapsedTime(timestamp, TimeProvider.GetTimestamp()).TotalMilliseconds; + + var tagList = new TagList + { + new(Metric.ReqHost, hostName), + new(Metric.DependencyName, dependencyName), + new(Metric.ReqName, requestName), + new(Metric.RspResultCode, (int)statusCode) + }; + + // keep default case fast by avoiding allocations + if (_enrichersCount == 0) + { + _outgoingRequestMetric.Record(value: duration, tagList); + } + else + { + var propertyBag = _propertyBagPool.Get(); + try + { + foreach (var enricher in _requestEnrichers) + { + enricher.Enrich(propertyBag); + } + + foreach (var item in propertyBag) + { + tagList.Add(item.Key, item.Value); + } + + _outgoingRequestMetric.Record(value: duration, tagList); + } + finally + { + _propertyBagPool.Return(propertyBag); + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/IOutgoingRequestMetricEnricher.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/IOutgoingRequestMetricEnricher.cs new file mode 100644 index 0000000000..fb986fd564 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/IOutgoingRequestMetricEnricher.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Http.Telemetry.Metering; + +/// +/// Interface for implementing enrichers for outgoing request metrics. +/// +public interface IOutgoingRequestMetricEnricher : IMetricEnricher +{ + /// + /// Gets a list of dimension names to enrich outgoing request metrics. + /// + IReadOnlyList DimensionNames { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/HttpClientMeteringConstants.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/HttpClientMeteringConstants.cs new file mode 100644 index 0000000000..d7be135534 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/HttpClientMeteringConstants.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Http.Telemetry.Metering.Internal; + +/// +/// HTTP client metering constants. +/// +internal static class HttpClientMeteringConstants +{ + /// + /// Key used to get/set dependency name in HTTP request message options/properties. + /// We use "R9-" prefix to avoid collisions. + /// + public const string DependencyNameKey = "R9-DependencyName"; + + /// + /// Key used to get/set request name in HTTP request message options/properties. + /// We use "R9-" prefix to avoid collisions. + /// + public const string RequestNameKey = "R9-RequestName"; +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/Metric.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/Metric.cs new file mode 100644 index 0000000000..6409d250ef --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Metering/Internal/Metric.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +namespace Microsoft.Extensions.Http.Telemetry.Metering.Internal; + +internal static partial class Metric +{ + internal const string OutgoingRequestMetricName = @"R9\Http\OutgoingRequest"; + + /// + /// The host of the outgoing request. + /// + internal const string ReqHost = "req_host"; + + /// + /// The name of the target dependency service for the outgoing request. + /// + internal const string DependencyName = "dep_name"; + + /// + /// The name of the outgoing request. + /// + internal const string ReqName = "req_name"; + + /// + /// The response status code for the outgoing request. + /// + /// + /// This is the status code returned by the target dependency service. In case of exceptions, when + /// no status code is available, this will be set to InternalServerError i.e. 500. + /// + internal const string RspResultCode = "rsp_resultCode"; + + /// + /// Creates a new histogram instrument for an outgoing HTTP request. + /// + /// Meter object. + /// + [Histogram(ReqHost, DependencyName, ReqName, RspResultCode, Name = OutgoingRequestMetricName)] + public static partial OutgoingMetric CreateHistogram(Meter meter); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Microsoft.Extensions.Http.Telemetry.csproj b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Microsoft.Extensions.Http.Telemetry.csproj new file mode 100644 index 0000000000..737f31fcdd --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Microsoft.Extensions.Http.Telemetry.csproj @@ -0,0 +1,53 @@ + + + Microsoft.Extensions.Http.Telemetry + Telemetry support for HTTP Client. + Telemetry + + + + true + true + true + true + false + true + false + false + true + false + false + true + false + true + true + + + + normal + 97 + 100 + 78 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Constants.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Constants.cs new file mode 100644 index 0000000000..c98e948028 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Constants.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Http.Telemetry.Tracing; + +internal static class Constants +{ + public const string AttributeHttpPath = "http.path"; + public const string AttributeHttpRoute = "http.route"; + public const string AttributeHttpTarget = "http.target"; + public const string AttributeHttpUrl = "http.url"; + public const string AttributeHttpScheme = "http.scheme"; + public const string AttributeHttpFlavor = "http.flavor"; + public const string AttributeHttpHost = "http.host"; + public const string AttributeNetPeerName = "net.peer.name"; + public const string AttributeNetPeerPort = "net.peer.port"; + public const string AttributeUserAgent = "http.user_agent"; + public const string CustomPropertyHttpRequestMessage = "Tracing.CustomProperty.HttpRequestMessage"; + public const string CustomPropertyHttpResponseMessage = "Tracing.CustomProperty.HttpResponseMessage"; + public const string ActivityStartEvent = "OnStartActivity"; + public const string ActivityStopEvent = "OnStopActivity"; +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientRedactionProcessor.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientRedactionProcessor.cs new file mode 100644 index 0000000000..b1b6ea321d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientRedactionProcessor.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Collections.Frozen; +using System.Diagnostics; +using System.Net.Http; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Http.Telemetry.Tracing.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing; + +internal sealed class HttpClientRedactionProcessor +{ + private readonly ILogger _logger; + private readonly IHttpPathRedactor _httpPathRedactor; + private readonly FrozenDictionary _parametersToRedact; + private readonly ConcurrentDictionary _urlCache = new(); + private readonly IOutgoingRequestContext _requestMetadataContext; + private readonly IDownstreamDependencyMetadataManager? _downstreamDependencyMetadataManager; + private readonly HttpClientTracingOptions _options; + + public HttpClientRedactionProcessor( + IOptions options, + IHttpPathRedactor httpPathRedactor, + IOutgoingRequestContext requestMetadataContext, + ILogger? logger = null, + IDownstreamDependencyMetadataManager? downstreamDependencyMetadataManager = null) + { + _options = Throw.IfNullOrMemberNull(options, options.Value); + _logger = logger ?? NullLogger.Instance; + + _httpPathRedactor = httpPathRedactor; + _requestMetadataContext = requestMetadataContext; + _downstreamDependencyMetadataManager = downstreamDependencyMetadataManager; + + _parametersToRedact = _options.RouteParameterDataClasses.ToFrozenDictionary(StringComparer.Ordinal, optimizeForReading: true); + + _logger.ConfiguredHttpClientTracingOptions(_options); + } + + public void Process(Activity activity, HttpRequestMessage request) + { + // Remove tags that shouldn't be exported as they may contain sensitive information. + _ = activity.SetTag(Constants.AttributeUserAgent, null); + _ = activity.SetTag(Constants.AttributeHttpTarget, null); + _ = activity.SetTag(Constants.AttributeHttpPath, null); + _ = activity.SetTag(Constants.AttributeHttpScheme, null); + _ = activity.SetTag(Constants.AttributeHttpFlavor, null); + _ = activity.SetTag(Constants.AttributeNetPeerName, null); + _ = activity.SetTag(Constants.AttributeNetPeerPort, null); + + if (request.RequestUri == null) + { + HttpTracingEventSource.Instance.HttpRequestUriWasNotSet(activity.OperationName, activity.Id); + _logger.HttpRequestUriWasNotSet(activity.OperationName, activity.Id); + return; + } + + _ = activity.SetTag(Constants.AttributeHttpHost, request.RequestUri.Host); + if (_options.RequestPathParameterRedactionMode == HttpRouteParameterRedactionMode.None) + { + var path = request.RequestUri.AbsolutePath; + _ = activity.DisplayName = path; + _ = activity.SetTag(Constants.AttributeHttpRoute, path); + _ = activity.SetTag(Constants.AttributeHttpUrl, GetFormattedUrl(request.RequestUri, path)); + return; + } + + var httpPath = request.RequestUri.AbsolutePath; + var requestMetadata = request.GetRequestMetadata() ?? + _requestMetadataContext.RequestMetadata ?? + _downstreamDependencyMetadataManager?.GetRequestMetadata(request); + + if (requestMetadata == null) + { + _logger.RequestMetadataIsNotSetForTheRequest(request.RequestUri.AbsoluteUri); + + _ = activity.DisplayName = TelemetryConstants.Unknown; + _ = activity.SetTag(Constants.AttributeHttpRoute, TelemetryConstants.Unknown); + _ = activity.SetTag(Constants.AttributeHttpUrl, GetFormattedUrl(request.RequestUri, TelemetryConstants.Unknown)); + return; + } + + var requestRoute = requestMetadata.RequestRoute; + if (requestRoute == TelemetryConstants.Unknown) + { + _ = activity.DisplayName = requestMetadata.RequestName; + _ = activity.SetTag(Constants.AttributeHttpRoute, requestMetadata.RequestName); + _ = activity.SetTag(Constants.AttributeHttpUrl, GetFormattedUrl(request.RequestUri, requestMetadata.RequestName)); + } + else + { + var redactedPath = _httpPathRedactor.Redact(requestRoute, httpPath, _parametersToRedact, out var routeParameterCount); + + string redactedUrl; + if (routeParameterCount == 0) + { + // Route is either empty or has no parameters. + redactedUrl = _urlCache.GetOrAdd(requestRoute, (_) => GetFormattedUrl(request.RequestUri, redactedPath)); + } + else + { + redactedUrl = GetFormattedUrl(request.RequestUri, redactedPath); + } + + activity.DisplayName = requestMetadata.RequestName == TelemetryConstants.Unknown + ? redactedPath : requestMetadata.RequestName; + + _ = activity.SetTag(Constants.AttributeHttpRoute, requestRoute); + _ = activity.SetTag(Constants.AttributeHttpUrl, redactedUrl); + } + } + +#pragma warning disable S3995 // URI return values should not be strings + private static string GetFormattedUrl(Uri requestUri, string path) + { + if (path.Length > 0 && path[0] == '/') + { + return $"{requestUri.Scheme}{Uri.SchemeDelimiter}{requestUri.Authority}{path}"; + } + else + { + return $"{requestUri.Scheme}{Uri.SchemeDelimiter}{requestUri.Authority}/{path}"; + } + } +#pragma warning restore S3995 // URI return values should not be strings +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientTraceEnrichmentProcessor.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientTraceEnrichmentProcessor.cs new file mode 100644 index 0000000000..436bc18353 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientTraceEnrichmentProcessor.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing; + +internal sealed class HttpClientTraceEnrichmentProcessor +{ + private readonly IHttpClientTraceEnricher[] _traceEnrichers; + + public HttpClientTraceEnrichmentProcessor(IEnumerable traceEnrichers) + { + _traceEnrichers = traceEnrichers.ToArray(); + } + + public void Enrich(Activity activity, HttpRequestMessage request, HttpResponseMessage? response) + { + foreach (var enricher in _traceEnrichers) + { + enricher.Enrich(activity, request, response); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientTracingConstants.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientTracingConstants.cs new file mode 100644 index 0000000000..f13315be08 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientTracingConstants.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Http.Telemetry.Tracing; + +internal static class HttpClientTracingConstants +{ + /// + /// Key used to get/set dependency name in HTTP request message options/properties. + /// We use "R9-" prefix to avoid collisions. + /// + public const string DependencyNameKey = "R9-DependencyName"; + + /// + /// Key used to get/set request name in HTTP request message options/properties. + /// We use "R9-" prefix to avoid collisions. + /// + public const string RequestRouteKey = "R9-RequestRoute"; +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientTracingExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientTracingExtensions.cs new file mode 100644 index 0000000000..4dea5dc8c5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientTracingExtensions.cs @@ -0,0 +1,160 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Http.Telemetry.Tracing.Internal; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Shared.Diagnostics; +using OpenTelemetry.Instrumentation.Http; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing; + +/// +/// Extensions for adding and configuring trace auto collectors for outgoing HTTP requests. +/// +public static class HttpClientTracingExtensions +{ + /// + /// Adds trace auto collector for outgoing HTTP requests. + /// + /// The to add the tracing auto collector. + /// The so that additional calls can be chained. + /// The argument is . + public static TracerProviderBuilder AddHttpClientTracing(this TracerProviderBuilder builder) + { + _ = Throw.IfNull(builder); + + return builder + .ConfigureServices(services => services + .AddValidatedOptions()) + .AddHttpClientTracingInternal(); + } + + /// + /// Adds trace auto collector for outgoing HTTP requests. + /// + /// The to add the tracing auto collector. + /// The configuration delegate. + /// The so that additional calls can be chained. + /// The argument or is . + public static TracerProviderBuilder AddHttpClientTracing(this TracerProviderBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + return builder + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(configure)) + .AddHttpClientTracingInternal(); + } + + /// + /// Adds trace auto collector for outgoing HTTP requests. + /// + /// The to add the tracing auto collector. + /// Configuration section that contains . + /// The so that additional calls can be chained. + /// The argument or is . + public static TracerProviderBuilder AddHttpClientTracing(this TracerProviderBuilder builder, IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + + return builder + .ConfigureServices(services => services + .AddValidatedOptions() + .Bind(section)) + .AddHttpClientTracingInternal(); + } + + /// + /// Adds an enricher that enriches only outgoing HTTP requests traces. + /// + /// Enricher object type. + /// The to add this enricher to. + /// for chaining. + /// The argument is . + [Experimental] + public static IServiceCollection AddHttpClientTraceEnricher(this IServiceCollection services) + where T : class, IHttpClientTraceEnricher + { + _ = Throw.IfNull(services); + + return services.AddSingleton(); + } + + /// + /// Adds an enricher that enriches only outgoing HTTP requests traces. + /// + /// The to add this enricher to. + /// Enricher to be added. + /// for chaining. + /// The argument or is . + [Experimental] + public static IServiceCollection AddHttpClientTraceEnricher(this IServiceCollection services, IHttpClientTraceEnricher enricher) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(enricher); + + return services.AddSingleton(enricher); + } + + /// + /// Adds an enricher that enriches only outgoing HTTP requests traces. + /// + /// Enricher object type. + /// The to add this enricher to. + /// for chaining. + /// is . + public static TracerProviderBuilder AddHttpClientTraceEnricher(this TracerProviderBuilder builder) + where T : class, IHttpClientTraceEnricher + { + _ = Throw.IfNull(builder); + + return builder.ConfigureServices(services => services + .AddHttpClientTraceEnricher()); + } + + /// + /// Adds an enricher that enriches only outgoing HTTP requests traces. + /// + /// The to add this enricher. + /// Enricher to be added. + /// for chaining. + /// The argument or is . + public static TracerProviderBuilder AddHttpClientTraceEnricher(this TracerProviderBuilder builder, IHttpClientTraceEnricher enricher) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(enricher); + + return builder.ConfigureServices(services => services + .AddHttpClientTraceEnricher(enricher)); + } + + private static TracerProviderBuilder AddHttpClientTracingInternal(this TracerProviderBuilder builder) + { + SelfDiagnostics.EnsureInitialized(); + + return builder + .ConfigureServices(services => + { + _ = services + .AddOutgoingRequestContext() + .AddHttpRouteProcessor() + .AddSingleton, ConfigureHttpClientInstrumentationOptions>(); + + services.TryAddSingleton(); + services.TryAddActivatedSingleton(); + services.TryAddActivatedSingleton(); + }) + .AddHttpClientInstrumentation(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientTracingOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientTracingOptions.cs new file mode 100644 index 0000000000..a97b5c3c34 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/HttpClientTracingOptions.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Http.Telemetry; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing; + +/// +/// Options class for providing configuration parameters to configure outgoing HTTP trace auto collection. +/// +public class HttpClientTracingOptions +{ + private const HttpRouteParameterRedactionMode DefaultPathParameterRedactionMode = HttpRouteParameterRedactionMode.Strict; + + /// + /// Gets or sets a value indicating how HTTP request path parameters should be redacted. + /// + /// + /// Default set to . + /// + [Experimental] + public HttpRouteParameterRedactionMode RequestPathParameterRedactionMode { get; set; } = DefaultPathParameterRedactionMode; + + /// + /// Gets or sets a map between HTTP request parameters and their data classification. + /// + /// + /// Default set to empty . + /// If a parameter within a controller's action is not annotated with a data classification attribute and + /// it's not found in this map, it will be redacted as if it was . + /// If the parameter will not contain sensitive information and shouldn't be redacted, mark it as . + /// + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Options pattern.")] + [Required] + public IDictionary RouteParameterDataClasses { get; set; } + = new Dictionary(StringComparer.OrdinalIgnoreCase); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/IHttpClientTraceEnricher.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/IHttpClientTraceEnricher.cs new file mode 100644 index 0000000000..d654e41fd7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/IHttpClientTraceEnricher.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Net.Http; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing; + +/// +/// Interface for implementing enricher for enriching only traces for outgoing HTTP requests. +/// +public interface IHttpClientTraceEnricher +{ + /// + /// Enrich trace with desired tags. + /// + /// object to be used to add the required tags to enrich the traces. + /// HTTP request object associated with the outgoing request for the trace. + /// HTTP response object associated with the outgoing request for the trace. + /// + /// If your enricher fetches some information from or to enrich HTTP traces, + /// then make sure to check them for . + /// + void Enrich(Activity activity, HttpRequestMessage? request, HttpResponseMessage? response); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/IHttpPathRedactor.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/IHttpPathRedactor.cs new file mode 100644 index 0000000000..f15a5465e6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/IHttpPathRedactor.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Compliance.Classification; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing; + +/// +/// Interface for implementing a redaction mechanism for outgoing HTTP request paths. +/// +[Experimental] +public interface IHttpPathRedactor +{ + /// + /// Redact of found in . + /// + /// HTTP route template such as "/api/v1/users/{userId}". + /// HTTP request path such as "/api/v1/users/my-user-id". + /// Parameters to redact, such as "userId". + /// Number of parameters found in . + /// Redacted HTTP request path, such as "/api/v1/users/redacted-user-id". + string Redact(string routeTemplate, string httpPath, IReadOnlyDictionary parametersToRedact, out int parameterCount); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/ConfigureHttpClientInstrumentationOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/ConfigureHttpClientInstrumentationOptions.cs new file mode 100644 index 0000000000..c5b4dbaf03 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/ConfigureHttpClientInstrumentationOptions.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Net.Http; +using Microsoft.Extensions.Options; +using OpenTelemetry.Instrumentation.Http; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing.Internal; + +internal sealed class ConfigureHttpClientInstrumentationOptions : IConfigureOptions +{ + private readonly HttpClientTraceEnrichmentProcessor _enrichmentProcessor; + private readonly HttpClientRedactionProcessor _redactionProcessor; + + public ConfigureHttpClientInstrumentationOptions(HttpClientTraceEnrichmentProcessor enrichmentProcessor, HttpClientRedactionProcessor redactionProcessor) + { + _enrichmentProcessor = enrichmentProcessor; + _redactionProcessor = redactionProcessor; + } + + public void Configure(HttpClientInstrumentationOptions options) + { + options.EnrichWithHttpRequestMessage = (activity, request) => activity.SetCustomProperty(Constants.CustomPropertyHttpRequestMessage, request); + options.EnrichWithHttpResponseMessage = (activity, response) => EnrichAndRedact(activity, response.RequestMessage, response); + options.EnrichWithException = (activity, _) => EnrichAndRedact(activity, (HttpRequestMessage?)activity.GetCustomProperty(Constants.CustomPropertyHttpRequestMessage), null); + } + + private void EnrichAndRedact(Activity activity, HttpRequestMessage? request, HttpResponseMessage? response) + { + if (request is not null) + { + _enrichmentProcessor.Enrich(activity, request, response); + _redactionProcessor.Process(activity, request); + } + + activity.SetCustomProperty(Constants.CustomPropertyHttpRequestMessage, null); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/HttpClientTracingOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/HttpClientTracingOptionsValidator.cs new file mode 100644 index 0000000000..bdae1d856e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/HttpClientTracingOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing.Internal; + +[OptionsValidator] +internal sealed partial class HttpClientTracingOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/HttpPathRedactor.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/HttpPathRedactor.cs new file mode 100644 index 0000000000..f5b1d2adda --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/HttpPathRedactor.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing.Internal; + +internal sealed class HttpPathRedactor : IHttpPathRedactor +{ + private readonly HttpRouteParameterRedactionMode _parameterRedactionMode; + private readonly IHttpRouteFormatter _httpRouteFormatter; + private readonly IHttpRouteParser _httpRouteParser; + + public HttpPathRedactor( + IOptions options, + IHttpRouteFormatter routeFormatter, + IHttpRouteParser httpRouteParser) + { + var opts = Throw.IfNullOrMemberNull(options, options.Value); + _parameterRedactionMode = opts.RequestPathParameterRedactionMode; + _httpRouteFormatter = routeFormatter; + _httpRouteParser = httpRouteParser; + } + + public string Redact(string routeTemplate, string httpPath, IReadOnlyDictionary parametersToRedact, out int parameterCount) + { + parameterCount = 0; + if (!IsRouteValid(routeTemplate)) + { + return TelemetryConstants.Redacted; + } + + var routeSegments = _httpRouteParser.ParseRoute(routeTemplate); + parameterCount = routeSegments.ParameterCount; + return _httpRouteFormatter.Format(routeSegments, httpPath, _parameterRedactionMode, parametersToRedact); + } + + private static bool IsRouteValid(string route) + => !string.IsNullOrEmpty(route) && route != TelemetryConstants.Unknown; +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/HttpTracingEventSource.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/HttpTracingEventSource.cs new file mode 100644 index 0000000000..d38075b129 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/HttpTracingEventSource.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing.Internal; + +[EventSource(Name = "R9-OutgoingHttpTracing-Instrumentation")] +[SuppressMessage("Major Code Smell", "S109:Magic numbers should not be used", Justification = "Event IDs.")] +internal sealed class HttpTracingEventSource : EventSource +{ + internal static readonly HttpTracingEventSource Instance = new(); + + private HttpTracingEventSource() + { + } + + [Event(2, Level = EventLevel.Error, Message = "Outgoing Http Request URI for Activity (Name = '{activityName}', Id = '{activityId}') was not set.")] + public void HttpRequestUriWasNotSet(string activityName, string? activityId) + { + WriteEvent(2, activityName, activityId); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/Log.cs b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/Log.cs new file mode 100644 index 0000000000..0a8020a885 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Telemetry/Tracing/Internal/Log.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing.Internal; + +[SuppressMessage("Major Code Smell", "S109:Magic numbers should not be used", Justification = "Event IDs.")] +internal static partial class Log +{ + /// + /// Logs `Outgoing Http Request URI for Activity (Name = '{activityName}', Id = '{activityId}') was not set.` at `Error` level. + /// + [LogMethod(2, LogLevel.Error, "Outgoing Http Request URI for Activity (Name = '{activityName}', Id = '{activityId}') was not set.")] + public static partial void HttpRequestUriWasNotSet(this ILogger logger, string activityName, string? activityId); + + /// + /// Logs `Request metadata is not set for the request {absoluteUri}` at `Trace` level. + /// + [LogMethod(3, LogLevel.Trace, "Request metadata is not set for the request {absoluteUri}")] + internal static partial void RequestMetadataIsNotSetForTheRequest(this ILogger logger, string absoluteUri); + + /// + /// Logs `Configured HttpClientTracingOptions: {options}` at `Information` level. + /// + [LogMethod(4, LogLevel.Information, "Configured HttpClientTracingOptions: {options}")] + internal static partial void ConfiguredHttpClientTracingOptions(this ILogger logger, HttpClientTracingOptions options); +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/ConfigureContextualOptions.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/ConfigureContextualOptions.cs new file mode 100644 index 0000000000..35faff4002 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/ConfigureContextualOptions.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Options.Contextual; + +/// +/// Configures the type. +/// +/// The type of options configured. +internal sealed class ConfigureContextualOptions : IConfigureContextualOptions + where TOptions : class +{ + private readonly IOptionsContext _context; + + /// + /// Initializes a new instance of the class. + /// + /// The action to apply to configure options. + /// The context used to configure the options. + public ConfigureContextualOptions(Action configureOptions, IOptionsContext context) + { + ConfigureOptions = configureOptions; + _context = context; + } + + /// + /// Gets the delegate used to configure options instances. + /// + public Action ConfigureOptions { get; } + + /// + public void Configure(TOptions options) => ConfigureOptions(_context, Throw.IfNull(options)); + + /// + /// Does nothing. + /// + public void Dispose() + { + // Method intentionally left empty. + } +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/ContextualOptions.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/ContextualOptions.cs new file mode 100644 index 0000000000..7a09dc9046 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/ContextualOptions.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Options.Contextual; + +/// +/// Used to retrieve configured TOptions instances based on a context. +/// +/// The type of options being requested. +internal sealed class ContextualOptions : INamedContextualOptions + where TOptions : class +{ + private readonly IContextualOptionsFactory _factory; + + /// + /// Initializes a new instance of the class. + /// + /// The factory to create instances of with. + public ContextualOptions(IContextualOptionsFactory factory) + { + _factory = factory; + } + + /// + public ValueTask GetAsync(in TContext context, CancellationToken cancellationToken) + where TContext : notnull, IOptionsContext + => GetAsync(Microsoft.Extensions.Options.Options.DefaultName, context, cancellationToken); + + /// + public ValueTask GetAsync(string name, in TContext context, CancellationToken cancellationToken) + where TContext : notnull, IOptionsContext + => _factory.CreateAsync(Throw.IfNull(name), context, cancellationToken); +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/ContextualOptionsFactory.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/ContextualOptionsFactory.cs new file mode 100644 index 0000000000..88cc5ebf8b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/ContextualOptionsFactory.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Options.Contextual; + +/// +/// Implementation of . +/// +/// The type of options being requested. +internal sealed class ContextualOptionsFactory : IContextualOptionsFactory +#if NET5_0_OR_GREATER + where TOptions : class +#else + where TOptions : class, new() +#endif +{ + private readonly IOptionsFactory _baseFactory; + private readonly ILoadContextualOptions[] _loaders; + private readonly IPostConfigureContextualOptions[] _postConfigures; + private readonly IValidateContextualOptions[] _validations; + + /// + /// Initializes a new instance of the class. + /// + /// The factory to create instances of with. + /// The configuration loaders to run. + /// The initialization actions to run. + /// The validations to run. + public ContextualOptionsFactory( + IOptionsFactory baseFactory, + IEnumerable> loaders, + IEnumerable> postConfigures, + IEnumerable> validations) + { + _baseFactory = baseFactory; + _loaders = loaders.ToArray(); + _postConfigures = postConfigures.ToArray(); + _validations = validations.ToArray(); + } + + /// + [SuppressMessage("Reliability", "CA2012:Use ValueTasks correctly", Justification = "The ValueTasks are awaited only once.")] + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "We need to catch it all so we can rethrow it all.")] + public ValueTask CreateAsync(string name, in TContext context, CancellationToken cancellationToken) + where TContext : notnull, IOptionsContext + { + _ = Throw.IfNull(name); + _ = Throw.IfNull(context); + + cancellationToken.ThrowIfCancellationRequested(); + var options = _baseFactory.Create(name); + return ConfigureOptions(context); + + async ValueTask ConfigureOptions(TContext context) + { + var loadTasks = ArrayPool>>.Shared.Rent(_loaders.Length); + var tasksCreated = 0; + List? loadExceptions = null; + + foreach (var loader in _loaders) + { + try + { + loadTasks[tasksCreated] = loader.LoadAsync(name, context, cancellationToken); + tasksCreated++; + } + catch (Exception e) + { + loadExceptions ??= new(); + loadExceptions.Add(e); + break; + } + } + + for (var i = 0; i < tasksCreated; i++) + { + try + { + using var configurer = await loadTasks[i].ConfigureAwait(false); // ValueTasks are awaited only here and only once. + if (!cancellationToken.IsCancellationRequested) + { + configurer.Configure(options); + } + } + catch (Exception e) + { + loadExceptions ??= new(); + loadExceptions.Add(e); + } + finally + { + loadTasks[i] = default; + } + } + + ArrayPool>>.Shared.Return(loadTasks); + + if (loadExceptions is not null) + { + throw new AggregateException(loadExceptions); + } + + cancellationToken.ThrowIfCancellationRequested(); + + foreach (var post in _postConfigures) + { + post.PostConfigure(name, context, options); + } + + List? failures = default; + foreach (var validate in _validations) + { + var result = validate.Validate(name, options); + if (result.Failed) + { + failures ??= new(); +#if NETFRAMEWORK || NETSTANDARD2_0 + failures.Add(result.FailureMessage); +#else + failures.AddRange(result.Failures); +#endif + } + } + + if (failures is not null) + { + throw new OptionsValidationException(name, typeof(TOptions), failures); + } + + return options; + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/ContextualOptionsServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/ContextualOptionsServiceCollectionExtensions.cs new file mode 100644 index 0000000000..c7caad729d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/ContextualOptionsServiceCollectionExtensions.cs @@ -0,0 +1,161 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Options.Contextual; + +/// +/// Extension methods for adding contextual options services to the DI container. +/// +public static class ContextualOptionsServiceCollectionExtensions +{ + /// + /// Adds services required for using contextual options. + /// + /// The to add the services to. + /// The so that additional calls can be chained. + public static IServiceCollection AddContextualOptions(this IServiceCollection services) + { + _ = Throw.IfNull(services).AddOptions(); + + services.TryAdd(ServiceDescriptor.Singleton(typeof(IContextualOptionsFactory<>), typeof(ContextualOptionsFactory<>))); + services.TryAdd(ServiceDescriptor.Singleton(typeof(IContextualOptions<>), typeof(ContextualOptions<>))); + services.TryAdd(ServiceDescriptor.Singleton(typeof(INamedContextualOptions<>), typeof(ContextualOptions<>))); + + return services; + } + + /// + /// Registers an action used to configure a particular type of options. + /// + /// The options type to be configured. + /// The to add the services to. + /// The action used to configure the options. + /// The so that additional calls can be chained. + public static IServiceCollection Configure( + this IServiceCollection services, + Func>> loadOptions) + where TOptions : class + => services.Configure(Microsoft.Extensions.Options.Options.DefaultName, Throw.IfNull(loadOptions)); + + /// + /// Registers an action used to configure a particular type of options. + /// + /// The options type to be configured. + /// The to add the services to. + /// The name of the options to configure. + /// The action used to configure the options. + /// The so that additional calls can be chained. + public static IServiceCollection Configure( + this IServiceCollection services, + string name, + Func>> loadOptions) + where TOptions : class + => services + .AddContextualOptions() + .AddSingleton>( + new LoadContextualOptions( + Throw.IfNull(name), + Throw.IfNull(loadOptions))); + + /// + /// Registers an action used to configure a particular type of options. + /// + /// The options type to be configured. + /// The to add the services to. + /// The action used to configure the options. + /// The so that additional calls can be chained. + public static IServiceCollection Configure(this IServiceCollection services, Action configureOptions) + where TOptions : class + => services.Configure(Microsoft.Extensions.Options.Options.DefaultName, Throw.IfNull(configureOptions)); + + /// + /// Registers an action used to configure a particular type of options. + /// + /// The options type to be configured. + /// The to add the services to. + /// The name of the options to configure. + /// The action used to configure the options. + /// The so that additional calls can be chained. + public static IServiceCollection Configure(this IServiceCollection services, string name, Action configureOptions) + where TOptions : class + { + return services.AddContextualOptions().AddSingleton>( + new LoadContextualOptions( + Throw.IfNull(name), + (context, _) => + new ValueTask>( + new ConfigureContextualOptions(Throw.IfNull(configureOptions), Throw.IfNull(context))))); + } + + /// + /// Registers an action used to initialize all instances of a particular type of options. + /// + /// The options type to be configured. + /// The to add the services to. + /// The action used to configure the options. + /// The so that additional calls can be chained. + public static IServiceCollection PostConfigureAll(this IServiceCollection services, Action configureOptions) + where TOptions : class + => services.PostConfigure(null, Throw.IfNull(configureOptions)); + + /// + /// Registers an action used to initialize a particular type of options. + /// + /// The options type to be configured. + /// The to add the services to. + /// The action used to configure the options. + /// The so that additional calls can be chained. + public static IServiceCollection PostConfigure(this IServiceCollection services, Action configureOptions) + where TOptions : class + => services.PostConfigure(Microsoft.Extensions.Options.Options.DefaultName, Throw.IfNull(configureOptions)); + + /// + /// Registers an action used to initialize a particular type of options. + /// + /// The options type to be configured. + /// The to add the services to. + /// The name of the options instance. + /// The action used to configure the options. + /// The so that additional calls can be chained. + public static IServiceCollection PostConfigure(this IServiceCollection services, string? name, Action configureOptions) + where TOptions : class + => services + .AddContextualOptions() + .AddSingleton>( + new PostConfigureContextualOptions(name, Throw.IfNull(configureOptions))); + + /// + /// Register a validation action for an options type. + /// + /// The options type to be validated. + /// The to add the services to. + /// The validation function. + /// The failure message to use when validation fails. + /// The so that additional calls can be chained. + public static IServiceCollection ValidateContextualOptions(this IServiceCollection services, Func validate, string failureMessage) + where TOptions : class + => services.ValidateContextualOptions(Microsoft.Extensions.Options.Options.DefaultName, Throw.IfNull(validate), Throw.IfNull(failureMessage)); + + /// + /// Register a validation action for an options type. + /// + /// The options type to be validated. + /// The to add the services to. + /// The name of the options instance. + /// The validation function. + /// The failure message to use when validation fails. + /// The so that additional calls can be chained. + public static IServiceCollection ValidateContextualOptions(this IServiceCollection services, string name, Func validate, string failureMessage) + where TOptions : class + => services + .AddContextualOptions() + .AddSingleton>( + new ValidateContextualOptions(Throw.IfNull(name), Throw.IfNull(validate), Throw.IfNull(failureMessage))); +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/IConfigureContextualOptions.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/IConfigureContextualOptions.cs new file mode 100644 index 0000000000..7e488ada72 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/IConfigureContextualOptions.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Options.Contextual; + +/// +/// Represents something that configures the type. +/// +/// The type of options configured. +public interface IConfigureContextualOptions : IDisposable + where TOptions : class +{ + /// + /// Invoked to configure a instance. + /// + /// The options instance to configure. + void Configure(TOptions options); +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/IContextualOptions.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/IContextualOptions.cs new file mode 100644 index 0000000000..92e5073a06 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/IContextualOptions.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Options.Contextual; + +/// +/// Used to retrieve configured instances. +/// +/// The type of options being requested. +public interface IContextualOptions + where TOptions : class +{ + /// + /// Gets the configured instance. + /// + /// A type defining the context for this request. + /// The context that will be used to create the options. + /// The token to monitor for cancellation requests. + /// A configured instance of . + ValueTask GetAsync(in TContext context, CancellationToken cancellationToken) + where TContext : IOptionsContext; +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/IContextualOptionsFactory.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/IContextualOptionsFactory.cs new file mode 100644 index 0000000000..9438803b17 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/IContextualOptionsFactory.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Options.Contextual; + +/// +/// A factory to create instances of . +/// +/// The type of options being created. +public interface IContextualOptionsFactory + where TOptions : class +{ + /// + /// Creates the configured instance. + /// + /// A type defining the context for this request. + /// The name of the options to create. + /// The context that will be used to create the options. + /// The token to monitor for cancellation requests. + /// A configured instance of . + ValueTask CreateAsync(string name, in TContext context, CancellationToken cancellationToken) + where TContext : IOptionsContext; +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/ILoadContextualOptions.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/ILoadContextualOptions.cs new file mode 100644 index 0000000000..14e4d18002 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/ILoadContextualOptions.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Options.Contextual; + +/// +/// Used to retrieve named configuration data from a contextual options provider implementation. +/// +/// The type of options configured. +public interface ILoadContextualOptions + where TOptions : class +{ + /// + /// Gets the data to configure an instance of . + /// + /// A type defining the context for this request. + /// The name of the options to configure. + /// The context that will be used to configure the options. + /// The token to monitor for cancellation requests. + /// An object to configure an instance of . + ValueTask> LoadAsync(string name, in TContext context, CancellationToken cancellationToken) + where TContext : IOptionsContext; +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/INamedContextualOptions.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/INamedContextualOptions.cs new file mode 100644 index 0000000000..e60774ca8b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/INamedContextualOptions.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Options.Contextual; + +/// +/// Used to retrieve named configured instances. +/// +/// The type of options being requested. +public interface INamedContextualOptions : IContextualOptions + where TOptions : class +{ + /// + /// Gets the named configured instance. + /// + /// A type defining the context for this request. + /// The name of the options to get. + /// The context that will be used to create the options. + /// The token to monitor for cancellation requests. + /// A configured instance of . + ValueTask GetAsync(string name, in TContext context, CancellationToken cancellationToken) + where TContext : IOptionsContext; +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/IOptionsContext.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/IOptionsContext.cs new file mode 100644 index 0000000000..4a55aad24c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/IOptionsContext.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Options.Contextual; + +/// +/// The context used to configure contextual options. +/// +public interface IOptionsContext +{ + /// + /// Passes context data to a contextual options provider. + /// + /// The type that the contextual options provider uses to collect context. + /// The object that the contextual options provider uses to collect the context. + void PopulateReceiver(T receiver) + where T : IOptionsContextReceiver; +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/IOptionsContextReceiver.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/IOptionsContextReceiver.cs new file mode 100644 index 0000000000..f3342f0d68 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/IOptionsContextReceiver.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Options.Contextual; + +/// +/// Used by contextual options providers to collect context data. +/// +public interface IOptionsContextReceiver +{ + /// + /// Add a key-value pair to the context. + /// + /// The type of the data. + /// The name of the data. + /// The data used to determine how to populate contextual options. + void Receive(string key, T value); +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/IPostConfigureContextualOptions.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/IPostConfigureContextualOptions.cs new file mode 100644 index 0000000000..77688c4ed1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/IPostConfigureContextualOptions.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Options.Contextual; + +/// +/// Represents something that configures the type. +/// +/// Options type being configured. +public interface IPostConfigureContextualOptions + where TOptions : class +{ + /// + /// Invoked to configure a instance. + /// + /// Options type being configured. + /// The name of the options instance being configured. + /// The context that will be used to configure the options. + /// The options instance to configured. + void PostConfigure(string name, in TContext context, TOptions options) + where TContext : IOptionsContext; +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/IValidateContextualOptions.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/IValidateContextualOptions.cs new file mode 100644 index 0000000000..21ff8899ec --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/IValidateContextualOptions.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Options.Contextual; + +/// +/// Interface used to validate options. +/// +/// The options type to validate. +public interface IValidateContextualOptions + where TOptions : class +{ + /// + /// Validates a specific named options instance (or all when name is null). + /// + /// The name of the options instance being validated. + /// The options instance. + /// The result. + ValidateOptionsResult Validate(string? name, TOptions options); +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/LoadContextualOptions.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/LoadContextualOptions.cs new file mode 100644 index 0000000000..3c7b1d2af3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/LoadContextualOptions.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Options.Contextual; + +/// +/// Used to retrieve named configuration data from a contextual options provider implementation. +/// +/// The type of options configured. +internal sealed class LoadContextualOptions : ILoadContextualOptions + where TOptions : class +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the options instance being configured. If null, this instance will configure options with any name. + /// The delegate used to load configuration data. + public LoadContextualOptions(string? name, Func>> load) + { + Name = name; + LoadAction = load; + } + + /// + /// Gets the name of the options instance this object configures. + /// + public string? Name { get; } + + /// + /// Gets the delegate used to load configuration data. + /// + public Func>> LoadAction { get; } + + /// + public ValueTask> LoadAsync(string name, in TContext context, CancellationToken cancellationToken) + where TContext : notnull, IOptionsContext + { + _ = Throw.IfNull(name); + _ = Throw.IfNull(context); + + if (Name == null || name == Name) + { + return LoadAction(context, cancellationToken); + } + + return new ValueTask>(NullConfigureContextualOptions.GetInstance()); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/Microsoft.Extensions.Options.Contextual.csproj b/src/Libraries/Microsoft.Extensions.Options.Contextual/Microsoft.Extensions.Options.Contextual.csproj new file mode 100644 index 0000000000..6b162a2e18 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/Microsoft.Extensions.Options.Contextual.csproj @@ -0,0 +1,37 @@ + + + Microsoft.Extensions.Options.Contextual + A common abstraction for contextual options. + Config and Experimentation + + + + true + true + + + + dev + 99 + 80 + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/NullConfigureContextualOptions.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/NullConfigureContextualOptions.cs new file mode 100644 index 0000000000..19c35c1611 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/NullConfigureContextualOptions.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Options.Contextual; + +/// +/// Helper class. +/// +public static class NullConfigureContextualOptions +{ + /// + /// Gets a singleton instance of . + /// + /// The options type to configure. + /// A do-nothing instance of . + [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S4049:Properties should be preferred", Justification = "Not possible for generic methods.")] + public static IConfigureContextualOptions GetInstance() + where TOptions : class + => NullConfigureContextualOptions.Instance; +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/NullConfigureContextualOptions_1.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/NullConfigureContextualOptions_1.cs new file mode 100644 index 0000000000..61fa72fe68 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/NullConfigureContextualOptions_1.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Options.Contextual; + +#pragma warning disable SA1649 // File name should match first type name + +/// +/// A do-nothing implementation of . +/// +/// The options type to configure. +internal sealed class NullConfigureContextualOptions : IConfigureContextualOptions + where TOptions : class +{ + internal static IConfigureContextualOptions Instance { get; } = new NullConfigureContextualOptions(); + + /// + public void Configure(TOptions options) + { + // Method intentionally left empty. + } + + /// + /// Does nothing. + /// + public void Dispose() + { + // Method intentionally left empty. + } +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/OptionsContextAttribute.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/OptionsContextAttribute.cs new file mode 100644 index 0000000000..3d20aa5e54 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/OptionsContextAttribute.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; + +namespace Microsoft.Extensions.Options.Contextual; + +/// +/// Generates an implementation of for the annotated type. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)] +[Conditional("CODE_GENERATION_ATTRIBUTES")] +public sealed class OptionsContextAttribute : Attribute +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/PostConfigureContextualOptions.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/PostConfigureContextualOptions.cs new file mode 100644 index 0000000000..7bb75f7188 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/PostConfigureContextualOptions.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Options.Contextual; + +/// +/// Implementation of . +/// +/// Options type being configured. +internal sealed class PostConfigureContextualOptions : IPostConfigureContextualOptions + where TOptions : class +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the options. + /// The action to register. + public PostConfigureContextualOptions(string? name, Action action) + { + Name = name; + Action = action; + } + + /// + /// Gets the options name. + /// + public string? Name { get; } + + /// + /// Gets the initialization action. + /// + public Action Action { get; } + + /// + public void PostConfigure(string name, in TContext context, TOptions options) + where TContext : notnull, IOptionsContext + { + _ = Throw.IfNull(name); + _ = Throw.IfNull(context); + _ = Throw.IfNull(options); + + if (Name == null || name == Name) + { + Action(context, options); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/ValidateContextualOptions.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/ValidateContextualOptions.cs new file mode 100644 index 0000000000..1b8dd97265 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/ValidateContextualOptions.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Options.Contextual; + +/// +/// Implementation of . +/// +/// The options type to validate. +internal sealed class ValidateContextualOptions : ValidateOptions, IValidateContextualOptions + where TOptions : class +{ + /// + /// Initializes a new instance of the class. + /// + /// Options name. + /// Validation function. + /// Validation failure message. + public ValidateContextualOptions(string? name, Func validation, string failureMessage) + : base(name, validation, failureMessage) + { + } +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/buildTransitive/Microsoft.Extensions.Options.Contextual.props b/src/Libraries/Microsoft.Extensions.Options.Contextual/buildTransitive/Microsoft.Extensions.Options.Contextual.props new file mode 100644 index 0000000000..7bc91e4438 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/buildTransitive/Microsoft.Extensions.Options.Contextual.props @@ -0,0 +1,2 @@ + + diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/buildTransitive/Microsoft.Extensions.Options.Contextual.targets b/src/Libraries/Microsoft.Extensions.Options.Contextual/buildTransitive/Microsoft.Extensions.Options.Contextual.targets new file mode 100644 index 0000000000..ceadfacb28 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/buildTransitive/Microsoft.Extensions.Options.Contextual.targets @@ -0,0 +1,3 @@ + + + diff --git a/src/Libraries/Microsoft.Extensions.Options.Validation/Microsoft.Extensions.Options.Validation.csproj b/src/Libraries/Microsoft.Extensions.Options.Validation/Microsoft.Extensions.Options.Validation.csproj new file mode 100644 index 0000000000..be466cb0dc --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Validation/Microsoft.Extensions.Options.Validation.csproj @@ -0,0 +1,38 @@ + + + Microsoft.Extensions.Options.Validation + Support for extended option validation. + Fundamentals + + + + true + true + true + true + + + + normal + 100 + 95 + 92 + + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Options.Validation/OptionsValidatorAttribute.cs b/src/Libraries/Microsoft.Extensions.Options.Validation/OptionsValidatorAttribute.cs new file mode 100644 index 0000000000..c5a8fa15ba --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Validation/OptionsValidatorAttribute.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; + +namespace Microsoft.Extensions.Options.Validation; + +/// +/// Triggers the automatic generation of the implementation of at compile time. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] +[Conditional("CODE_GENERATION_ATTRIBUTES")] +public sealed class OptionsValidatorAttribute : Attribute +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Validation/ValidateEnumeratedItemsAttribute.cs b/src/Libraries/Microsoft.Extensions.Options.Validation/ValidateEnumeratedItemsAttribute.cs new file mode 100644 index 0000000000..11ccd1c22f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Validation/ValidateEnumeratedItemsAttribute.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Options.Validation; + +/// +/// Marks a field or property to be enumerated, and each enumerated object to be validated. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +[Experimental] +public sealed class ValidateEnumeratedItemsAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// Using this constructor for a field/property tells the code generator to + /// generate validation for the individual members of the enumerable's type. + /// + public ValidateEnumeratedItemsAttribute() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// A type that implements for the enumerable's type. + /// + /// Using this constructor for a field/property tells the code generator to use the given type to validate + /// the object held by the enumerable. + /// + public ValidateEnumeratedItemsAttribute(Type validator) + { + Validator = validator; + } + + /// + /// Gets the type to use to validate the enumerable's objects. + /// + public Type? Validator { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Validation/ValidateObjectMembersAttribute.cs b/src/Libraries/Microsoft.Extensions.Options.Validation/ValidateObjectMembersAttribute.cs new file mode 100644 index 0000000000..6bdd46261f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Validation/ValidateObjectMembersAttribute.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Options.Validation; + +/// +/// Marks a field or property to be validated transitively. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +public sealed class ValidateObjectMembersAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// Using this constructor for a field/property tells the code generator to + /// generate validation for the individual members of the field/property's type. + /// + public ValidateObjectMembersAttribute() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// A type that implements for the field/property's type. + /// + /// Using this constructor for a field/property tells the code generator to use the given type to validate + /// the object held by the field/property. + /// + public ValidateObjectMembersAttribute(Type validator) + { + Validator = validator; + } + + /// + /// Gets the type to use to validate a field or property. + /// + public Type? Validator { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Options.Validation/buildTransitive/Microsoft.Extensions.Options.Validation.props b/src/Libraries/Microsoft.Extensions.Options.Validation/buildTransitive/Microsoft.Extensions.Options.Validation.props new file mode 100644 index 0000000000..7bc91e4438 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Validation/buildTransitive/Microsoft.Extensions.Options.Validation.props @@ -0,0 +1,2 @@ + + diff --git a/src/Libraries/Microsoft.Extensions.Options.Validation/buildTransitive/Microsoft.Extensions.Options.Validation.targets b/src/Libraries/Microsoft.Extensions.Options.Validation/buildTransitive/Microsoft.Extensions.Options.Validation.targets new file mode 100644 index 0000000000..ceadfacb28 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Options.Validation/buildTransitive/Microsoft.Extensions.Options.Validation.targets @@ -0,0 +1,3 @@ + + + diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/FaultInjectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/FaultInjectionExtensions.cs new file mode 100644 index 0000000000..162cb7d634 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/FaultInjectionExtensions.cs @@ -0,0 +1,188 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +/// +/// Provides extension methods for Fault-Injection library. +/// +public static class FaultInjectionExtensions +{ + private const string ChaosPolicyOptionsGroupName = "ChaosPolicyOptionsGroupName"; + + /// + /// Registers default implementations for and . + /// + /// The services collection. + /// + /// The so that additional calls can be chained. + /// + /// + /// All parameters cannot be null. + /// + public static IServiceCollection AddFaultInjection(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + return services.AddFaultInjection(builder => builder.Configure()); + } + + /// + /// Configures and registers default implementations for + /// and . + /// + /// The services collection. + /// + /// The configuration section to bind to . + /// + /// + /// The so that additional calls can be chained. + /// + /// + /// All parameters cannot be null. + /// + public static IServiceCollection AddFaultInjection(this IServiceCollection services, IConfiguration section) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(section); + + return services.AddFaultInjection( + builder => builder.Configure(section)); + } + + /// + /// Calls the given action to configure and registers default implementations for + /// and . + /// + /// The services collection. + /// Function to configure . + /// + /// The so that additional calls can be chained. + /// + /// + /// All parameters cannot be null. + /// + public static IServiceCollection AddFaultInjection(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + var builder = new FaultInjectionOptionsBuilder(services); + configure.Invoke(builder); + + _ = services.RegisterMetering(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + return services; + } + + /// + /// Associates the given instance to the given identifier name + /// for an registered at . + /// + /// The context instance. + /// The identifier name for an . + /// + /// The so that additional calls can be chained. + /// + /// + /// All parameters must not be null. + /// + public static Context WithFaultInjection(this Context context, string groupName) + { + _ = Throw.IfNull(context); + _ = Throw.IfNull(groupName); + + context[ChaosPolicyOptionsGroupName] = groupName; + + return context; + } + + /// + /// Associates to the calling instance, where + /// will be used to determine which to use at each fault-injection run. + /// + /// The context instance. + /// The fault policy weight assignment. + /// + /// The so that additional calls can be chained. + /// + /// + /// All parameters must not be null. + /// + [Experimental] + public static Context WithFaultInjection(this Context context, FaultPolicyWeightAssignmentsOptions weightAssignments) + { + _ = Throw.IfNull(context); + _ = Throw.IfNull(weightAssignments); + + context[ChaosPolicyOptionsGroupName] = weightAssignments.WeightAssignments + .OrderBy(pair => pair.Value) + .ToDictionary(pair => pair.Key, pair => pair.Value); + + return context; + } + + /// + /// Gets the name of the registered from . + /// + /// The context instance. + /// + /// The if registered; null if it isn't. + /// + /// + /// All parameters must not be null. + /// + public static string? GetFaultInjectionGroupName(this Context context) + { + _ = Throw.IfNull(context); + + if (context.TryGetValue(ChaosPolicyOptionsGroupName, out var contextObj)) + { + if (contextObj is string name) + { + return name; + } + else if (contextObj is Dictionary weightAssignments) + { + return GetGroupNameFromWeightAssignments(weightAssignments); + } + } + + return null; + } + + private static string? GetGroupNameFromWeightAssignments(Dictionary weightAssignments) + { + var maxValue = WeightAssignmentHelper.GetWeightSum(weightAssignments); + var randNum = WeightAssignmentHelper.GenerateRandom(maxValue); + var accumulatedVal = 0.0; + string result = null!; + + foreach (var entry in weightAssignments) + { + accumulatedVal += entry.Value; + if (WeightAssignmentHelper.IsUnderMax(randNum, accumulatedVal)) + { + result = entry.Key; + break; + } + } + + return result; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/FaultInjectionOptionsBuilder.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/FaultInjectionOptionsBuilder.cs new file mode 100644 index 0000000000..464f4cc6bd --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/FaultInjectionOptionsBuilder.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +/// +/// Builder class to provide options configuration methods for +/// and . +/// +public class FaultInjectionOptionsBuilder +{ + private readonly IServiceCollection _services; + + /// + /// Initializes a new instance of the class. + /// + /// The services collection. + /// + /// All parameters cannot be null. + /// + public FaultInjectionOptionsBuilder(IServiceCollection services) + { + _services = Throw.IfNull(services); + } + + /// + /// Configures default . + /// + /// + /// The builder object itself so that additional calls can be chained. + /// + public FaultInjectionOptionsBuilder Configure() + { + _ = _services + .AddValidatedOptions(); + return this; + } + + /// + /// Configures through + /// the provided . + /// + /// + /// The configuration section to bind to . + /// + /// The builder object itself so that additional calls can be chained. + /// + /// All parameters cannot be null. + /// + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(FaultInjectionOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed by [DynamicDependency]")] + public FaultInjectionOptionsBuilder Configure(IConfiguration section) + { + _ = Throw.IfNull(section); + + _ = _services + .AddValidatedOptions() + .Bind(section); + + return this; + } + + /// + /// Configures through + /// the provided configure. + /// + /// + /// The function to be registered to configure . + /// + /// The builder object itself so that additional calls can be chained. + /// + /// All parameters cannot be null. + /// + public FaultInjectionOptionsBuilder Configure(Action configureOptions) + { + _ = Throw.IfNull(configureOptions); + + _ = _services + .AddValidatedOptions() + .Configure(configureOptions); + + return this; + } + + /// + /// Add an exception instance to . + /// + /// The identifier for the exception instance to be added. + /// The exception instance to be added. + /// The builder object itself so that additional calls can be chained. + /// + /// The exception cannot be null. + /// + /// + /// The key must not be an empty string or null. + /// + public FaultInjectionOptionsBuilder AddException(string key, Exception exception) + { + _ = Throw.IfNull(exception); + _ = Throw.IfNullOrWhitespace(key); + + _ = _services.Configure(key, o => o.Exception = exception); + + return this; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/IChaosPolicyFactory.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/IChaosPolicyFactory.cs new file mode 100644 index 0000000000..7161e0f90b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/IChaosPolicyFactory.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Polly; +using Polly.Contrib.Simmy.Latency; +using Polly.Contrib.Simmy.Outcomes; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +/// +/// Factory for chaos policy creation. +/// +public interface IChaosPolicyFactory +{ + /// + /// Creates an async latency policy with delegate functions to fetch fault injection + /// settings from . + /// + /// The type of value policies created by this method will inject. + /// + /// A latency policy, + /// an instance of . + /// + public AsyncInjectLatencyPolicy CreateLatencyPolicy(); + + /// + /// Creates an async exception policy with delegate functions to fetch + /// fault injection settings from . + /// + /// + /// An exception policy, + /// an instance of . + /// + public AsyncInjectOutcomePolicy CreateExceptionPolicy(); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/IFaultInjectionOptionsProvider.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/IFaultInjectionOptionsProvider.cs new file mode 100644 index 0000000000..3b6736b63e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/IFaultInjectionOptionsProvider.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +/// +/// Interface for fault-injection options provider implementations. +/// +/// +/// A fault-injection options provider is intended to retain chaos policy configurations and +/// to allow instances to be retrieved by other services. +/// +public interface IFaultInjectionOptionsProvider +{ + /// + /// Get an instance of from the provider by the options group name. + /// + /// The chaos policy options group name. + /// + /// The associated with the options group name if it is found; otherwise, null. + /// + /// + /// True if the associated with the options group name if it is found; otherwise, false. + /// + public bool TryGetChaosPolicyOptionsGroup(string optionsGroupName, [NotNullWhen(true)] out ChaosPolicyOptionsGroup? optionsGroup); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/InjectedFaultException.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/InjectedFaultException.cs new file mode 100644 index 0000000000..1d9b76d827 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/InjectedFaultException.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +/// +/// An exception class that should only be used for fault injection purposes. +/// +public class InjectedFaultException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public InjectedFaultException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + public InjectedFaultException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, + /// or a null reference (Nothing in Visual Basic) if no inner exception is specified. + /// + public InjectedFaultException(string message, Exception innerException) + : base(message, innerException) + { + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/ChaosPolicyFactory.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/ChaosPolicyFactory.cs new file mode 100644 index 0000000000..4a5a1a835e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/ChaosPolicyFactory.cs @@ -0,0 +1,196 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Metering; +using Polly; +using Polly.Contrib.Simmy; +using Polly.Contrib.Simmy.Latency; +using Polly.Contrib.Simmy.Outcomes; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +/// +/// Default implementation of . +/// +internal sealed class ChaosPolicyFactory : IChaosPolicyFactory +{ + private const string FaultTypeLatency = "Latency"; + private const string FaultTypeException = "Exception"; + + private readonly Task _enabled = Task.FromResult(true); + private readonly Task _notEnabled = Task.FromResult(false); + private readonly Task _noInjectionRate = Task.FromResult(0); + + private readonly ILogger _logger; + private readonly FaultInjectionMetricCounter _counter; + private readonly IFaultInjectionOptionsProvider _optionsProvider; + private readonly IExceptionRegistry _exceptionRegistry; + + private readonly Func> _getLatencyAsync; + private readonly Func> _getInjectionRateAsync; + private readonly Func> _getEnabledAsync; + private readonly Func> _getExceptionAsync; + private readonly Func> _getInjectionRateAsyncEx; + private readonly Func> _getEnabledAsyncEx; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The meter. + /// The provider of . + /// + /// The registry that contains registered exception instances for fault-injection. + /// + [ExcludeFromCodeCoverage] + public ChaosPolicyFactory(ILogger logger, Meter meter, + IFaultInjectionOptionsProvider optionsProvider, IExceptionRegistry exceptionRegistry) + { + _logger = logger; + _counter = Metric.CreateFaultInjectionMetricCounter(meter); + _optionsProvider = optionsProvider; + _exceptionRegistry = exceptionRegistry; + + _getLatencyAsync = GetLatencyAsync; + _getInjectionRateAsync = GetInjectionRateAsync; + _getEnabledAsync = GetEnabledAsync; + _getExceptionAsync = GetExceptionAsync; + _getInjectionRateAsyncEx = GetInjectionRateAsync; + _getEnabledAsyncEx = GetEnabledAsync; + } + + /// + public AsyncInjectLatencyPolicy CreateLatencyPolicy() => + MonkeyPolicy.InjectLatencyAsync(with => + with.Latency(_getLatencyAsync) + .InjectionRate(_getInjectionRateAsync) + .EnabledWhen(_getEnabledAsync)); + + /// + public AsyncInjectOutcomePolicy CreateExceptionPolicy() => + MonkeyPolicy.InjectExceptionAsync(with => + with.Fault(_getExceptionAsync) + .InjectionRate(_getInjectionRateAsyncEx) + .EnabledWhen(_getEnabledAsyncEx)); + + /// + /// Task for checking if fault-injection is enabled from the 's associated chaos policy options. + /// + internal Task GetEnabledAsync(Context context, CancellationToken _0) + { + var groupName = context.GetFaultInjectionGroupName(); + if (groupName == null) + { + return _notEnabled; + } + + _ = _optionsProvider.TryGetChaosPolicyOptionsGroup(groupName, out var optionsGroup); + if (optionsGroup == null) + { + return _notEnabled; + } + + ChaosPolicyOptionsBase? options = null; + if (typeof(TOptions) == typeof(LatencyPolicyOptions)) + { + options = optionsGroup.LatencyPolicyOptions; + } + else if (typeof(TOptions) == typeof(ExceptionPolicyOptions)) + { + options = optionsGroup.ExceptionPolicyOptions; + } + + if (options == null || !options.Enabled) + { + return _notEnabled; + } + + return _enabled; + } + + /// + /// Task for checking the injection rate from the 's associated chaos policy options. + /// + internal Task GetInjectionRateAsync(Context context, CancellationToken _0) + { + var groupName = context.GetFaultInjectionGroupName(); + if (groupName == null) + { + return _noInjectionRate; + } + + _ = _optionsProvider.TryGetChaosPolicyOptionsGroup(groupName, out var optionsGroup); + if (optionsGroup == null) + { + return _noInjectionRate; + } + + ChaosPolicyOptionsBase? options = null; + if (typeof(TOptions) == typeof(LatencyPolicyOptions)) + { + options = optionsGroup.LatencyPolicyOptions; + } + else if (typeof(TOptions) == typeof(ExceptionPolicyOptions)) + { + options = optionsGroup.ExceptionPolicyOptions; + } + + if (options == null) + { + return _noInjectionRate; + } + + return Task.FromResult(options.FaultInjectionRate); + } + + /// + /// Fault provider task for . + /// + /// + /// This task only gets executed when LatencyPolicyOptions is defined at the options group and is enabled, + /// as defined in . + /// See how faults are injected at . + /// + internal Task GetLatencyAsync(Context context, CancellationToken _0) + { + var groupName = context.GetFaultInjectionGroupName()!; + _ = _optionsProvider.TryGetChaosPolicyOptionsGroup(groupName, out var optionsGroup); + + var latency = optionsGroup!.LatencyPolicyOptions!.Latency; + + FaultInjectionTelemetryHandler.LogAndMeter( + _logger, _counter, groupName, + FaultTypeLatency, latency.ToString()); + + return Task.FromResult(latency); + } + + /// + /// Fault provider task for . + /// + /// + /// This task only gets executed when ExceptionPolicyOptions is defined at the options group and is enabled, + /// as defined in . + /// If exception is null, the result will simply be ignored by Simmy's AsyncMonkeyEngine. + /// See how faults are injected at . + /// + internal Task GetExceptionAsync(Context context, CancellationToken _0) + { + var groupName = context.GetFaultInjectionGroupName()!; + _ = _optionsProvider.TryGetChaosPolicyOptionsGroup(groupName, out var optionsGroup); + + // Exception is not going to be null + var exception = _exceptionRegistry.GetException(optionsGroup!.ExceptionPolicyOptions!.ExceptionKey); + + FaultInjectionTelemetryHandler.LogAndMeter( + _logger, _counter, groupName, + FaultTypeException, exception.GetType().FullName!); + + return Task.FromResult(exception); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/ExceptionRegistry.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/ExceptionRegistry.cs new file mode 100644 index 0000000000..f9cead2208 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/ExceptionRegistry.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +/// +/// Default implementation for . +/// +internal sealed class ExceptionRegistry : IExceptionRegistry +{ + private readonly IOptionsMonitor _options; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The instance to retrieve from. + /// + /// + /// All parameters must not be null. + /// + public ExceptionRegistry(IOptionsMonitor options) + { + _options = options; + } + + /// + public Exception GetException(string key) + { + _ = Throw.IfNull(key); + + return _options.Get(key).Exception; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/FaultInjectionEventMeterDimensions.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/FaultInjectionEventMeterDimensions.cs new file mode 100644 index 0000000000..2681e235e0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/FaultInjectionEventMeterDimensions.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +/// +/// FaultInjection event metric counter key names. +/// +internal static class FaultInjectionEventMeterDimensions +{ + /// + /// Client using fault injection library. + /// + public const string FaultInjectionGroupName = "FaultInjectionGroupName"; + + /// + /// Type of fault injected, e.g, latency, exception, etc. + /// + public const string FaultType = "FaultType"; + + /// + /// Value corresponding to injected fault, e.g., NotFonudException, 200ms. + /// + public const string InjectedValue = "InjectedValue"; +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/FaultInjectionOptionsProvider.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/FaultInjectionOptionsProvider.cs new file mode 100644 index 0000000000..b1223a8d5e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/FaultInjectionOptionsProvider.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +/// +/// Default implementation for . +/// +internal sealed class FaultInjectionOptionsProvider : IFaultInjectionOptionsProvider +{ + private readonly IOptionsMonitor _optionsMonitor; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The options monitor instance to retrieve chaos policy related configurations from. + /// + /// + /// All parameters must not be null. + /// + public FaultInjectionOptionsProvider(IOptionsMonitor optionsMonitor) + { + _optionsMonitor = optionsMonitor; + } + + /// + public bool TryGetChaosPolicyOptionsGroup(string optionsGroupName, [NotNullWhen(true)] out ChaosPolicyOptionsGroup? optionsGroup) + { + _ = Throw.IfNull(optionsGroupName); + + if (!_optionsMonitor.CurrentValue.ChaosPolicyOptionsGroups.TryGetValue(optionsGroupName, out optionsGroup)) + { + return false; + } + + return true; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/FaultInjectionOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/FaultInjectionOptionsValidator.cs new file mode 100644 index 0000000000..12a92ab73e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/FaultInjectionOptionsValidator.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +internal sealed class FaultInjectionOptionsValidator : IValidateOptions +{ + // This is 100% code coverage. It's likely due to a bug in code coverage that line 19 is marked as not fully covered. + // Microsoft.Extensions.Resilience.FaultInjection.Test.Options.OptionsValidationTests includes tests to cover this method. + [ExcludeFromCodeCoverage] + public static IEnumerable GenerateFailureMessages(ICollection validationResults) + { + foreach (var result in validationResults) + { + yield return $"{result.ErrorMessage ?? "Unknown Error"}"; + } + } + + public ValidateOptionsResult Validate(string? name, FaultInjectionOptions options) + { + foreach (var keyValuePair in options.ChaosPolicyOptionsGroups) + { + var optionsGroup = keyValuePair.Value; + if (optionsGroup.LatencyPolicyOptions != null && + !ValidatePolicyOption(optionsGroup.LatencyPolicyOptions, out var latencyOptionsResults)) + { + throw new OptionsValidationException(name!, typeof(FaultInjectionOptions), GenerateFailureMessages(latencyOptionsResults)); + } + + if (optionsGroup.HttpResponseInjectionPolicyOptions != null && + !ValidatePolicyOption(optionsGroup.HttpResponseInjectionPolicyOptions, out var httpOptionsResults)) + { + throw new OptionsValidationException(name!, typeof(FaultInjectionOptions), GenerateFailureMessages(httpOptionsResults)); + } + + if (optionsGroup.ExceptionPolicyOptions != null && !ValidatePolicyOption(optionsGroup.ExceptionPolicyOptions, out var exceptionOptionsResults)) + { + throw new OptionsValidationException(name!, typeof(FaultInjectionOptions), GenerateFailureMessages(exceptionOptionsResults)); + } + } + + return ValidateOptionsResult.Success; + } + + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed with [DynamicallyAddressedMembers]")] + private static bool ValidatePolicyOption<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T obj, out ICollection validationResults) + where T : notnull + { + validationResults = new List(); + var validationContext = new ValidationContext(obj, null, null); + return Validator.TryValidateObject(obj, validationContext, validationResults, true); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/FaultInjectionTelemetryHandler.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/FaultInjectionTelemetryHandler.cs new file mode 100644 index 0000000000..a84517be33 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/FaultInjectionTelemetryHandler.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +internal static class FaultInjectionTelemetryHandler +{ + public static void LogAndMeter( + ILogger logger, FaultInjectionMetricCounter counter, string groupName, string faultType, string injectedValue) + { + Log.LogInjection(logger, groupName, faultType, injectedValue); + counter.Add(1, groupName, faultType, injectedValue); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/IExceptionRegistry.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/IExceptionRegistry.cs new file mode 100644 index 0000000000..82e53f2082 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/IExceptionRegistry.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +/// +/// The interface of a registry class implementation for exception instances +/// registration and retrieval. +/// +internal interface IExceptionRegistry +{ + /// + /// Gets an exception from the registry by key. + /// + /// The identifier for a registered exception instance. + /// + /// The registered exception instance identified by the given key. + /// Returns an instance of by + /// default if no exception instance with the given key is found. + /// + public Exception GetException(string key); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/Log.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/Log.cs new file mode 100644 index 0000000000..8c6fe74515 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/Log.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +internal static partial class Log +{ + [LogMethod(0, LogLevel.Information, + "Fault-injection group name: {groupName}. " + + "Fault type: {faultType}. " + + "Injected value: {injectedValue}. ")] + public static partial void LogInjection( + ILogger logger, + string groupName, + string faultType, + string injectedValue); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/Metric.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/Metric.cs new file mode 100644 index 0000000000..ec5bf2eb66 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/Metric.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +internal static partial class Metric +{ + [Counter( + FaultInjectionEventMeterDimensions.FaultInjectionGroupName, + FaultInjectionEventMeterDimensions.FaultType, + FaultInjectionEventMeterDimensions.InjectedValue, + Name = @"R9\Resilience\FaultInjection\InjectedFaults")] + public static partial FaultInjectionMetricCounter CreateFaultInjectionMetricCounter(Meter meter); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/WeightAssignmentHelper.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/WeightAssignmentHelper.cs new file mode 100644 index 0000000000..93c9083f24 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Internals/WeightAssignmentHelper.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +internal static class WeightAssignmentHelper +{ + public static double GetWeightSum(Dictionary weightAssignments) + { + return weightAssignments.Sum(pair => pair.Value); + } + + [SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "Not intended to be cryptographically secure.")] + public static double GenerateRandom(double maxValue) + { + var random = new Random(); + return random.NextDouble() * maxValue; + } + + public static bool IsUnderMax(double value, double maxValue) + { + return value <= maxValue; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/ChaosPolicyOptionsBase.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/ChaosPolicyOptionsBase.cs new file mode 100644 index 0000000000..ec5c2692c6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/ChaosPolicyOptionsBase.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +/// +/// Chaos policy options base class. +/// +public class ChaosPolicyOptionsBase +{ + internal const double DefaultInjectionRate = 0.1; + internal const bool DefaultEnabled = false; + + /// + /// Initializes a new instance of the class. + /// + protected ChaosPolicyOptionsBase() + { + } + + /// + /// Gets or sets a value indicating whether + /// a chaos policy should be enabled or not. + /// + /// + /// Default is set to . + /// + public bool Enabled { get; set; } = DefaultEnabled; + + /// + /// Gets or sets the injection rate. + /// + /// + /// The value should be a decimal between 0 and 1 inclusive, + /// and it indicates the rate at which a chaos policy injects faults. + /// 0 indicates an injection rate of 0% while 1 indicates an injection rate of 100%. + /// Default is set to 0.1. + /// + [Range(0, 1)] + public double FaultInjectionRate { get; set; } = DefaultInjectionRate; +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/ChaosPolicyOptionsGroup.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/ChaosPolicyOptionsGroup.cs new file mode 100644 index 0000000000..1b3e87d8fb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/ChaosPolicyOptionsGroup.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +/// +/// Class for chaos policy options group. +/// +public class ChaosPolicyOptionsGroup +{ + /// + /// Gets or sets the latency policy options of the chaos policy options group. + /// + /// + /// Default set to . + /// + [ValidateObjectMembers] + public LatencyPolicyOptions? LatencyPolicyOptions { get; set; } + + /// + /// Gets or sets the http response injection policy options of the chaos policy options group. + /// + /// + /// Default set to . + /// + [ValidateObjectMembers] + public HttpResponseInjectionPolicyOptions? HttpResponseInjectionPolicyOptions { get; set; } + + /// + /// Gets or sets the exception policy options of the chaos policy options group. + /// + /// + /// Default set to . + /// + [ValidateObjectMembers] + public ExceptionPolicyOptions? ExceptionPolicyOptions { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/ExceptionPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/ExceptionPolicyOptions.cs new file mode 100644 index 0000000000..75f4ece039 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/ExceptionPolicyOptions.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +/// +/// Class for exception policy options definition. +/// +public class ExceptionPolicyOptions : ChaosPolicyOptionsBase +{ + /// + /// The key for the default exception instance in the registry. + /// + internal const string DefaultExceptionKey = "DefaultException"; + + /// + /// Gets or sets the exception key. + /// + /// + /// This key is used for fetching an exception instance + /// from . + /// Default is set to "DefaultException". + /// + public string ExceptionKey { get; set; } = DefaultExceptionKey; +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/FaultInjectionExceptionOptions.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/FaultInjectionExceptionOptions.cs new file mode 100644 index 0000000000..65c7bf94a9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/FaultInjectionExceptionOptions.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +internal sealed class FaultInjectionExceptionOptions +{ + public Exception Exception { get; set; } = new InjectedFaultException(); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/FaultInjectionOptions.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/FaultInjectionOptions.cs new file mode 100644 index 0000000000..cdc97f47a3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/FaultInjectionOptions.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +/// +/// Class to contain fault injection options provider option values loaded from configuration sources. +/// +public class FaultInjectionOptions +{ + /// + /// Gets or sets the dictionary that stores . + /// + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Options pattern.")] + public IDictionary ChaosPolicyOptionsGroups { get; set; } = new Dictionary(); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/FaultPolicyWeightAssignmentsOptions.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/FaultPolicyWeightAssignmentsOptions.cs new file mode 100644 index 0000000000..05254afa45 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/FaultPolicyWeightAssignmentsOptions.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +/// +/// Class to contain fault-injection policy weight assignments. +/// +[Experimental] +public class FaultPolicyWeightAssignmentsOptions +{ + /// + /// Gets or sets the dictionary that defines fault policy weight assignments. + /// + /// + /// The key of an entry shall be the identifier name of a chaos policy, while the value of an entry shall be the weight value for the chaos policy. + /// The weight value ranges from 0 to 100, with 0 translates to 0% while 100 translates to 100%. And the total weight shall add up to 100. + /// + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Options pattern.")] + public IDictionary WeightAssignments { get; set; } = new Dictionary(); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/HttpResponseInjectionPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/HttpResponseInjectionPolicyOptions.cs new file mode 100644 index 0000000000..44f8391277 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/HttpResponseInjectionPolicyOptions.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; +using System.Net; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +/// +/// Class for http response injection policy options definition. +/// +public class HttpResponseInjectionPolicyOptions : ChaosPolicyOptionsBase +{ + internal const HttpStatusCode DefaultStatusCode = HttpStatusCode.BadGateway; + + /// + /// Gets or sets the status code to inject. + /// + /// + /// Default is set to . + /// + [EnumDataType(typeof(HttpStatusCode))] + public HttpStatusCode StatusCode { get; set; } = DefaultStatusCode; + + /// + /// Gets or sets the key to retrieve custom response settings. + /// + /// + /// This field is optional and it defaults to null. + /// + public string? HttpContentKey { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/LatencyPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/LatencyPolicyOptions.cs new file mode 100644 index 0000000000..e3a6c1d547 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/FaultInjection/Options/LatencyPolicyOptions.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Data.Validation; + +namespace Microsoft.Extensions.Resilience.FaultInjection; + +/// +/// Class for latency policy options definition. +/// +public class LatencyPolicyOptions : ChaosPolicyOptionsBase +{ + internal static readonly TimeSpan DefaultLatency = TimeSpan.FromSeconds(30); + + /// + /// Gets or sets the latency value to inject. + /// + /// + /// The value should be between 0 seconds to 10 minutes. + /// Default is set to 30 seconds. + /// + [TimeSpan("00:00:00", "00:10:00")] + public TimeSpan Latency { get; set; } = DefaultLatency; +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Microsoft.Extensions.Resilience.csproj b/src/Libraries/Microsoft.Extensions.Resilience/Microsoft.Extensions.Resilience.csproj new file mode 100644 index 0000000000..b98539e081 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Microsoft.Extensions.Resilience.csproj @@ -0,0 +1,51 @@ + + + Microsoft.Extensions.Resilience + Mechanisms to harden applications against transient failures. + Resilience + + + + true + true + true + true + true + true + true + true + true + true + true + + + + normal + 100 + 100 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/FailureResultContext.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/FailureResultContext.cs new file mode 100644 index 0000000000..ad41d6165f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/FailureResultContext.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience; + +/// +/// Object model capturing the dimensions metered for a transient failure result. +/// +#pragma warning disable CA1815 // Override equals and operator equals on value types (Such usage is not expected in this scenario) +public readonly struct FailureResultContext +#pragma warning restore CA1815 +{ + /// + /// Initializes a new instance of the structure. + /// + /// The source of the failure. + /// The reason of the failure. + /// Additional information for the failure. + /// object. + public static FailureResultContext Create( + string failureSource = TelemetryConstants.Unknown, + string failureReason = TelemetryConstants.Unknown, + string additionalInformation = TelemetryConstants.Unknown) + => new(failureSource, failureReason, additionalInformation); + + private FailureResultContext(string failureSource, string failureReason, string additionalInformation) + { + FailureSource = Throw.IfNullOrEmpty(failureSource); + FailureReason = Throw.IfNullOrEmpty(failureReason); + AdditionalInformation = Throw.IfNullOrEmpty(additionalInformation); + } + + /// + /// Gets the source of the failure presented in delegate result. + /// + public string FailureSource { get; } + + /// + /// Gets the reason of the failure presented in delegate result. + /// + public string FailureReason { get; } + + /// + /// Gets additional information of the failure presented in delegate result. + /// + public string AdditionalInformation { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/FallbackScenarioTaskArguments.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/FallbackScenarioTaskArguments.cs new file mode 100644 index 0000000000..26c9a9f1e9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/FallbackScenarioTaskArguments.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Structure with the arguments of the on bulkhead task. +/// +#pragma warning disable CA1815 // Override equals and operator equals on value types (Such usage is not expected in this scenario) +public readonly struct FallbackScenarioTaskArguments : IPolicyEventArguments +#pragma warning restore CA1815 +{ + /// + /// Initializes a new instance of the structure. + /// + /// The policy context. + /// The cancellation token. + public FallbackScenarioTaskArguments(Context context, CancellationToken cancellationToken) + { + Context = Throw.IfNull(context); + CancellationToken = cancellationToken; + } + + /// + /// Gets the Polly associated with the policy execution. + /// + public Context Context { get; } + + /// + /// Gets the cancellation token associated with the policy execution. + /// + public CancellationToken CancellationToken { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/FallbackScenarioTaskProvider.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/FallbackScenarioTaskProvider.cs new file mode 100644 index 0000000000..9bc59d4298 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/FallbackScenarioTaskProvider.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Options; +using Polly.Fallback; + +namespace Microsoft.Extensions.Resilience; + +/// +/// A delegate that executes in the fallback scenarios when the initial execution encounters a failure. +/// +/// Arguments for the fallback scenario task provider. See . +/// A task representing asynchronous operation. +/// +/// +public delegate Task FallbackScenarioTaskProvider(FallbackScenarioTaskArguments args); diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/FallbackScenarioTaskProviderT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/FallbackScenarioTaskProviderT.cs new file mode 100644 index 0000000000..53180d0aa9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/FallbackScenarioTaskProviderT.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Options; +using Polly.Fallback; + +namespace Microsoft.Extensions.Resilience; + +#pragma warning disable SA1649 // File name should match first type name + +/// +/// A delegate that executes in the fallback scenarios when the initial execution encounters a failure. +/// +/// Type of the result returned. +/// Arguments for the fallback scenario task provider. See . +/// Result of a fallback task. +/// +/// +public delegate Task FallbackScenarioTaskProvider(FallbackScenarioTaskArguments args); diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/GlobalSuppressions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/GlobalSuppressions.cs new file mode 100644 index 0000000000..5ce4d7eee2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/GlobalSuppressions.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Misc", "CS8002", Justification = "Referenced assemblies from within R9 SDK without strong name are accepted")] diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/HedgedTaskProvider.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/HedgedTaskProvider.cs new file mode 100644 index 0000000000..9399d318ec --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/HedgedTaskProvider.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Resilience; + +/// +/// A delegate used by the hedging policy to determine whether the next hedged task can be created. +/// +/// Arguments for the hedged task provider. See . +/// Hedged task created by the provider. if the task was not created. +/// if a hedged task is created, otherwise. +public delegate bool HedgedTaskProvider(HedgingTaskProviderArguments args, out Task? result); diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/HedgedTaskProviderT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/HedgedTaskProviderT.cs new file mode 100644 index 0000000000..030eaf5d8f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/HedgedTaskProviderT.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Resilience; + +#pragma warning disable SA1649 // File name should match first type name + +/// +/// A delegate used by the hedging policy to determine whether the next hedged task can be created. +/// +/// Type of result returned. +/// Arguments for the hedged task provider. See . +/// Hedged task created by the provider. if the task was not created. +/// if a hedged task is created, otherwise. +public delegate bool HedgedTaskProvider(HedgingTaskProviderArguments args, out Task? result); diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/AsyncHedgingPolicy.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/AsyncHedgingPolicy.cs new file mode 100644 index 0000000000..f596a50f25 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/AsyncHedgingPolicy.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Options; +using Polly; + +namespace Microsoft.Extensions.Resilience.Hedging; + +/// +/// A hedging policy that can be applied to delegates. +/// +/// The return type of delegates which may be executed through the policy. +internal sealed class AsyncHedgingPolicy : AsyncPolicy, IsPolicy +{ + private readonly HedgingEngineOptions _hedgingEngineOptions; + private readonly HedgedTaskProvider _hedgedTaskProvider; + + internal AsyncHedgingPolicy( + PolicyBuilder policyBuilder, + HedgedTaskProvider hedgedTaskProvider, + int maxHedgedTasks, + Func hedgingDelayGenerator, + Func, Context, int, CancellationToken, Task> onHedgingAsync) + : base(policyBuilder) + { + _hedgedTaskProvider = hedgedTaskProvider; + _hedgingEngineOptions = new HedgingEngineOptions( + maxHedgedTasks, + hedgingDelayGenerator, + ExceptionPredicates, + ResultPredicates.None, + onHedgingAsync); + } + + protected override async Task ImplementationAsync( + Func> action, + Context context, + CancellationToken cancellationToken, + bool continueOnCapturedContext) + { + cancellationToken.ThrowIfCancellationRequested(); + + TResult? result = default; + + _ = await HedgingEngine.ExecuteAsync( + async (ctx, ct) => + { + result = await action(ctx, ct).ConfigureAwait(continueOnCapturedContext); + return EmptyStruct.Instance; + }, + context, + _hedgedTaskProvider, + _hedgingEngineOptions, + continueOnCapturedContext, + cancellationToken).ConfigureAwait(continueOnCapturedContext); + +#pragma warning disable CS8603 // Possible null reference return. + return result; +#pragma warning restore CS8603 // Possible null reference return. + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/AsyncHedgingPolicyT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/AsyncHedgingPolicyT.cs new file mode 100644 index 0000000000..307cb94d9b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/AsyncHedgingPolicyT.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Options; +using Polly; + +namespace Microsoft.Extensions.Resilience.Hedging; + +#pragma warning disable SA1649 // File name should match first type name + +/// +/// A hedging policy that can be applied to delegates. +/// +/// +/// The return type of delegates which may be executed through the policy. +/// +internal sealed class AsyncHedgingPolicy : AsyncPolicy, IsPolicy +{ + private readonly HedgingEngineOptions _hedgingEngineOptions; + private readonly HedgedTaskProvider _hedgedTaskProvider; + + internal AsyncHedgingPolicy( + PolicyBuilder policyBuilder, + HedgedTaskProvider hedgedTaskProvider, + int maxHedgedTasks, + Func hedgingDelayGenerator, + Func, Context, int, CancellationToken, Task> onHedgingAsync) + : base(policyBuilder) + { + _hedgedTaskProvider = hedgedTaskProvider; + _hedgingEngineOptions = new HedgingEngineOptions( + maxHedgedTasks, + hedgingDelayGenerator, + ExceptionPredicates, + ResultPredicates, + onHedgingAsync); + } + + /// + protected override Task ImplementationAsync( + Func> action, + Context context, + CancellationToken cancellationToken, + bool continueOnCapturedContext) + { + cancellationToken.ThrowIfCancellationRequested(); + + return HedgingEngine.ExecuteAsync( + action, + context, + _hedgedTaskProvider, + _hedgingEngineOptions, + continueOnCapturedContext, + cancellationToken); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/AsyncHedgingSyntax.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/AsyncHedgingSyntax.cs new file mode 100644 index 0000000000..dad1214ace --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/AsyncHedgingSyntax.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Options; +using Polly; + +namespace Microsoft.Extensions.Resilience.Hedging; + +/// +/// Fluent API for defining a hedging . +/// +internal static class AsyncHedgingSyntax +{ + /// + /// Builds an which provides the fastest result + /// returned from a set of tasks (i.e. hedged execution) if the main execution fails or is too slow. + /// If this throws a handled exception or raises a handled result, + /// if asynchronously calls + /// with details of the handled exception or result and the execution context; + /// Then will continue to wait and check for the first allowed of task provided by + /// the and returns its result; + /// If none of the tasks returned by returns an allowed result, + /// the last handled exception or result will be returned. + /// + /// The type of the result. + /// The policy builder. + /// The hedged action provider. + /// The maximum hedged tasks. + /// The delegate that provides the hedging delay for each hedged task. + /// The action to call asynchronously after invoking one hedged task. + /// + /// The policy instance. + /// + /// Arguments cannot be null. + public static AsyncHedgingPolicy AsyncHedgingPolicy( + this PolicyBuilder policyBuilder, + HedgedTaskProvider hedgedTaskProvider, + int maxHedgedTasks, + Func hedgingDelayGenerator, + Func, Context, int, CancellationToken, Task> onHedgingAsync) + { + return new AsyncHedgingPolicy( + policyBuilder, + hedgedTaskProvider, + maxHedgedTasks, + hedgingDelayGenerator, + onHedgingAsync); + } + + public static AsyncHedgingPolicy AsyncHedgingPolicy( + this PolicyBuilder policyBuilder, + HedgedTaskProvider hedgedTaskProvider, + int maxHedgedTasks, + Func hedgingDelayGenerator, + Func onHedgingAsync) + { + return new AsyncHedgingPolicy( + policyBuilder, + WrapProvider(hedgedTaskProvider), + maxHedgedTasks, + hedgingDelayGenerator, + (ex, ctx, task, token) => onHedgingAsync(ex.Exception, ctx, task, token)); + } + + internal static HedgedTaskProvider WrapProvider(HedgedTaskProvider provider) + { + return WrappedProvider; + + bool WrappedProvider(HedgingTaskProviderArguments args, [NotNullWhen(true)] out Task? result) + { + if (provider(args, out var hedgingTask) && hedgingTask is not null) + { + result = hedgingTask.ContinueWith(task => EmptyStruct.Instance, TaskScheduler.Default); + return true; + } + + result = null; + return false; + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/EmptyStruct.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/EmptyStruct.cs new file mode 100644 index 0000000000..dcb52b8b75 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/EmptyStruct.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Resilience.Hedging; + +/// +/// A null struct for policies and actions which do not return a TResult. +/// +internal readonly struct EmptyStruct +{ + /// + /// Initializes a new instance of the EmptyStruct for policies which do not return a result." /> structure. + /// + public static readonly EmptyStruct Instance; +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/HedgingEngine.WhenAny.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/HedgingEngine.WhenAny.cs new file mode 100644 index 0000000000..7790791ecb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/HedgingEngine.WhenAny.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +#pragma warning disable R9A061 + +namespace Microsoft.Extensions.Resilience.Hedging; + +internal static partial class HedgingEngine +{ + private static Task> WhenAnyAsync(Dictionary, CancellationPair>.KeyCollection tasks) + { +#pragma warning disable S109 // Magic numbers should not be used + return tasks.Count switch + { + 1 => WhenAny1Async(tasks), + 2 => WhenAny2Async(tasks), + _ => Task.WhenAny(tasks) + }; +#pragma warning restore S109 // Magic numbers should not be used + + static async Task> WhenAny1Async(Dictionary, CancellationPair>.KeyCollection tasks) + { + using var enumerator = tasks.GetEnumerator(); + _ = enumerator.MoveNext(); + + try + { + _ = await enumerator.Current.ConfigureAwait(false); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception) +#pragma warning restore CA1031 // Do not catch general exception types + { + // discard exception and propagate it in task + } + + return enumerator.Current; + } + + static Task> WhenAny2Async(Dictionary, CancellationPair>.KeyCollection tasks) + { + using var enumerator = tasks.GetEnumerator(); + + _ = enumerator.MoveNext(); + var first = enumerator.Current; + + _ = enumerator.MoveNext(); + var second = enumerator.Current; + + return Task.WhenAny(first, second); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/HedgingEngine.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/HedgingEngine.cs new file mode 100644 index 0000000000..622a2a0cb0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/HedgingEngine.cs @@ -0,0 +1,309 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Shared.Pools; +using Polly; +using Polly.Utilities; + +namespace Microsoft.Extensions.Resilience.Hedging; + +/// +/// Approach consistent with the retry policy engine. +/// . +/// +/// The type of the result handled by the policy delegate. +internal static partial class HedgingEngine +{ + private static readonly ObjectPool, CancellationPair>> _dictionaryPool = PoolFactory.CreateDictionaryPool, CancellationPair>(); + private static readonly ObjectPool _cancellationSources = PoolFactory.CreateCancellationTokenSourcePool(); + + public static async Task ExecuteAsync( + Func> primaryHedgedTask, + Context context, + HedgedTaskProvider hedgedTaskProvider, + HedgingEngineOptions options, + bool continueOnCapturedContext, + CancellationToken cancellationToken) + { + var loadedHedgedTasks = 0; + var hedgedTasks = _dictionaryPool.Get(); + + DelegateResult? previousResult = null; + + try + { + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + + bool loaded = TryLoadNextHedgedTask(primaryHedgedTask, context, hedgedTaskProvider, options, ref loadedHedgedTasks, hedgedTasks, cancellationToken); + + if (!loaded && hedgedTasks.Count == 0) + { + if (previousResult!.Exception is not null) + { + ExceptionDispatchInfo.Capture(previousResult.Exception).Throw(); + } + + return previousResult.Result; + } + + var completedHedgedTask = + await WaitForTaskAsync( + options, + context, + loadedHedgedTasks, + hedgedTasks.Keys, + continueOnCapturedContext, + cancellationToken).ConfigureAwait(continueOnCapturedContext); + + if (completedHedgedTask == null) + { + // If completedHedgedTask is null it indicates that we still do not have any finished hedged task within the hedging delay. + // We will create additional hedged task in the next iteration. + continue; + } + + // Drop the source for completed task and dispose it, so it won't be canceled later + if (hedgedTasks.Remove(completedHedgedTask, out var source)) + { + source.Dispose(); + } + + if (previousResult != null) + { + DisposeResult(previousResult.Result); + } + + try + { + // Note: Task.WhenAny *does not* throw. + // To fetch the possible exception, the actual task must be awaited. + var result = await completedHedgedTask.ConfigureAwait(continueOnCapturedContext); + if (!options.ShouldHandleResultPredicates.AnyMatch(result)) + { + return result; + } + + previousResult = new DelegateResult(result); + } + catch (Exception ex) + { + var handledException = options.ShouldHandleExceptionPredicates.FirstMatchOrDefault(ex); + if (handledException is null) + { + throw; + } + + previousResult = new DelegateResult(ex); + } + + // If nothing has been returned or thrown yet, the result is a transient failure, + // and other hedged request will be awaited. + // Before it, one needs to perform the task adjacent to each hedged call. + try + { + await options.OnHedgingAsync(previousResult, context, loadedHedgedTasks, cancellationToken).ConfigureAwait(continueOnCapturedContext); + } + catch (Exception) + { + DisposeResult(previousResult.Result); + throw; + } + } + } + finally + { + CleanupPendingTasks(hedgedTasks); + } + } + + private static bool TryLoadNextHedgedTask( + Func> primaryHedgedTask, + Context context, + HedgedTaskProvider hedgedTaskProvider, + HedgingEngineOptions options, + ref int loadedHedgedTasks, + Dictionary, CancellationPair> hedgedTasks, + CancellationToken cancellationToken) + { + if (loadedHedgedTasks >= options.MaxHedgedTasks) + { + return false; + } + + var pair = CancellationPair.Create(cancellationToken); + + Task? task; + try + { + if (loadedHedgedTasks == 0) + { + task = primaryHedgedTask(context, pair.CancellationToken); + } + + // Stryker disable once Logical: https://domoreexp.visualstudio.com/R9/_workitems/edit/2804465 + else if (!hedgedTaskProvider(new HedgingTaskProviderArguments(context, loadedHedgedTasks, pair.CancellationToken), out task) || task is null) + { + pair.Dispose(); + return false; + } + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + // this was handled exception, continue with hedging + task = Task.FromException(ex); + } + + loadedHedgedTasks++; + hedgedTasks.Add(task, pair); + + // Stryker disable once boolean : no means to test this + return true; + } + + private static async Task?> WaitForTaskAsync( + HedgingEngineOptions options, + Context context, + int loadedHedgedTasks, + Dictionary, CancellationPair>.KeyCollection hedgedTasks, + bool continueOnCapturedContext, + CancellationToken cancellationToken) + { + // before doing anything expensive, let's check whether any existing task is already completed + foreach (var task in hedgedTasks) + { + if (task.IsCompleted) + { + return task; + } + } + + if (loadedHedgedTasks == options.MaxHedgedTasks) + { + return await WhenAnyAsync(hedgedTasks).ConfigureAwait(continueOnCapturedContext); + } + + var hedgingDelay = options.HedgingDelayGenerator(new HedgingDelayArguments(context, loadedHedgedTasks - 1, cancellationToken)); + + if (hedgingDelay == TimeSpan.Zero) + { + // just load the next task + return null; + } + + // Stryker disable once equality : no means to test this, stryker changes '<' to '<=' where 0 is already covered in the branch above + if (hedgingDelay < TimeSpan.Zero) + { + // this disables the hedging, we wait for first finished task + return await WhenAnyAsync(hedgedTasks).ConfigureAwait(continueOnCapturedContext); + } + + using var delayTaskCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + var delayTask = SystemClock.SleepAsync(hedgingDelay, delayTaskCancellation.Token); + var whenAnyHedgedTask = WhenAnyAsync(hedgedTasks); + var completedTask = await Task.WhenAny(whenAnyHedgedTask, delayTask).ConfigureAwait(continueOnCapturedContext); + + if (completedTask == delayTask) + { + return null; + } + + // cancel the ongoing delay task + // Stryker disable once boolean : no means to test this +#if NET8_0_OR_GREATER + await delayTaskCancellation.CancelAsync().ConfigureAwait(continueOnCapturedContext); +#else + delayTaskCancellation.Cancel(throwOnFirstException: false); +#endif + + return await whenAnyHedgedTask.ConfigureAwait(continueOnCapturedContext); + } + + private static void CleanupPendingTasks(Dictionary, CancellationPair> tasks) + { + // Stryker disable once equality : there is no way to check that the tasks were returned to pool + if (tasks.Count == 0) + { + _dictionaryPool.Return(tasks); + return; + } + + // first, cancel any pending requests + foreach (var pair in tasks) + { + pair.Value.Cancellation.Cancel(); + } + + // We are intentionally doing the cleanup in the background as we do not want to + // delay the hedging. + // The background cleanup is safe. All exceptions are handled. + _ = CleanupInBackgroundAsync(tasks); + + static async Task CleanupInBackgroundAsync(Dictionary, CancellationPair> tasks) + { + foreach (var task in tasks) + { + try + { + var result = await task.Key.ConfigureAwait(false); + DisposeResult(result); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception) +#pragma warning restore CA1031 // Do not catch general exception types + { + // The tasks are spawned inside of HedgingHandler and possible exceptions are handled on ExecuteAsync method. + // During the dispose phase we swallow the exception for every task that is running. + // They are almost guaranteed to throw since they get canceled. + } + + // dispose cancellation token source linked with this task + task.Value.Dispose(); + } + + _dictionaryPool.Return(tasks); + } + } + + private static void DisposeResult(TResult? result) + { + if (result is IDisposable disposableResult) + { + disposableResult.Dispose(); + } + } + + internal record struct CancellationPair(CancellationTokenSource Cancellation, CancellationTokenRegistration? Registration) : IDisposable + { + public CancellationToken CancellationToken => Cancellation.Token; + + public static CancellationPair Create(CancellationToken token) + { + var currentCancellation = _cancellationSources.Get(); + + if (token.CanBeCanceled) + { + return new CancellationPair(currentCancellation, token.Register(o => ((CancellationTokenSource)o!).Cancel(), currentCancellation)); + } + + return new CancellationPair(currentCancellation, null); + } + + public void Dispose() + { + Registration?.Dispose(); + _cancellationSources.Return(Cancellation); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/HedgingEngineOptions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/HedgingEngineOptions.cs new file mode 100644 index 0000000000..999bb513c6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Hedging/HedgingEngineOptions.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Options; +using Polly; + +namespace Microsoft.Extensions.Resilience.Hedging; + +internal sealed class HedgingEngineOptions +{ + public int MaxHedgedTasks { get; } + + public Func HedgingDelayGenerator { get; } + + public ExceptionPredicates ShouldHandleExceptionPredicates { get; } + + public ResultPredicates ShouldHandleResultPredicates { get; } + + public Func, Context, int, CancellationToken, Task> OnHedgingAsync { get; } + + public HedgingEngineOptions( + int maxHedgedTasks, + Func hedgingDelayGenerator, + ExceptionPredicates shouldHandleExceptionPredicates, + ResultPredicates shouldHandleResultPredicates, + Func, Context, int, CancellationToken, Task> onHedgingAsync) + { + MaxHedgedTasks = maxHedgedTasks; + ShouldHandleExceptionPredicates = shouldHandleExceptionPredicates; + ShouldHandleResultPredicates = shouldHandleResultPredicates; + OnHedgingAsync = onHedgingAsync; + HedgingDelayGenerator = hedgingDelayGenerator; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/HedgingTaskProviderArguments.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/HedgingTaskProviderArguments.cs new file mode 100644 index 0000000000..0a9ac562fd --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/HedgingTaskProviderArguments.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience; + +/// +/// A wrapper that holds current request's +/// and the current hedging attempt number. +/// +[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Comparing instances is not an expected scenario")] +public readonly struct HedgingTaskProviderArguments : IPolicyEventArguments +{ + /// + /// Initializes a new instance of the struct. + /// + /// Current request's context. + /// Count of already executed hedging attempts. + /// The cancellation token. + public HedgingTaskProviderArguments(Context context, int attemptNumber, CancellationToken cancellationToken) + { + _ = Throw.IfNull(context); + + Context = context; + AttemptNumber = attemptNumber; + CancellationToken = cancellationToken; + } + + /// + /// Gets the hedging attempt number. + /// + /// The attempt number starts with the 1 as is used after the primary hedging attempt is executed. + public int AttemptNumber { get; } + + /// + /// Gets the Polly associated with the policy execution. + /// + public Context Context { get; } + + /// + /// Gets the cancellation token associated with the policy execution. + /// + public CancellationToken CancellationToken { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/ContextExtensions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/ContextExtensions.cs new file mode 100644 index 0000000000..36aa48c867 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/ContextExtensions.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Polly; + +namespace Microsoft.Extensions.Resilience.Internal; + +internal static class ContextExtensions +{ + public static string GetPolicyPipelineName(this Context context) + { + // Stryker disable once all: https://domoreexp.visualstudio.com/R9/_workitems/edit/2804465 + return context.PolicyWrapKey ?? context.PolicyKey ?? string.Empty; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/FailureEventMetricsOptions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/FailureEventMetricsOptions.cs new file mode 100644 index 0000000000..e688fb57b6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/FailureEventMetricsOptions.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Resilience.Internal; + +internal sealed class FailureEventMetricsOptions +{ + public Func GetContextFromResult { get; set; } = (_) => FailureResultContext.Create(); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/FailureReasonResolver.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/FailureReasonResolver.cs new file mode 100644 index 0000000000..d6a489cbfb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/FailureReasonResolver.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Net; +using System.Net.Http; +using Microsoft.Extensions.EnumStrings; +using Polly; + +[assembly: EnumStrings(typeof(HttpStatusCode))] + +namespace Microsoft.Extensions.Resilience.Internal; + +/// +/// Static methods to extract the failure reason of a call sent using polices. +/// +internal static class FailureReasonResolver +{ + private const string Undefined = "Undefined"; + private static readonly ConcurrentDictionary _statusCodeCache = new(); + + /// + /// Gets the failure reason. + /// + /// The type of the result. + /// The result. + /// Reason string defining the failure's reason. + public static string GetFailureReason(DelegateResult result) + { + if (Equals(result.Result, default(TResult)) && Equals(result.Exception, null)) + { + return Undefined; + } + + var errMessage = GetFailureFromException(result.Exception); + + if (errMessage == Undefined) + { + var msg = result.Result as HttpResponseMessage; + return msg == null ? Undefined : $"Status code: {GetOrAddStatusCodeString(msg.StatusCode, () => msg.StatusCode.ToInvariantString())}"; + } + + return errMessage; + } + + public static string GetFailureFromException(Exception e) + { + var errMessage = e?.Message; + if (!string.IsNullOrWhiteSpace(errMessage)) + { + return $"Error: {errMessage}"; + } + + return Undefined; + } + + private static string GetOrAddStatusCodeString(HttpStatusCode key, Func createItem) + { + _ = _statusCodeCache.TryAdd(key, createItem()); + return _statusCodeCache[key]; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/IPolicyFactory.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/IPolicyFactory.cs new file mode 100644 index 0000000000..eee3e29a51 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/IPolicyFactory.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Resilience.Options; +using Polly; + +namespace Microsoft.Extensions.Resilience.Internal; + +/// +/// Factory interface for policy creation. +/// +internal interface IPolicyFactory +{ + /// + /// Sets the pipeline identifiers. + /// + /// The pipeline id. + void Initialize(PipelineId pipelineId); + + /// + /// Creates an advanced circuit breaker policy which allow granular customization of the fault tolerance. + /// + /// The policy name. + /// The configuration. + /// A circuit breaker policy. + /// + /// Reacts on proportion of failures (i.e. failureThreshold) by measuring the data within a custom interval (i.e. sampling duration) + /// Imposes a minimal time interval before acting (i.e. minimumThroughput) and configurable break duration. + /// + /// + IAsyncPolicy CreateCircuitBreakerPolicy(string policyName, CircuitBreakerPolicyOptions options); + + /// + /// Creates an advanced circuit breaker policy which allow granular customization of the fault tolerance. + /// + /// The policy name. + /// The configuration. + /// The type of the result returned by the action executed by the policy. + /// A circuit breaker policy. + /// + /// Reacts on proportion of failures (i.e. failureThreshold) by measuring the data within a custom interval (i.e. sampling duration) + /// Imposes a minimal time interval before acting (i.e. minimumThroughput) and configurable break duration. + /// + /// + IAsyncPolicy CreateCircuitBreakerPolicy(string policyName, CircuitBreakerPolicyOptions options); + + /// + /// Creates a retry policy. + /// + /// The policy name. + /// The configuration of the retry policy, . + /// A retry policy. + IAsyncPolicy CreateRetryPolicy(string policyName, RetryPolicyOptions options); + + /// + /// Creates a retry policy. + /// + /// The policy name. + /// The configuration of the retry policy, . + /// The type of the result returned by the action executed by the policy. + /// A retry policy. + IAsyncPolicy CreateRetryPolicy(string policyName, RetryPolicyOptions options); + + /// + /// Creates a fallback policy to provide a substitute action in the event of failure. + /// . + /// + /// The policy name. + /// The task performed in the fallback scenario when the initial execution encounters a transient failure. + /// The options of the fallback policy. + /// + /// A fallback policy. + /// + public IAsyncPolicy CreateFallbackPolicy( + string policyName, + FallbackScenarioTaskProvider provider, + FallbackPolicyOptions options); + + /// + /// Creates a fallback policy to provide a substitute action in the event of failure. + /// . + /// + /// The policy name. + /// The task performed in the fallback scenario when the initial execution encounters a transient failure. + /// The options of the fallback policy. + /// The type of the result returned by the action executed by the policy. + /// + /// A fallback policy. + /// + public IAsyncPolicy CreateFallbackPolicy( + string policyName, + FallbackScenarioTaskProvider provider, + FallbackPolicyOptions options); + + /// + /// Creates the hedging policy. + /// + /// The policy name. + /// The hedged task provider. + /// The options. + /// + /// A hedging policy. + /// + public IAsyncPolicy CreateHedgingPolicy( + string policyName, + HedgedTaskProvider provider, + HedgingPolicyOptions options); + + /// + /// Creates the hedging policy. + /// + /// The policy name. + /// The hedged task provider. + /// The options. + /// The type of the result returned by the action executed by the policy. + /// + /// A hedging policy. + /// + public IAsyncPolicy CreateHedgingPolicy( + string policyName, + HedgedTaskProvider provider, + HedgingPolicyOptions options); + + /// + /// Creates a timeout policy to ensure the caller never has to wait beyond the configured timeout. + /// . + /// + /// The policy name. + /// The options. + /// + /// Timeout policy. + /// + public IAsyncPolicy CreateTimeoutPolicy(string policyName, TimeoutPolicyOptions options); + + /// + /// Creates a bulkhead policy to limit the resources consumable by the governed actions, + /// such that a fault 'storm' cannot cause a cascading failure also bringing down other operations. + /// . + /// + /// The policy name. + /// The options. + /// + /// A bulkhead policy. + /// + public IAsyncPolicy CreateBulkheadPolicy(string policyName, BulkheadPolicyOptions options); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/IPolicyMetering.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/IPolicyMetering.cs new file mode 100644 index 0000000000..27d64e815b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/IPolicyMetering.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Polly; + +namespace Microsoft.Extensions.Resilience.Internal; + +/// +/// Metering support for generic and non-generic policies. +/// +internal interface IPolicyMetering +{ + /// + /// Initializes the instance. + /// + /// The pipeline id. + void Initialize(PipelineId pipelineId); + + /// + /// Records the policy event. + /// + /// The policy name. + /// The event name. + /// The fault instance. + /// The context associated with the event. + void RecordEvent(string policyName, string eventName, Exception? fault, Context? context); + + /// + /// Records the policy event. + /// + /// The type of result. + /// The policy name. + /// The event name. + /// The fault instance. + /// The context associated with the event. + void RecordEvent(string policyName, string eventName, DelegateResult? fault, Context? context); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Log.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Log.cs new file mode 100644 index 0000000000..59496c260d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Log.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.Extensions.Resilience.Internal; + +[SuppressMessage("Usage", "CA1801:Review unused parameters", Justification = "Generators.")] +[SuppressMessage("Major Code Smell", "S109:Magic numbers should not be used", Justification = "Generators.")] +[SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "Readability.")] +internal static partial class Log +{ + [LogMethod(0, LogLevel.Warning, + "Fallback policy: {policyName}. " + + "Request failed with the reason: {reason}. " + + "Performing fallback.")] + public static partial void LogFallback( + ILogger logger, + string policyName, + string reason); + + [LogMethod(1, LogLevel.Error, + "Circuit breaker policy: {policyName}. " + + "Circuit has been broken for {seconds} seconds. " + + "The reason: {reason}.")] + public static partial void LogCircuitBreak( + ILogger logger, + string policyName, + double seconds, + string reason); + + [LogMethod(2, LogLevel.Information, + "Circuit breaker policy: {policyName}. " + + "Reset has been triggered.")] + public static partial void LogCircuitReset( + ILogger logger, + string policyName); + + [LogMethod(3, LogLevel.Warning, + "Retry policy: {policyName}. " + + "Request failed with the reason: {reason}. " + + "Waiting {seconds} seconds before next retry. " + + "Retry attempt {attemptNumber}.")] + public static partial void LogRetry( + ILogger logger, + string policyName, + string reason, + double seconds, + int attemptNumber); + + [LogMethod(4, LogLevel.Warning, + "Bulkhead policy: {policyName}. " + + "Bulkhead policy has been triggered.")] + public static partial void LogBulkhead( + ILogger logger, + string policyName); + + [LogMethod(5, + LogLevel.Warning, + "Hedging policy: {policyName}. " + + "Request failed with the reason: {reason}. " + + "Performing hedging.")] + public static partial void LogHedging( + ILogger logger, + string policyName, + string reason); + + [LogMethod(6, + LogLevel.Warning, + "Timeout policy: {policyName}. " + + "Timeout interval has been reached.")] + public static partial void LogTimeout( + ILogger logger, + string policyName); + + [LogMethod(7, LogLevel.Information, "Circuit breaker policy: {policyName}. Half-Open has been triggered.")] + public static partial void LogCircuitHalfOpen(ILogger logger, string policyName); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Metric.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Metric.cs new file mode 100644 index 0000000000..582cbbdda2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Metric.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +namespace Microsoft.Extensions.Resilience; + +internal static partial class PollyMetric +{ + [Counter( + ResilienceDimensions.PipelineName, + ResilienceDimensions.PipelineKey, + ResilienceDimensions.ResultType, + ResilienceDimensions.PolicyName, + ResilienceDimensions.EventName, + ResilienceDimensions.FailureSource, + ResilienceDimensions.FailureReason, + ResilienceDimensions.FailureSummary, + ResilienceDimensions.DependencyName, + ResilienceDimensions.RequestName, + Name = @"R9\Resilience\Policies")] + public static partial PoliciesMetricCounter CreatePoliciesMetricCounter(Meter meter); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PipelineId.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PipelineId.cs new file mode 100644 index 0000000000..94afb13edd --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PipelineId.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience.Internal; + +/// +/// Composite key for the pipeline. +/// +[Experimental] +internal sealed record PipelineId(string PipelineName, string PipelineKey, string? ResultType, string PolicyPipelineKey) +{ + /// + /// Creates a pipeline id. + /// + /// The type of result the pipeline handles. + /// The pipeline name. + /// The pipeline key. + /// The pipeline id instance. + [Experimental] + public static PipelineId Create(string pipelineName, string pipelineKey) + { + var policyPipelineKey = string.IsNullOrEmpty(pipelineKey) ? $"{typeof(T).Name}-{pipelineName}" : $"{typeof(T).Name}-{pipelineName}-{pipelineKey}"; + + return new PipelineId(Throw.IfNullOrEmpty(pipelineName), pipelineKey, typeof(T).Name, policyPipelineKey); + } + + /// + /// Creates a pipeline id. + /// + /// The pipeline name. + /// The pipeline key. + /// The pipeline id instance. + [Experimental] + public static PipelineId Create(string pipelineName, string pipelineKey) + { + var policyPipelineKey = string.IsNullOrEmpty(pipelineKey) ? pipelineName : $"{pipelineName}-{pipelineKey}"; + + return new PipelineId(Throw.IfNullOrEmpty(pipelineName), pipelineKey, null, policyPipelineKey); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyEvents.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyEvents.cs new file mode 100644 index 0000000000..9e3f36c925 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyEvents.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Resilience.Internal; +internal static class PolicyEvents +{ + public const string FallbackPolicyEvent = "FallbackPolicy-OnFallback"; + public const string CircuitBreakerOnBreakPolicyEvent = "CircuitBreakerPolicy-OnBreak"; + public const string CircuitBreakerOnResetPolicyEvent = "CircuitBreakerPolicy-OnReset"; + public const string CircuitBreakerOnHalfOpenPolicyEvent = "CircuitBreakerPolicy-OnHalfOpen"; + public const string RetryPolicyEvent = "RetryPolicy-OnRetry"; + public const string TimeoutPolicyEvent = "TimeoutPolicy-OnTimeout"; + public const string BulkheadPolicyEvent = "BulkheadPolicy-OnBulkheadRejected"; + public const string HedgingPolicyEvent = "HedgingPolicy-OnHedgingAsync"; +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyFactory.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyFactory.cs new file mode 100644 index 0000000000..340be8448e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyFactory.cs @@ -0,0 +1,446 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Resilience.Hedging; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Shared.Diagnostics; +using Polly; +using Polly.Retry; + +namespace Microsoft.Extensions.Resilience.Internal; + +#pragma warning disable CS0618 // access obsoleted members +#pragma warning disable R9A061 + +/// +/// Factory class for policy creation. +/// +internal sealed class PolicyFactory : IPolicyFactory +{ + private static readonly CircuitBreakerPolicyOptionsValidator _circuitBreakerOptionsValidator = new(); + private static readonly RetryPolicyOptionsValidator _retryOptionsValidator = new(); + private static readonly RetryPolicyOptionsCustomValidator _retryOptionsCustomValidator = new(); + private static readonly HedgingPolicyOptionsValidator _hedgingOptionsValidator = new(); + private static readonly TimeoutPolicyOptionsValidator _timeoutOptionsValidator = new(); + private static readonly BulkheadPolicyOptionsValidator _bulkheadOptionsValidator = new(); + + private readonly ILogger _logger; + private readonly IPolicyMetering _metering; + private readonly Action _recordMetric; + + public PolicyFactory(ILogger logger, IPolicyMetering metering) + { + _logger = logger; + _metering = metering; + _recordMetric = RecordMetric; + } + + public void Initialize(PipelineId pipelineId) => _metering.Initialize(pipelineId); + + /// + /// Reacts on proportion of failures (i.e. failureThreshold) by measuring the data within a custom interval (i.e. sampling duration) + /// Imposes a minimal time interval before acting (i.e. minimumThroughput) and configurable break duration. + /// + /// + public IAsyncPolicy CreateCircuitBreakerPolicy( + string policyName, + CircuitBreakerPolicyOptions options) + { + _ = Throw.IfNull(options); + + PolicyFactoryUtility.ValidateOptions(_circuitBreakerOptionsValidator, options); + + return GetPolicyBuilderWithErrorHandling(options.ShouldHandleException) + .AdvancedCircuitBreakerAsync( + options.FailureThreshold, + options.SamplingDuration, + options.MinimumThroughput, + options.BreakDuration, + (triggerEvent, duration, context) => + { + var reason = FailureReasonResolver.GetFailureFromException(triggerEvent); + Log.LogCircuitBreak(_logger, policyName, duration.TotalSeconds, reason); + RecordMetric(PolicyEvents.CircuitBreakerOnBreakPolicyEvent, policyName, triggerEvent, context); + + options.OnCircuitBreak(new BreakActionArguments(triggerEvent, context, duration, CancellationToken.None)); + }, + PolicyFactoryUtility.OnCircuitReset(policyName, options, PolicyEvents.CircuitBreakerOnResetPolicyEvent, _logger, _recordMetric), + () => + { + Log.LogCircuitHalfOpen(_logger, policyName); + _metering.RecordEvent(policyName, PolicyEvents.CircuitBreakerOnHalfOpenPolicyEvent, fault: null, context: null); + }); + } + + /// + /// + /// Reacts on proportion of failures (i.e. failureThreshold) by measuring the data within a custom interval (i.e. sampling duration) + /// Imposes a minimal time interval before acting (i.e. minimumThroughput) and configurable break duration. + /// + /// + public IAsyncPolicy CreateCircuitBreakerPolicy( + string policyName, + CircuitBreakerPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(options); + + ValidateOptions(_circuitBreakerOptionsValidator, options); + + var onCircuitReset = PolicyFactoryUtility.OnCircuitReset( + policyName, + options, + PolicyEvents.CircuitBreakerOnResetPolicyEvent, + _logger, + _recordMetric); + + return GetPolicyBuilderWithErrorHandling(options.ShouldHandleResultAsError, options.ShouldHandleException) + .AdvancedCircuitBreakerAsync( + options.FailureThreshold, + options.SamplingDuration, + options.MinimumThroughput, + options.BreakDuration, + (triggerEvent, duration, context) => + { + var reason = FailureReasonResolver.GetFailureReason(triggerEvent); + Log.LogCircuitBreak(_logger, policyName, duration.TotalSeconds, reason); + RecordMetric(PolicyEvents.CircuitBreakerOnBreakPolicyEvent, policyName, triggerEvent, context); + options.OnCircuitBreak(new BreakActionArguments(triggerEvent, context, duration, CancellationToken.None)); + }, + (context) => onCircuitReset(context), + () => + { + Log.LogCircuitHalfOpen(_logger, policyName); + _metering.RecordEvent(policyName, PolicyEvents.CircuitBreakerOnHalfOpenPolicyEvent, fault: null, context: null); + }); + } + + /// + /// + /// Timing-wise, the onFallbackAsync runs the statement before the fallbackAction. + /// The onFallbackAsync might use the initial result (i.e. before the fallbackAction is performed). + /// Therefore the result should be disposed after the onFallbackAsync, before the fallbackAction. + /// . + /// . + /// + public IAsyncPolicy CreateFallbackPolicy( + string policyName, + FallbackScenarioTaskProvider fallbackScenarioTaskProvider, + FallbackPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(fallbackScenarioTaskProvider); + _ = Throw.IfNull(options); + + return + GetPolicyBuilderWithErrorHandling(options.ShouldHandleException) + .FallbackAsync( + (_, context, ct) => + { + return fallbackScenarioTaskProvider(new FallbackScenarioTaskArguments(context, ct)); + }, + (fault, context) => + { + var reason = FailureReasonResolver.GetFailureFromException(fault); + Log.LogFallback(_logger, policyName, reason); + RecordMetric(PolicyEvents.FallbackPolicyEvent, policyName, fault, context); + return options.OnFallbackAsync(new FallbackTaskArguments(fault, context, CancellationToken.None)); + }); + } + + public IAsyncPolicy CreateFallbackPolicy( + string policyName, + FallbackScenarioTaskProvider provider, + FallbackPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(provider); + _ = Throw.IfNull(options); + + return + GetPolicyBuilderWithErrorHandling( + options.ShouldHandleResultAsError, + options.ShouldHandleException) + .FallbackAsync( + (initialResult, context, ct) => + { + DisposeResult(initialResult); + return provider(new FallbackScenarioTaskArguments(context, ct)); + }, + (result, context) => + { + var reason = FailureReasonResolver.GetFailureReason(result); + Log.LogFallback(_logger, policyName, reason); + RecordMetric(PolicyEvents.FallbackPolicyEvent, policyName, result, context); + return options.OnFallbackAsync(new FallbackTaskArguments(result, context, CancellationToken.None)); + }); + } + + public IAsyncPolicy CreateRetryPolicy(string policyName, RetryPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(options); + + PolicyFactoryUtility.ValidateOptions(_retryOptionsValidator, options); + + return CreateRetryPolicyWithDefaultDelay(policyName, options); + } + + public IAsyncPolicy CreateRetryPolicy(string policyName, RetryPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(options); + + ValidateOptions(_retryOptionsValidator, options); + ValidateOptions(_retryOptionsCustomValidator, options); + + if (options.RetryDelayGenerator != null) + { + return CreateRetryPolicyWithCustomDelay(policyName, options); + } + + return CreateRetryPolicyWithDefaultDelay(policyName, options); + } + + public IAsyncPolicy CreateHedgingPolicy( + string policyName, + HedgedTaskProvider provider, + HedgingPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(provider); + _ = Throw.IfNull(options); + + PolicyFactoryUtility.ValidateOptions(_hedgingOptionsValidator, options); + + var hedgingDelay = options.HedgingDelay; + + return + GetPolicyBuilderWithErrorHandling(options.ShouldHandleException) + .AsyncHedgingPolicy( + provider, + options.MaxHedgedAttempts, + + // Stryker disable once all: https://domoreexp.visualstudio.com/R9/_workitems/edit/2804465 + options.HedgingDelayGenerator ?? (_ => hedgingDelay), + (exception, context, attempt, cancellationToken) => + { + var reason = FailureReasonResolver.GetFailureFromException(exception); + Log.LogHedging(_logger, policyName, reason); + RecordMetric(PolicyEvents.HedgingPolicyEvent, policyName, exception, context); + return options.OnHedgingAsync(new HedgingTaskArguments(exception, context, attempt, cancellationToken)); + }); + } + + public IAsyncPolicy CreateHedgingPolicy( + string policyName, + HedgedTaskProvider provider, + HedgingPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(provider); + _ = Throw.IfNull(options); + + ValidateOptions(_hedgingOptionsValidator, options); + + var hedgingDelay = options.HedgingDelay; + + return + GetPolicyBuilderWithErrorHandling( + options.ShouldHandleResultAsError, + options.ShouldHandleException) + .AsyncHedgingPolicy( + provider, + options.MaxHedgedAttempts, + + // Stryker disable once all: https://domoreexp.visualstudio.com/R9/_workitems/edit/2804465 + options.HedgingDelayGenerator ?? (_ => hedgingDelay), + (result, context, attempt, cancellationToken) => + { + var reason = FailureReasonResolver.GetFailureReason(result); + Log.LogHedging(_logger, policyName, reason); + RecordMetric(PolicyEvents.HedgingPolicyEvent, policyName, result, context); + return options.OnHedgingAsync(new HedgingTaskArguments(result, context, attempt, cancellationToken)); + }); + } + + public IAsyncPolicy CreateTimeoutPolicy(string policyName, TimeoutPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(options); + + PolicyFactoryUtility.ValidateOptions(_timeoutOptionsValidator, options); + + return Policy.TimeoutAsync(options.TimeoutInterval, options.TimeoutStrategy, (context, _, _) => + { + return PolicyFactoryUtility.OnTimeoutAsync(context, policyName, options, _logger, _recordMetric); + }); + } + + public IAsyncPolicy CreateBulkheadPolicy(string policyName, BulkheadPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(options); + + PolicyFactoryUtility.ValidateOptions(_bulkheadOptionsValidator, options); + + return Policy.BulkheadAsync( + options.MaxConcurrency, + options.MaxQueuedActions, + context => + { + return PolicyFactoryUtility.OnBulkheadRejectedAsync(context, policyName, options, _logger, _recordMetric); + }); + } + + private static void DisposeResult(DelegateResult delegateResult) + { + if (delegateResult.Result is IDisposable disposableResult) + { + disposableResult.Dispose(); + } + } + + /// + /// Validates the options. + /// + /// When are not valid. + private static void ValidateOptions(IValidateOptions validator, TOptions options) + where TOptions : class, new() + { + validator.Validate(null, options).ThrowIfFailed(); + } + + private static PolicyBuilder GetPolicyBuilderWithErrorHandling(Predicate shouldHandleExp) + { + return Policy.Handle(ex => shouldHandleExp(ex)); + } + + /// + /// Creates the policy builder with error handling. + /// + /// Predicate to define what type of result shall be treated and handled as an error. + /// Defines what exceptions should be handled. + /// + /// A policy builder on which policies can be chained. + /// + private static PolicyBuilder GetPolicyBuilderWithErrorHandling(Predicate shouldHandleResult, Predicate shouldHandleExp) + { + return Policy.HandleResult(r => shouldHandleResult(r)).Or(ex => shouldHandleExp(ex)); + } + + private AsyncRetryPolicy CreateRetryPolicyWithCustomDelay(string policyName, RetryPolicyOptions options) + { + var policyBase = GetPolicyBuilderWithErrorHandling(options.ShouldHandleResultAsError, options.ShouldHandleException); + + TimeSpan sleepDurationProvider(int attemptCount, DelegateResult response, Context context) + { + var customDelay = options.RetryDelayGenerator!( + new RetryDelayArguments(response, context, CancellationToken.None)); + + // If the generator returns an invalid delay, use the default one based on the backoff + return customDelay > TimeSpan.Zero ? customDelay : options.BaseDelay; + } + + Task onRetryAsync(DelegateResult result, TimeSpan timeSpan, int attemptNumber, Context context) + => HandleRetryEventAsync(policyName, options, result, context, timeSpan, attemptNumber); + + Task onInfiniteRetryAsync(DelegateResult result, int attemptNumber, TimeSpan timeSpan, Context context) => onRetryAsync(result, timeSpan, attemptNumber, context); + +#pragma warning disable R9A034 // Optimize method group use to avoid allocations + if (options.RetryCount == RetryPolicyOptions.InfiniteRetry) + { + return policyBase.WaitAndRetryForeverAsync(sleepDurationProvider, onInfiniteRetryAsync); + } + else + { + return policyBase.WaitAndRetryAsync(options.RetryCount, sleepDurationProvider, onRetryAsync); + } +#pragma warning restore R9A034 // Optimize method group use to avoid allocations + } + + private AsyncRetryPolicy CreateRetryPolicyWithDefaultDelay(string policyName, RetryPolicyOptions options) + { + var delay = options.GetDelays(); + + return GetPolicyBuilderWithErrorHandling(options.ShouldHandleException) + .WaitAndRetryAsync(delay, (exception, timeSpan, attemptNumber, context) => + HandleRetryEventAsync(policyName, options, exception, context, timeSpan, attemptNumber)); + } + + private AsyncRetryPolicy CreateRetryPolicyWithDefaultDelay(string policyName, RetryPolicyOptions options) + { + var policyBase = GetPolicyBuilderWithErrorHandling(options.ShouldHandleResultAsError, options.ShouldHandleException); + + if (options.RetryCount == RetryPolicyOptions.InfiniteRetry) + { + return policyBase + .WaitAndRetryForeverAsync((_, _, _) => options.BaseDelay, + (result, attemptNumber, timeSpan, context) => + HandleRetryEventAsync(policyName, options, result, context, timeSpan, attemptNumber)); + } + + var delays = options.GetDelays(); + + return policyBase + .WaitAndRetryAsync(delays, (result, timeSpan, attemptNumber, context) => + HandleRetryEventAsync(policyName, options, result, context, timeSpan, attemptNumber)); + } + + private Task HandleRetryEventAsync( + string policyName, + RetryPolicyOptions options, + Exception exception, + Context context, + TimeSpan timeSpan, + int attemptNumber) + { + var reason = FailureReasonResolver.GetFailureFromException(exception); + Log.LogRetry(_logger, policyName, reason, timeSpan.TotalSeconds, attemptNumber); + RecordMetric(PolicyEvents.RetryPolicyEvent, policyName, exception, context); + + var arguments = new RetryActionArguments(exception, context, timeSpan, attemptNumber, CancellationToken.None); + return options.OnRetryAsync(arguments); + } + + private async Task HandleRetryEventAsync( + string policyName, + RetryPolicyOptions options, + DelegateResult result, + Context context, + TimeSpan timeSpan, + int attemptNumber) + { + var reason = FailureReasonResolver.GetFailureReason(result); + Log.LogRetry(_logger, policyName, reason, timeSpan.TotalSeconds, attemptNumber); + RecordMetric(PolicyEvents.RetryPolicyEvent, policyName, result, context); + + var arguments = new RetryActionArguments(result, context, timeSpan, attemptNumber, CancellationToken.None); + + await options.OnRetryAsync(arguments).ConfigureAwait(false); + + DisposeResult(result); + } + + private void RecordMetric(string eventType, string policyName, Exception fault, Context context) + { + _metering.RecordEvent(policyName, eventType, fault, context); + } + + private void RecordMetric(string eventType, string policyName, Context context) + { + _metering.RecordEvent(policyName, eventType, null, context); + } + + private void RecordMetric(string eventType, string policyName, DelegateResult result, Context context) + { + _metering.RecordEvent(policyName, eventType, result, context); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyFactoryServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyFactoryServiceCollectionExtensions.cs new file mode 100644 index 0000000000..493419b31e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyFactoryServiceCollectionExtensions.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Diagnostics.ExceptionSummarization; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience.Internal; + +/// +/// Extension class for the Service Collection DI container. +/// +internal static class PolicyFactoryServiceCollectionExtensions +{ + /// + /// Registers to the DI container a singleton policy pipeline factory . + /// + /// The type of the result returned by the action executed by the policies. + /// The DI container. + /// The input . + /// cannot be null. + public static IServiceCollection AddPolicyFactory(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + _ = services.AddOptions>(); + _ = services.AddExceptionSummarizer(); + _ = services.RegisterMetering(); + + services.TryAddTransient(); + services.TryAddTransient(); + + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyFactoryUtility.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyFactoryUtility.cs new file mode 100644 index 0000000000..909f56d4ae --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyFactoryUtility.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Resilience.Options; +using Polly; + +#pragma warning disable R9A061 + +namespace Microsoft.Extensions.Resilience.Internal; + +internal sealed class PolicyFactoryUtility +{ + /// + /// Validates the options. + /// + /// When are not valid. + public static void ValidateOptions(IValidateOptions validator, TOptions options) + where TOptions : class, new() + { + validator.Validate(null, options).ThrowIfFailed(); + } + + public static Action OnCircuitReset(string policyName, CircuitBreakerPolicyOptions options, + string eventType, ILogger logger, Action recordMetric) + { + return (context) => + { + Log.LogCircuitReset(logger, policyName); + recordMetric(eventType, policyName, context); + + options.OnCircuitReset(new ResetActionArguments(context, CancellationToken.None)); + }; + } + + public static Task OnTimeoutAsync(Context context, string policyName, TimeoutPolicyOptions options, + ILogger logger, Action recordMetric) + { + Log.LogTimeout(logger, policyName); + recordMetric(PolicyEvents.TimeoutPolicyEvent, policyName, context); + + return options.OnTimedOutAsync(new TimeoutTaskArguments(context, CancellationToken.None)); + } + + public static Task OnBulkheadRejectedAsync(Context context, string policyName, BulkheadPolicyOptions options, ILogger logger, Action recordMetric) + { + Log.LogBulkhead(logger, policyName); + recordMetric(PolicyEvents.BulkheadPolicyEvent, policyName, context); + return options.OnBulkheadRejectedAsync(new BulkheadTaskArguments(context, CancellationToken.None)); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyMetering.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyMetering.cs new file mode 100644 index 0000000000..58f15e6eaf --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/PolicyMetering.cs @@ -0,0 +1,157 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.ExceptionSummarization; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Metering; +using Polly; + +namespace Microsoft.Extensions.Resilience.Internal; + +internal sealed class PolicyMetering : IPolicyMetering +{ + private static readonly RequestMetadata _fallbackMetadata = new(); + + private readonly ConcurrentDictionary _options = new(); + private readonly IExceptionSummarizer _exceptionSummarizer; + private readonly IOutgoingRequestContext? _outgoingRequestContext; + private readonly IServiceProvider _serviceProvider; + private readonly PoliciesMetricCounter _counter; + private PipelineId? _pipelineId; + + public PolicyMetering( + Meter meter, + IExceptionSummarizer exceptionSummarizer, + IServiceProvider serviceProvider) + { + _counter = Resilience.PollyMetric.CreatePoliciesMetricCounter(meter); + _exceptionSummarizer = exceptionSummarizer; + _outgoingRequestContext = serviceProvider.GetService(); + _serviceProvider = serviceProvider; + } + + private bool IsInitialized => _pipelineId != null; + + public void Initialize(PipelineId pipelineId) + { + if (IsInitialized) + { + throw new InvalidOperationException("This instance is already initialized."); + } + + _pipelineId = pipelineId; + } + + public void RecordEvent( + string policyName, + string eventName, + Exception? fault, + Context? context) + { + if (!IsInitialized) + { + return; + } + + string? failureSource = null; + string? failureReason = null; + string? failureSummary = null; + + if (fault != null) + { + failureSource = fault.Source; + failureReason = fault.GetType().Name; + failureSummary = _exceptionSummarizer.Summarize(fault).ToString(); + } + + var requestMetadata = GetRequestMetadata(context); + + _counter.Add( + 1, + _pipelineId!.PipelineName, + _pipelineId!.PipelineKey.GetDimensionOrUnknown(), + _pipelineId!.ResultType.GetDimensionOrUnknown(), + policyName, + eventName, + failureSource.GetDimensionOrUnknown(), + failureReason.GetDimensionOrUnknown(), + failureSummary.GetDimensionOrUnknown(), + requestMetadata.DependencyName, + requestMetadata.RequestName); + } + + public void RecordEvent( + string policyName, + string eventName, + DelegateResult? fault, + Context? context) + { + if (!IsInitialized) + { + return; + } + + string? failureSource = null; + string? failureReason = null; + string? failureSummary = null; + + if (fault != null) + { + if (fault.Exception != null) + { + RecordEvent(policyName, eventName, fault.Exception, context); + return; + } + else + { + if (!Equals(fault.Result, default(TResult))) + { + var failureContext = GetFailureContext(fault.Result); + + failureSource = failureContext.FailureSource; + failureReason = failureContext.FailureReason; + failureSummary = failureContext.AdditionalInformation; + } + } + } + + var requestMetadata = GetRequestMetadata(context); + + _counter.Add( + 1, + _pipelineId!.PipelineName, + _pipelineId!.PipelineKey.GetDimensionOrUnknown(), + _pipelineId!.ResultType.GetDimensionOrUnknown(), + policyName, + eventName, + failureSource.GetDimensionOrUnknown(), + failureReason.GetDimensionOrUnknown(), + failureSummary.GetDimensionOrUnknown(), + requestMetadata.DependencyName, + requestMetadata.RequestName); + } + + private FailureResultContext GetFailureContext(TResult result) + { + var options = (FailureEventMetricsOptions)_options.GetOrAdd( + typeof(TResult), + static (_, provider) => provider.GetRequiredService>>().Value, + _serviceProvider); + + return options.GetContextFromResult(result); + } + + private RequestMetadata GetRequestMetadata(Context? context) + { + if (context != null && context.TryGetValue(TelemetryConstants.RequestMetadataKey, out var val) && val is RequestMetadata requestMetadata) + { + return requestMetadata; + } + + return _outgoingRequestContext?.RequestMetadata ?? _fallbackMetadata; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/RetryPolicyOptionsExtensions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/RetryPolicyOptionsExtensions.cs new file mode 100644 index 0000000000..ffb3248cf9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/RetryPolicyOptionsExtensions.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Shared.Diagnostics; +using Polly.Contrib.WaitAndRetry; + +namespace Microsoft.Extensions.Resilience; + +/// +/// Extensions for . +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class RetryPolicyOptionsExtensions +{ + /// + /// Gets the delays generated based on the retry options configuration. + /// + /// The options instance. + /// The delays collection. + [EditorBrowsable(EditorBrowsableState.Never)] + public static IEnumerable GetDelays(this RetryPolicyOptions options) + { + _ = Throw.IfNull(options); + + var delays = GetDelayByBackoffType(options.BackoffType, options.BaseDelay, options.RetryCount); + + if (options.BackoffType == BackoffType.ExponentialWithJitter) + { + // We cannot materialize ExponentialWithJitter delays as every iteration returns slightly different delays. + // Materializing and caching it would remove the randomness factor in retry policy. + return delays; + } + + // here, the delays are the same so we can materialize the list + return delays.ToList(); + } + + internal static IEnumerable GetDelayByBackoffType(BackoffType retryType, TimeSpan backoffBasedDelay, int retryCount) + { + return retryType switch + { + BackoffType.ExponentialWithJitter => Backoff.DecorrelatedJitterBackoffV2(backoffBasedDelay, retryCount), + BackoffType.Linear => Backoff.LinearBackoff(backoffBasedDelay, retryCount), + BackoffType.Constant => Backoff.ConstantBackoff(backoffBasedDelay, retryCount), + _ => throw new InvalidOperationException($"{backoffBasedDelay} back-off type is not supported.") + }; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/TelemetryHelper.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/TelemetryHelper.cs new file mode 100644 index 0000000000..24c91b0e59 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/TelemetryHelper.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Http.Telemetry; + +namespace Microsoft.Extensions.Resilience.Internal; + +internal static class TelemetryHelper +{ + internal const string DefaultDimensionValue = "Undefined"; + + internal static string GetDimensionOrDefault(this string? dimension) + { + return string.IsNullOrEmpty(dimension) ? DefaultDimensionValue : dimension!; + } + + internal static string GetDimensionOrUnknown(this string? dimension) + { + return string.IsNullOrEmpty(dimension) ? TelemetryConstants.Unknown : dimension!; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/BulkheadPolicyOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/BulkheadPolicyOptionsValidator.cs new file mode 100644 index 0000000000..933add3b64 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/BulkheadPolicyOptionsValidator.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Resilience.Internal; + +[OptionsValidator] +internal sealed partial class BulkheadPolicyOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/CircuitBreakerPolicyOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/CircuitBreakerPolicyOptionsValidator.cs new file mode 100644 index 0000000000..b9e44fb2c2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/CircuitBreakerPolicyOptionsValidator.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Resilience.Internal; + +[OptionsValidator] +internal sealed partial class CircuitBreakerPolicyOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/CircuitBreakerPolicyOptionsValidatorT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/CircuitBreakerPolicyOptionsValidatorT.cs new file mode 100644 index 0000000000..1721378ee2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/CircuitBreakerPolicyOptionsValidatorT.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Resilience.Internal; + +[OptionsValidator] +internal sealed partial class CircuitBreakerPolicyOptionsValidator : IValidateOptions> +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/HedgingPolicyOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/HedgingPolicyOptionsValidator.cs new file mode 100644 index 0000000000..96912a27e8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/HedgingPolicyOptionsValidator.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Resilience.Internal; + +[OptionsValidator] +internal sealed partial class HedgingPolicyOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/HedgingPolicyOptionsValidatorT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/HedgingPolicyOptionsValidatorT.cs new file mode 100644 index 0000000000..7a01df2ddf --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/HedgingPolicyOptionsValidatorT.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Resilience.Internal; + +[OptionsValidator] +internal sealed partial class HedgingPolicyOptionsValidator : IValidateOptions> +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/RetryPolicyOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/RetryPolicyOptionsValidator.cs new file mode 100644 index 0000000000..79de3d93c1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/RetryPolicyOptionsValidator.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Resilience.Internal; + +[OptionsValidator] +internal sealed partial class RetryPolicyOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/RetryPolicyOptionsValidatorT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/RetryPolicyOptionsValidatorT.cs new file mode 100644 index 0000000000..4f17bf0bbb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/RetryPolicyOptionsValidatorT.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Resilience.Internal; + +[OptionsValidator] +internal sealed partial class RetryPolicyOptionsValidator : IValidateOptions> +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/TimeoutPolicyOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/TimeoutPolicyOptionsValidator.cs new file mode 100644 index 0000000000..a12aee93c2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/Generated/TimeoutPolicyOptionsValidator.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Resilience.Internal; + +[OptionsValidator] +internal sealed partial class TimeoutPolicyOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/RetryPolicyOptionsCustomValidator.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/RetryPolicyOptionsCustomValidator.cs new file mode 100644 index 0000000000..e2d7126a13 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Internals/Validators/RetryPolicyOptionsCustomValidator.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Resilience.Internal; + +internal sealed class RetryPolicyOptionsCustomValidator : IValidateOptions +{ + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed with [DynamicallyAddressedMembers]")] + public ValidateOptionsResult Validate(string? name, RetryPolicyOptions options) + { + var builder = new ValidateOptionsResultBuilder(); + var context = new ValidationContext(options); + + if (options.RetryCount == RetryPolicyOptions.InfiniteRetry && options.BackoffType != BackoffType.Constant) + { + builder.AddError($"must be {BackoffType.Constant} when infinite retries are enabled.", nameof(options.BackoffType)); + } + + if (options.RetryCount != RetryPolicyOptions.InfiniteRetry) + { + int position = -1; + foreach (var retryDelay in options.GetDelays()) + { + position++; + ValidateRetryDelay(builder, position, retryDelay); + } + } + + return builder.Build(); + } + + private static void ValidateRetryDelay(ValidateOptionsResultBuilder builder, int attempt, TimeSpan value) + { + var retryDelay = (long)value.TotalMilliseconds; + if (retryDelay > int.MaxValue) + { + builder.AddError( + $"unable to validate retry delay #{attempt} = {retryDelay}. Must be a positive TimeSpan and less than {int.MaxValue} milliseconds long.", + nameof(RetryPolicyOptions.RetryCount)); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BackoffType.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BackoffType.cs new file mode 100644 index 0000000000..936b205344 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BackoffType.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Retry type. +/// +public enum BackoffType +{ + /// + /// Exponential delay with randomization retry type, + /// making sure to mitigate any correlations. + /// + /// + /// 850ms, 1455ms, 3060ms. + /// + /// + /// In transient failures handling scenarios, this is the + /// recommended retry type. + /// + ExponentialWithJitter, + + /// + /// The constant retry type. + /// + /// + /// 200ms, 200ms, 200ms, etc. + /// + /// + /// It ensures a constant wait duration before each retry attempt. + /// For concurrent database access with possibility of conflicting updates, + /// retrying the failures in a constant manner allows consistent transient failures mitigation. + /// + Constant, + + /// + /// The linear retry type. + /// + /// + /// 100ms, 200ms, 300ms, 400ms, etc. + /// + /// + /// Generates sleep durations in an linear manner. + /// In case randomization introduced by the jitter and exponential growth are not intended, + /// the linear growth allows more control over the delay intervals. + /// + Linear +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BreakActionArguments.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BreakActionArguments.cs new file mode 100644 index 0000000000..4614606eab --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BreakActionArguments.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Structure with the arguments of the on break action. +/// +#pragma warning disable CA1815 // Override equals and operator equals on value types (Such usage is not expected in this scenario) +public readonly struct BreakActionArguments : IPolicyEventArguments +#pragma warning restore CA1815 +{ + /// + /// Initializes a new instance of the structure. + /// + /// The fault. + /// The policy context. + /// The duration of break. + /// Cancellation token. + public BreakActionArguments( + Exception exception, + Context context, + TimeSpan breakDuration, + CancellationToken cancellationToken) + { + _ = Throw.IfLessThanOrEqual(breakDuration.Ticks, 0); + BreakDuration = breakDuration; + Exception = Throw.IfNull(exception); + Context = Throw.IfNull(context); + CancellationToken = cancellationToken; + } + + /// + /// Gets the result of the action executed by the retry policy. + /// + public Exception Exception { get; } + + /// + /// Gets the duration of break. + /// + public TimeSpan BreakDuration { get; } + + /// + /// Gets the Polly associated with the policy execution. + /// + public Context Context { get; } + + /// + /// Gets the cancellation token associated with the policy execution. + /// + public CancellationToken CancellationToken { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BreakActionArgumentsT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BreakActionArgumentsT.cs new file mode 100644 index 0000000000..58162523e5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BreakActionArgumentsT.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Structure with the arguments of the on break action. +/// +/// The type of the result handled by the policy. +#pragma warning disable CA1815 // Override equals and operator equals on value types (Such usage is not expected in this scenario) +#pragma warning disable SA1649 // File name should match first type name +public readonly struct BreakActionArguments : IPolicyEventArguments +#pragma warning restore SA1649 // File name should match first type name +#pragma warning restore CA1815 +{ + /// + /// Initializes a new instance of the structure. + /// + /// The result. + /// The policy context. + /// The duration of break. + /// Cancellation token. + public BreakActionArguments( + DelegateResult result, + Context context, + TimeSpan breakDuration, + CancellationToken cancellationToken) + { + _ = Throw.IfLessThanOrEqual(breakDuration.Ticks, 0); + BreakDuration = breakDuration; + Result = Throw.IfNull(result); + Context = Throw.IfNull(context); + CancellationToken = cancellationToken; + } + + /// + /// Gets the result of the action executed by the retry policy. + /// + public DelegateResult Result { get; } + + /// + /// Gets the duration of break. + /// + public TimeSpan BreakDuration { get; } + + /// + /// Gets the Polly associated with the policy execution. + /// + public Context Context { get; } + + /// + /// Gets the cancellation token associated with the policy execution. + /// + public CancellationToken CancellationToken { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BulkheadPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BulkheadPolicyOptions.cs new file mode 100644 index 0000000000..7ff7a426a4 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BulkheadPolicyOptions.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Options for the bulkhead policy. +/// +public class BulkheadPolicyOptions +{ + private const int DefaultMaxLimitActions = 10000; + private const int DefaultMaxQueuedActions = 0; + private const int DefaultMaxConcurrency = 1000; + + /// + /// Gets or sets the maximum parallelization of executions through the bulkhead. + /// + /// + /// Default set to 1000. + /// + [Range(1, DefaultMaxLimitActions)] + public int MaxConcurrency { get; set; } = DefaultMaxConcurrency; + + /// + /// Gets or sets the maximum number of actions that may be queued (waiting to acquire an execution slot) at any one time. + /// + /// + /// Default set to 0. + /// + [Range(0, DefaultMaxLimitActions)] + public int MaxQueuedActions { get; set; } = DefaultMaxQueuedActions; + + private Func _onBulkheadRejectedAsync = _ => Task.CompletedTask; + + /// + /// Gets or sets the action performed during the bulkhead rejection of the bulkhead policy. + /// + [Required] + public Func OnBulkheadRejectedAsync + { + get => _onBulkheadRejectedAsync; + set => _onBulkheadRejectedAsync = Throw.IfNull(value); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BulkheadTaskArguments.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BulkheadTaskArguments.cs new file mode 100644 index 0000000000..53a99fce9f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/BulkheadTaskArguments.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Structure with the arguments of the on bulkhead task. +/// +#pragma warning disable CA1815 // Override equals and operator equals on value types (Such usage is not expected in this scenario) +public readonly struct BulkheadTaskArguments : IPolicyEventArguments +#pragma warning restore CA1815 +{ + /// + /// Initializes a new instance of the structure. + /// + /// The policy context. + /// The cancellation token. + public BulkheadTaskArguments( + Context context, + CancellationToken cancellationToken) + { + Context = Throw.IfNull(context); + CancellationToken = cancellationToken; + } + + /// + /// Gets the Polly associated with the policy execution. + /// + public Context Context { get; } + + /// + /// Gets the cancellation token associated with the policy execution. + /// + public CancellationToken CancellationToken { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/CircuitBreakerPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/CircuitBreakerPolicyOptions.cs new file mode 100644 index 0000000000..c10430c884 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/CircuitBreakerPolicyOptions.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.Shared.Data.Validation; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Circuit breaker policy options. +/// +public class CircuitBreakerPolicyOptions +{ + private const double DefaultFailureThreshold = 0.1; + private const int DefaultMinimumThroughput = 100; + private const int MinPolicyWaitingMilliseconds = 500; + private static readonly TimeSpan _defaultBreakDuration = TimeSpan.FromSeconds(5); + private static readonly TimeSpan _defaultSamplingDuration = TimeSpan.FromSeconds(30); + private Action _onCircuitBreak = _ => { }; + private Predicate _shouldHandleException = _ => true; + private Action _onCircuitReset = (_) => { }; + + /// + /// Gets or sets the failure threshold. + /// + /// + /// If the ratio (between the number of failed request and total request) exceeds this threshold, the circuit will break. + /// A ratio number higher than 0, up to 1. + /// Default set to 0.1. + /// + [ExclusiveRange(0, 1.0)] + public double FailureThreshold { get; set; } = DefaultFailureThreshold; + + /// + /// Gets or sets the minimum throughput. + /// + /// + /// This defines how many actions must pass through the circuit in the time-slice, + /// for statistics to be considered significant and the circuit-breaker to come into action. + /// Value must be greater than one. + /// Default set to 100. + /// + [ExclusiveRange(1, int.MaxValue)] + public int MinimumThroughput { get; set; } = DefaultMinimumThroughput; + + /// + /// Gets or sets the duration of break. + /// + /// + /// The duration the circuit will stay open before resetting. + /// Value must be greater than 0.5 seconds. + /// Default set to 5 seconds. + /// + [TimeSpan(MinPolicyWaitingMilliseconds, Exclusive = true)] + public TimeSpan BreakDuration { get; set; } = _defaultBreakDuration; + + /// + /// Gets or sets the duration of the sampling. + /// + /// + /// The duration of the time-slice over which failure ratios are assessed. + /// Value must be greater than 0.5 seconds. + /// Default set to 30 seconds. + /// + [TimeSpan(MinPolicyWaitingMilliseconds, Exclusive = true)] + public TimeSpan SamplingDuration { get; set; } = _defaultSamplingDuration; + + /// + /// Gets or sets the predicate which filters the type of exception the policy can handle. + /// + /// + /// By default any exception will be retried. + /// + public Predicate ShouldHandleException + { + get => _shouldHandleException; + set => _shouldHandleException = Throw.IfNull(value); + } + + /// + /// Gets or sets the action performed when the circuit breaker resets itself. + /// + [Required] + public Action OnCircuitReset + { + get => _onCircuitReset; + set => _onCircuitReset = Throw.IfNull(value); + } + + /// + /// Gets or sets the action performed when the circuit breaker breaks. + /// + [Required] + public Action OnCircuitBreak + { + get => _onCircuitBreak; + set => _onCircuitBreak = Throw.IfNull(value); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/CircuitBreakerPolicyOptionsT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/CircuitBreakerPolicyOptionsT.cs new file mode 100644 index 0000000000..21270428e6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/CircuitBreakerPolicyOptionsT.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Circuit breaker policy options. +/// +/// The type of the result handled by the policy. +#pragma warning disable SA1649 // File name should match first type name +public class CircuitBreakerPolicyOptions : CircuitBreakerPolicyOptions +#pragma warning restore SA1649 // File name should match first type name +{ + private Predicate _shouldHandleResultAsError = _ => false; + private Action> _onCircuitBreak = _ => { }; + + /// + /// Gets or sets the predicate which defines the results which are treated as transient errors. + /// + /// + /// By default, it will not retry any final result. + /// + public Predicate ShouldHandleResultAsError + { + get => _shouldHandleResultAsError; + set => _shouldHandleResultAsError = Throw.IfNull(value); + } + + /// + /// Gets or sets the action performed when the circuit breaker breaks. + /// + [Required] + public new Action> OnCircuitBreak + { + get => _onCircuitBreak; + set => _onCircuitBreak = Throw.IfNull(value); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/FallbackPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/FallbackPolicyOptions.cs new file mode 100644 index 0000000000..65c651dcf5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/FallbackPolicyOptions.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Options for the fallback policy. +/// +public class FallbackPolicyOptions +{ + private Func _onFallbackAsync = _ => Task.CompletedTask; + + private Predicate _shouldHandleException = _ => true; + + /// + /// Gets or sets the exception predicate to filter the type of exception the policy can handle. + /// + /// + /// By default any exception will be retried. + /// + [Required] + public Predicate ShouldHandleException + { + get => _shouldHandleException; + set => _shouldHandleException = Throw.IfNull(value); + } + + /// + /// Gets or sets the action to call asynchronously before invoking the task performed in the fallback scenario, + /// after the initially executed action encounters a transient failure. + /// + [Required] + public Func OnFallbackAsync + { + get => _onFallbackAsync; + set => _onFallbackAsync = Throw.IfNull(value); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/FallbackPolicyOptionsT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/FallbackPolicyOptionsT.cs new file mode 100644 index 0000000000..7be6a7f5e7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/FallbackPolicyOptionsT.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience.Options; + +#pragma warning disable SA1649 // File name should match first type name + +/// +/// Options for the fallback policy. +/// +/// The type of the result handled by the policy. +public class FallbackPolicyOptions : FallbackPolicyOptions +{ + private Predicate _shouldHandleResultAsError = _ => false; + private Func, Task> _onFallbackAsync = _ => Task.CompletedTask; + + /// + /// Gets or sets the predicate to filter results the policy will handle. + /// + /// + /// By default, it will not retry any final result. + /// + [Required] + public Predicate ShouldHandleResultAsError + { + get => _shouldHandleResultAsError; + set => _shouldHandleResultAsError = Throw.IfNull(value); + } + + /// + /// Gets or sets the action to call asynchronously before invoking the task performed in the fallback scenario, + /// after the initially executed action encounters a transient failure. + /// + [Required] + public new Func, Task> OnFallbackAsync + { + get => _onFallbackAsync; + set => _onFallbackAsync = Throw.IfNull(value); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/FallbackTaskArguments.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/FallbackTaskArguments.cs new file mode 100644 index 0000000000..ec65b61c52 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/FallbackTaskArguments.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Structure with the arguments of the on bulkhead task. +/// +#pragma warning disable CA1815 // Override equals and operator equals on value types (Such usage is not expected in this scenario) +public readonly struct FallbackTaskArguments : IPolicyEventArguments +#pragma warning restore CA1815 +{ + /// + /// Initializes a new instance of the structure. + /// + /// The exception. + /// The policy context. + /// The cancellation token. + public FallbackTaskArguments( + Exception exception, + Context context, + CancellationToken cancellationToken) + { + Context = Throw.IfNull(context); + Exception = Throw.IfNull(exception); + CancellationToken = cancellationToken; + } + + /// + /// Gets the result of the action executed by the retry policy. + /// + public Exception Exception { get; } + + /// + /// Gets the Polly associated with the policy execution. + /// + public Context Context { get; } + + /// + /// Gets the cancellation token associated with the policy execution. + /// + public CancellationToken CancellationToken { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/FallbackTaskArgumentsT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/FallbackTaskArgumentsT.cs new file mode 100644 index 0000000000..51dfd22e4e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/FallbackTaskArgumentsT.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience.Options; + +#pragma warning disable SA1649 // File name should match first type name + +/// +/// Structure with the arguments of the on bulkhead task. +/// +/// The type of the result handled by the policy. +#pragma warning disable CA1815 // Override equals and operator equals on value types (Such usage is not expected in this scenario) +public readonly struct FallbackTaskArguments : IPolicyEventArguments +#pragma warning restore CA1815 +{ + /// + /// Initializes a new instance of the structure. + /// + /// The result. + /// The policy context. + /// The cancellation token. + public FallbackTaskArguments( + DelegateResult result, + Context context, + CancellationToken cancellationToken) + { + Context = Throw.IfNull(context); + Result = Throw.IfNull(result); + CancellationToken = cancellationToken; + } + + /// + /// Gets the result of the action executed by the retry policy. + /// + public DelegateResult Result { get; } + + /// + /// Gets the Polly associated with the policy execution. + /// + public Context Context { get; } + + /// + /// Gets the cancellation token associated with the policy execution. + /// + public CancellationToken CancellationToken { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingDelayArguments.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingDelayArguments.cs new file mode 100644 index 0000000000..d8c0bac7d3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingDelayArguments.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Structure with the arguments used by . +/// +#pragma warning disable CA1815 // Override equals and operator equals on value types (Such usage is not expected in this scenario) +public readonly struct HedgingDelayArguments : IPolicyEventArguments +#pragma warning restore CA1815 +{ + /// + /// Initializes a new instance of the structure. + /// + /// The policy context. + /// The attempt number. + /// Cancellation token. + public HedgingDelayArguments(Context context, int attemptNumber, CancellationToken cancellationToken) + { + Context = Throw.IfNull(context); + AttemptNumber = Throw.IfLessThan(attemptNumber, 0); + CancellationToken = cancellationToken; + } + + /// + /// Gets the zero-based hedging attempt number. + /// + public int AttemptNumber { get; } + + /// + /// Gets the Polly associated with the policy execution. + /// + public Context Context { get; } + + /// + /// Gets the cancellation token associated with the policy execution. + /// + public CancellationToken CancellationToken { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingPolicyOptions.cs new file mode 100644 index 0000000000..165271d7ce --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingPolicyOptions.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.Shared.Data.Validation; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Hedging policy options. +/// +public class HedgingPolicyOptions +{ + /// + /// A that represents the infinite hedging delay. + /// + public static readonly TimeSpan InfiniteHedgingDelay = TimeSpan.FromMilliseconds(-1); + + private const int DefaultMaxHedgedAttempts = 2; + private const int MinimumHedgedAttempts = 2; + private const int MaximumHedgedAttempts = 10; + private static readonly TimeSpan _defaultHedgingDelay = TimeSpan.FromSeconds(2); + + /// + /// Gets or sets the minimal time of waiting before spawning a new hedged call. + /// + /// + /// Default is set to 2 seconds. + /// + /// You can also use to create all hedged tasks (value of ) at once + /// or to force the hedging policy to never create new task before the old one is finished. + /// + /// If you want a greater control over hedging delay customization use . + /// + [TimeSpan(-1, Exclusive = false)] + public TimeSpan HedgingDelay { get; set; } = _defaultHedgingDelay; + + /// + /// Gets or sets the delegate that is used to customize the hedging delays after each hedging task is created. + /// + /// + /// The takes precedence over . If specified, the is ignored. + /// + /// By default, this value is null. + /// + public Func? HedgingDelayGenerator { get; set; } + + /// + /// Gets or sets the maximum hedged attempts to perform the desired task. + /// + /// + /// Default set to 2. + /// The value defines how many concurrent hedged tasks will be triggered by the policy. + /// This includes the primary hedged task that is initially performed, and the further tasks that will + /// be fetched from the provider and spawned in parallel. + /// The value must be bigger or equal to 2, and lower or equal to 10. + /// + [Range(MinimumHedgedAttempts, MaximumHedgedAttempts)] + public int MaxHedgedAttempts { get; set; } = DefaultMaxHedgedAttempts; + + private Predicate _shouldHandleException = _ => true; + private Func _onHedgingAsync = _ => Task.CompletedTask; + + /// + /// Gets or sets the exception predicate to filter the type of exception the policy can handle. + /// + /// + /// By default any exception will be retried. + /// + [Required] + public Predicate ShouldHandleException + { + get => _shouldHandleException; + set => _shouldHandleException = Throw.IfNull(value); + } + + /// + /// Gets or sets the action to call asynchronously before invoking the hedging delegate. + /// + [Required] + public Func OnHedgingAsync + { + get => _onHedgingAsync; + set => _onHedgingAsync = Throw.IfNull(value); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingPolicyOptionsT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingPolicyOptionsT.cs new file mode 100644 index 0000000000..8868f4ed37 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingPolicyOptionsT.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience.Options; + +#pragma warning disable SA1649 // File name should match first type name + +/// +/// Hedging policy options. +/// +/// The type of the result handled by the policy. +public class HedgingPolicyOptions : HedgingPolicyOptions +{ + private Predicate _shouldHandleResultAsError = _ => false; + private Func, Task> _onHedgingAsync = _ => Task.CompletedTask; + + /// + /// Gets or sets the predicate to filter results the policy will handle. + /// + /// + /// By default, it will not retry any final result. + /// + [Required] + public Predicate ShouldHandleResultAsError + { + get => _shouldHandleResultAsError; + set => _shouldHandleResultAsError = Throw.IfNull(value); + } + + /// + /// Gets or sets the action to call asynchronously before invoking the hedging delegate. + /// + [Required] + public new Func, Task> OnHedgingAsync + { + get => _onHedgingAsync; + set => _onHedgingAsync = Throw.IfNull(value); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingTaskArguments.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingTaskArguments.cs new file mode 100644 index 0000000000..456e3be996 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingTaskArguments.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Structure with the arguments of the on hedging task. +/// +#pragma warning disable CA1815 // Override equals and operator equals on value types (Such usage is not expected in this scenario) +public readonly struct HedgingTaskArguments : IPolicyEventArguments +#pragma warning restore CA1815 +{ + /// + /// Initializes a new instance of the structure. + /// + /// The exception. + /// The policy context. + /// The attempt number. + /// The cancellation token. + public HedgingTaskArguments( + Exception exception, + Context context, + int attemptNumber, + CancellationToken cancellationToken) + { + Exception = Throw.IfNull(exception); + Context = Throw.IfNull(context); + AttemptNumber = Throw.IfLessThan(attemptNumber, 0); + CancellationToken = cancellationToken; + } + + /// + /// Gets the exception of the action executed by the retry policy. + /// + public Exception Exception { get; } + + /// + /// Gets the attempt number. + /// + public int AttemptNumber { get; } + + /// + /// Gets the Polly associated with the policy execution. + /// + public Context Context { get; } + + /// + /// Gets the cancellation token associated with the policy execution. + /// + public CancellationToken CancellationToken { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingTaskArgumentsT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingTaskArgumentsT.cs new file mode 100644 index 0000000000..94de5116e7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/HedgingTaskArgumentsT.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience.Options; + +#pragma warning disable SA1649 // File name should match first type name + +/// +/// Structure with the arguments of the on hedging task. +/// +/// The type of the result handled by the policy. +#pragma warning disable CA1815 // Override equals and operator equals on value types (Such usage is not expected in this scenario) +public readonly struct HedgingTaskArguments : IPolicyEventArguments +#pragma warning restore CA1815 +{ + /// + /// Initializes a new instance of the structure. + /// + /// The result. + /// The policy context. + /// The attempt number. + /// The cancellation token. + public HedgingTaskArguments( + DelegateResult result, + Context context, + int attemptNumber, + CancellationToken cancellationToken) + { + Result = Throw.IfNull(result); + Context = Throw.IfNull(context); + AttemptNumber = Throw.IfLessThan(attemptNumber, 0); + CancellationToken = cancellationToken; + } + + /// + /// Gets the result of the action executed by the retry policy. + /// + public DelegateResult Result { get; } + + /// + /// Gets the attempt number. + /// + public int AttemptNumber { get; } + + /// + /// Gets the Polly associated with the policy execution. + /// + public Context Context { get; } + + /// + /// Gets the cancellation token associated with the policy execution. + /// + public CancellationToken CancellationToken { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/IPolicyEventArguments.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/IPolicyEventArguments.cs new file mode 100644 index 0000000000..5d39db0cc8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/IPolicyEventArguments.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Polly; + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Lexical interface for all non-generic policy arguments. +/// Do not use outside Argument struct header to avoid overhead. +/// +internal interface IPolicyEventArguments +{ + /// + /// Gets policy argument cancellation token. + /// + public CancellationToken CancellationToken { get; } + + /// + /// Gets the Polly associated with the event. + /// + public Context Context { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/IPolicyEventArgumentsT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/IPolicyEventArgumentsT.cs new file mode 100644 index 0000000000..e45bd6bdb5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/IPolicyEventArgumentsT.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Polly; + +namespace Microsoft.Extensions.Resilience.Options; + +#pragma warning disable SA1649 // File name should match first type name + +/// +/// Lexical interface for all generic policy arguments. +/// Do not use outside Argument struct header to avoid overhead. +/// +/// The type of the result handled by the policy. +internal interface IPolicyEventArguments : IPolicyEventArguments +{ + /// + /// Gets the result of the action executed by the policy. + /// + public DelegateResult Result { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/ResetActionArguments.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/ResetActionArguments.cs new file mode 100644 index 0000000000..920fddb0b3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/ResetActionArguments.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Structure with the arguments of the on reset action. +/// +#pragma warning disable CA1815 // Override equals and operator equals on value types (Such usage is not expected in this scenario) +public readonly struct ResetActionArguments : IPolicyEventArguments +#pragma warning restore CA1815 +{ + /// + /// Initializes a new instance of the structure. + /// + /// The policy context. + /// The cancellation token. + public ResetActionArguments( + Context context, + CancellationToken cancellationToken) + { + Context = Throw.IfNull(context); + CancellationToken = cancellationToken; + } + + /// + /// Gets the Polly associated with the policy execution. + /// + public Context Context { get; } + + /// + /// Gets the cancellation token associated with the policy execution. + /// + public CancellationToken CancellationToken { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryActionArguments.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryActionArguments.cs new file mode 100644 index 0000000000..e3e25235a4 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryActionArguments.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Structure with the arguments of the on retry action. +/// +#pragma warning disable CA1815 // Override equals and operator equals on value types (Such usage is not expected in this scenario) +public readonly struct RetryActionArguments : IPolicyEventArguments +#pragma warning restore CA1815 +{ + /// + /// Initializes a new instance of the structure. + /// + /// The exception. + /// The policy context. + /// The waiting time interval. + /// The attempt number. + /// The cancellation token. + public RetryActionArguments( + Exception exception, + Context context, + TimeSpan waitingTimeInterval, + int attemptNumber, + CancellationToken cancellationToken) + { + _ = Throw.IfLessThan(waitingTimeInterval.Ticks, 0); + WaitingTimeInterval = waitingTimeInterval; + Exception = Throw.IfNull(exception); + Context = Throw.IfNull(context); + AttemptNumber = Throw.IfLessThan(attemptNumber, 0); + CancellationToken = cancellationToken; + } + + /// + /// Gets the result of the action executed by the retry policy. + /// + public Exception Exception { get; } + + /// + /// Gets the waiting time interval. + /// + public TimeSpan WaitingTimeInterval { get; } + + /// + /// Gets the attempt number. + /// + public int AttemptNumber { get; } + + /// + /// Gets the Polly associated with the policy execution. + /// + public Context Context { get; } + + /// + /// Gets the cancellation token associated with the policy execution. + /// + public CancellationToken CancellationToken { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryActionArgumentsT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryActionArgumentsT.cs new file mode 100644 index 0000000000..15cf88e351 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryActionArgumentsT.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience.Options; + +#pragma warning disable SA1649 // File name should match first type name + +/// +/// Structure with the arguments of the on retry action. +/// +/// The type of the result handled by the policy. +#pragma warning disable CA1815 // Override equals and operator equals on value types (Such usage is not expected in this scenario) +public readonly struct RetryActionArguments : IPolicyEventArguments +#pragma warning restore CA1815 +{ + /// + /// Initializes a new instance of the structure. + /// + /// The result. + /// The policy context. + /// The waiting time interval. + /// The attempt number. + /// The cancellation token. + public RetryActionArguments( + DelegateResult result, + Context context, + TimeSpan waitingTimeInterval, + int attemptNumber, + CancellationToken cancellationToken) + { + _ = Throw.IfLessThan(waitingTimeInterval.Ticks, 0); + WaitingTimeInterval = waitingTimeInterval; + Result = Throw.IfNull(result); + Context = Throw.IfNull(context); + AttemptNumber = Throw.IfLessThan(attemptNumber, 0); + CancellationToken = cancellationToken; + } + + /// + /// Gets the result of the action executed by the retry policy. + /// + public DelegateResult Result { get; } + + /// + /// Gets the waiting time interval. + /// + public TimeSpan WaitingTimeInterval { get; } + + /// + /// Gets the attempt number. + /// + public int AttemptNumber { get; } + + /// + /// Gets the Polly associated with the policy execution. + /// + public Context Context { get; } + + /// + /// Gets the cancellation token associated with the policy execution. + /// + public CancellationToken CancellationToken { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryDelayArguments.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryDelayArguments.cs new file mode 100644 index 0000000000..2e92869533 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryDelayArguments.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Structure with the arguments of the delay generator. +/// +/// The type of the result handled by the policy. +#pragma warning disable CA1815 // Override equals and operator equals on value types (Such usage is not expected in this scenario) +public readonly struct RetryDelayArguments : IPolicyEventArguments +#pragma warning restore CA1815 +{ + /// + /// Initializes a new instance of the structure. + /// + /// The result. + /// The policy context. + /// The cancellation token. + public RetryDelayArguments( + DelegateResult result, + Context context, + CancellationToken cancellationToken) + { + Result = result; + Context = Throw.IfNull(context); + CancellationToken = cancellationToken; + } + + /// + /// Gets the Polly associated with the policy execution. + /// + public Context Context { get; } + + /// + /// Gets the cancellation token associated with the policy execution. + /// + public CancellationToken CancellationToken { get; } + + /// + /// Gets the result of the action executed by the retry policy. + /// + public DelegateResult Result { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryPolicyOptions.cs new file mode 100644 index 0000000000..7355f07bbf --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryPolicyOptions.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.Shared.Data.Validation; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Retry policy options. +/// +public class RetryPolicyOptions +{ + /// + /// Magic value representing infinite retries. + /// + public const int InfiniteRetry = -1; + + /// + /// Maximal allowed retry counts unless infinite. + /// + internal const int MaxRetryCount = 100; + + /// + /// Maximal allowed BaseDelay (1 day). + /// + internal const int MaxBaseDelay = 24 * 3600 * 1000; + + private const int DefaultRetryCount = 3; + private const BackoffType DefaultBackoffType = BackoffType.ExponentialWithJitter; + private static readonly TimeSpan _defaultBackoffBasedDelay = TimeSpan.FromSeconds(2); + + /// + /// Gets or sets the maximum number of retries to use, in addition to the original call. + /// + /// + /// For infinite retries use InfiniteRetry (-1). + /// + [Range(InfiniteRetry, MaxRetryCount)] + public int RetryCount { get; set; } = DefaultRetryCount; + + /// + /// Gets or sets the type of the back-off. + /// + /// + /// Default set to . + /// + public BackoffType BackoffType { get; set; } = DefaultBackoffType; + + /// + /// Gets or sets the delay between retries based on the backoff type, . + /// + /// + /// Default set to 2 seconds. + /// For this represents the median delay to target before the first retry. + /// For the it represents the initial delay, the following delays increasing linearly with this value. + /// In case of it represents the constant delay between retries. + /// + [TimeSpan(0, MaxBaseDelay)] + public TimeSpan BaseDelay { get; set; } = _defaultBackoffBasedDelay; + + private Predicate _shouldHandleException = _ => true; + private Func _onRetryAsync = _ => Task.CompletedTask; + + /// + /// Gets or sets the predicate which filters the type of exception the policy can handle. + /// + /// + /// By default any exception will be retried. + /// + [Required] + public Predicate ShouldHandleException + { + get => _shouldHandleException; + set => _shouldHandleException = Throw.IfNull(value); + } + + /// + /// Gets or sets the action performed during the retry attempt of the retry policy. + /// + [Required] + public Func OnRetryAsync + { + get => _onRetryAsync; + set => _onRetryAsync = Throw.IfNull(value); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryPolicyOptionsT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryPolicyOptionsT.cs new file mode 100644 index 0000000000..1bc17eeb2d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/RetryPolicyOptionsT.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience.Options; + +#pragma warning disable SA1649 // File name should match first type name + +/// +/// Retry policy options. +/// +/// The type of the result handled by the policy. +public class RetryPolicyOptions : RetryPolicyOptions +{ + internal static readonly Func, Task> DefaultOnRetryAsync = _ => Task.CompletedTask; + + private Predicate _shouldHandleResultAsError = _ => false; + private Func, Task> _onRetryAsync = DefaultOnRetryAsync; + + /// + /// Gets or sets the predicate which defines what results shall be + /// treated as transient error by the policy. + /// + /// + /// By default, it will not retry any final result. + /// + [Required] + public Predicate ShouldHandleResultAsError + { + get => _shouldHandleResultAsError; + set => _shouldHandleResultAsError = Throw.IfNull(value); + } + + /// + /// Gets or sets the action performed during the retry attempt of the retry policy. + /// + [Required] + public new Func, Task> OnRetryAsync + { + get => _onRetryAsync; + set => _onRetryAsync = Throw.IfNull(value); + } + + /// + /// Gets or sets the delegate for customizing delay for the retry policy. + /// + /// + /// By default this is null and the delay will be calculated based on the backoff type only. + /// + public Func, TimeSpan>? RetryDelayGenerator { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/TimeoutPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/TimeoutPolicyOptions.cs new file mode 100644 index 0000000000..33fdf18470 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/TimeoutPolicyOptions.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.Shared.Data.Validation; +using Microsoft.Shared.Diagnostics; +using Polly.Timeout; + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Options for the timeout policy. +/// +public class TimeoutPolicyOptions +{ + private static readonly Func _defaultOnTimedOutAsync = _ => Task.CompletedTask; + private static readonly TimeSpan _defaultTimeoutInterval = TimeSpan.FromSeconds(30); + + /// + /// Gets or sets the timeout interval. + /// + /// + /// Default set to 30 seconds. + /// + [TimeSpan(0, Exclusive = true)] + public TimeSpan TimeoutInterval { get; set; } = _defaultTimeoutInterval; + + /// + /// Gets or sets the timeout strategy. + /// + /// + /// Default is set to Optimistic Timeout strategy: + /// . + /// + public TimeoutStrategy TimeoutStrategy { get; set; } = TimeoutStrategy.Optimistic; + + private Func _onTimedOutAsync = _defaultOnTimedOutAsync; + + /// + /// Gets or sets the action performed during the timeout attempt of the timeout policy. + /// + [Required] + public Func OnTimedOutAsync + { + get => _onTimedOutAsync; + set => _onTimedOutAsync = Throw.IfNull(value); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/TimeoutTaskArguments.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/TimeoutTaskArguments.cs new file mode 100644 index 0000000000..53256e2c9d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/Options/TimeoutTaskArguments.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience.Options; + +/// +/// Structure with the arguments of the on timeout task. +/// +#pragma warning disable CA1815 // Override equals and operator equals on value types (Such usage is not expected in this scenario) +public readonly struct TimeoutTaskArguments : IPolicyEventArguments +#pragma warning restore CA1815 +{ + /// + /// Initializes a new instance of the structure. + /// + /// The policy context. + /// The cancellation token. + public TimeoutTaskArguments( + Context context, + CancellationToken cancellationToken) + { + Context = Throw.IfNull(context); + CancellationToken = cancellationToken; + } + + /// + /// Gets the Polly associated with the policy execution. + /// + public Context Context { get; } + + /// + /// Gets the cancellation token associated with the policy execution. + /// + public CancellationToken CancellationToken { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/PollyServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/PollyServiceCollectionExtensions.cs new file mode 100644 index 0000000000..07f53b0720 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/PollyServiceCollectionExtensions.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience; + +/// +/// Extension class for the Service Collection DI container. +/// +public static class PollyServiceCollectionExtensions +{ + /// + /// Configures the failure result dimensions. + /// + /// The type of the policy result. + /// The services. + /// The configure result dimensions. + /// The input . + public static IServiceCollection ConfigureFailureResultContext( + this IServiceCollection services, + Func configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + return services.Configure>(option => option.GetContextFromResult = configure); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Polly/ResilienceDimensions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Polly/ResilienceDimensions.cs new file mode 100644 index 0000000000..52705a74e0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Polly/ResilienceDimensions.cs @@ -0,0 +1,112 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Resilience; + +/// +/// Constants used for enrichment dimensions. +/// +/// +/// Constants are standardized in MS Common Schema. +/// +// Avoid changing const values in this class by all means. Such a breaking change would break customer's monitoring. +[Experimental] +[EditorBrowsable(EditorBrowsableState.Never)] +public static class ResilienceDimensions +{ + /// + /// Pipeline name. + /// + [Experimental] + [EditorBrowsable(EditorBrowsableState.Never)] + public const string PipelineName = "pipeline_name"; + + /// + /// Pipeline key. + /// + [Experimental] + [EditorBrowsable(EditorBrowsableState.Never)] + public const string PipelineKey = "pipeline_key"; + + /// + /// Result type. + /// + [Experimental] + [EditorBrowsable(EditorBrowsableState.Never)] + public const string ResultType = "result_type"; + + /// + /// Policy name. + /// + [Experimental] + [EditorBrowsable(EditorBrowsableState.Never)] + public const string PolicyName = "policy_name"; + + /// + /// Event name. + /// + [Experimental] + [EditorBrowsable(EditorBrowsableState.Never)] + public const string EventName = "event_name"; + + /// + /// Failure source. + /// + [Experimental] + [EditorBrowsable(EditorBrowsableState.Never)] + public const string FailureSource = "failure_source"; + + /// + /// Failure reason. + /// + [Experimental] + [EditorBrowsable(EditorBrowsableState.Never)] + public const string FailureReason = "failure_reason"; + + /// + /// Failure summary. + /// + [Experimental] + [EditorBrowsable(EditorBrowsableState.Never)] + public const string FailureSummary = "failure_summary"; + + /// + /// Dependency name. + /// + [Experimental] + [EditorBrowsable(EditorBrowsableState.Never)] + public const string DependencyName = "dep_name"; + + /// + /// Request name. + /// + [Experimental] + [EditorBrowsable(EditorBrowsableState.Never)] + public const string RequestName = "req_name"; + + /// + /// Gets a list of all dimension names. + /// + /// A read-only of all dimension names. + [Experimental] + [EditorBrowsable(EditorBrowsableState.Never)] + public static IReadOnlyList DimensionNames { get; } = + Array.AsReadOnly(new[] + { + PipelineName, + PipelineKey, + ResultType, + PolicyName, + EventName, + FailureSource, + FailureReason, + FailureSummary, + DependencyName, + RequestName, + }); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/GlobalSuppressions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/GlobalSuppressions.cs new file mode 100644 index 0000000000..5ce4d7eee2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/GlobalSuppressions.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Misc", "CS8002", Justification = "Referenced assemblies from within R9 SDK without strong name are accepted")] diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/IResiliencePipelineBuilder.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/IResiliencePipelineBuilder.cs new file mode 100644 index 0000000000..a4391da04b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/IResiliencePipelineBuilder.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Resilience; + +/// +/// The builder for configuring the policy pipeline. +/// +/// The type of the result this pipeline handles. +public interface IResiliencePipelineBuilder +{ + /// + /// Gets the name of the pipeline configured by this builder. + /// + string PipelineName { get; } + + /// + /// Gets the application service collection. + /// + IServiceCollection Services { get; } +} + diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/IResiliencePipelineProvider.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/IResiliencePipelineProvider.cs new file mode 100644 index 0000000000..49c3ce6ac7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/IResiliencePipelineProvider.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Options; +using Polly; + +namespace Microsoft.Extensions.Resilience; + +/// +/// The resilience pipeline provider creates and caches pipeline instances that are configured using . +/// +/// +/// +/// +/// +/// Use this interface to create instances of both generic and non-generic resilience pipelines. +/// +public interface IResiliencePipelineProvider +{ + /// Gets the pipeline instance. + /// A pipeline name. + /// The pipeline instance. + /// The type of the result returned by the action executed by the policies. + /// The cannot be an empty string. + /// The pipeline identified by is invalid or not configured. + /// + /// Make sure that the pipeline identified by is configured, otherwise the provider won't be able to create + /// it and throws an error. + /// + public IAsyncPolicy GetPipeline(string pipelineName); + + /// Gets a pipeline instance cached by . If the target pipeline is not cached yet, + /// the provider creates and caches it and then returns the instance. + /// A pipeline name. + /// The pipeline key associated with a cached instance of a pipeline. + /// The pipeline instance. + /// The type of the result returned by the action executed by the policies. + /// The cannot be an empty string. + /// The pipeline identified by is invalid or not configured. + /// + /// This method enables to have multiple instances of the same pipeline that are cached by the . + /// Make sure that the pipeline identified by is configured, otherwise the provider won't be able to create + /// it and throws an error. + /// + public IAsyncPolicy GetPipeline(string pipelineName, string pipelineKey); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncDynamicPipelineT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncDynamicPipelineT.cs new file mode 100644 index 0000000000..1f0d049fbc --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncDynamicPipelineT.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Polly; + +namespace Microsoft.Extensions.Resilience.Internal; + +#pragma warning disable SA1649 // File name should match first type name +internal sealed class AsyncDynamicPipeline : AsyncPolicy, IDisposable +{ + private readonly string _pipelineName; + private readonly Func, IAsyncPolicy> _pipelineFactory; + private readonly IDisposable? _changeListener; + private readonly ILogger> _logger; + + internal IAsyncPolicy CurrentValue { get; private set; } + + public AsyncDynamicPipeline( + string pipelineName, + IOptionsMonitor> optionsMonitor, + Func, IAsyncPolicy> factory, + ILogger> logger) + { + _pipelineName = pipelineName; + _pipelineFactory = factory; + _logger = logger; + + CurrentValue = _pipelineFactory(optionsMonitor.Get(_pipelineName)); + _changeListener = optionsMonitor.OnChange(UpdatePipeline); + } + + public void Dispose() + { + _changeListener?.Dispose(); + } + + protected override Task ImplementationAsync( + Func> action, + Context context, + CancellationToken cancellationToken, + bool continueOnCapturedContext) + { + return CurrentValue.ExecuteAsync(action, context, cancellationToken, continueOnCapturedContext); + } + + private void UpdatePipeline( + ResiliencePipelineFactoryOptions latestOptions, + string? changedPipelineName) + { + // Stryker disable once all: Stryker tries to swap `&&` with `||`. The pipelineName cannot be null. + if (changedPipelineName != null && changedPipelineName == _pipelineName) + { + try + { + CurrentValue = _pipelineFactory(latestOptions); + _logger.PipelineUpdated(_pipelineName); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) + { + // If the refresh of a pipeline fails due to any reasons, we do not want to break the entire execution, + // hence the event will be logged and old pipeline preserved. + _logger.PipelineUpdatedFailure(_pipelineName, ex); + } +#pragma warning restore CA1031 + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncPolicyPipeline.Args.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncPolicyPipeline.Args.cs new file mode 100644 index 0000000000..f343323c3a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncPolicyPipeline.Args.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.Pools; +using Polly; + +namespace Microsoft.Extensions.Resilience.Internal; + +internal sealed partial class AsyncPolicyPipeline +{ + private static class ArgPool + { + public static readonly ObjectPool> Instance = PoolFactory.CreatePool>(); + } + + /// + /// Structure with the arguments of the execute methods. + /// + private sealed class PollyExecuteAsyncArguments + { + public Func>? Action { get; set; } + + public Context? Context { get; set; } + + public bool ContinueOnCapturedContext { get; set; } + + public CancellationToken CancellationToken { get; set; } = CancellationToken.None; + + public Policies? Policies { get; set; } + } + + private sealed class Policies + { + public Policies(IReadOnlyList policies) + { +#pragma warning disable S109 // Magic numbers should not be used + Policy0 = GetPolicy(policies, 0); + Policy1 = GetPolicy(policies, 1); + Policy2 = GetPolicy(policies, 2); + Policy3 = GetPolicy(policies, 3); + Policy4 = GetPolicy(policies, 4); +#pragma warning restore S109 // Magic numbers should not be used + } + + public IAsyncPolicy? Policy0 { get; } + + public IAsyncPolicy? Policy1 { get; } + + public IAsyncPolicy? Policy2 { get; } + + public IAsyncPolicy? Policy3 { get; } + + public IAsyncPolicy? Policy4 { get; } + + private static IAsyncPolicy? GetPolicy(IReadOnlyList policies, int index) + { + if (index >= policies.Count) + { + return null; + } + + return policies[index]; + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncPolicyPipeline.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncPolicyPipeline.cs new file mode 100644 index 0000000000..b4291ddcb6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncPolicyPipeline.cs @@ -0,0 +1,211 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience.Internal; + +#pragma warning disable S109 // Magic numbers should not be used + +/// +/// The Async policy running the sequence of other async policies as pipelines. +/// +internal sealed partial class AsyncPolicyPipeline : AsyncPolicy +{ + private delegate Task ExecuteDelegate(PollyExecuteAsyncArguments args); + + /// + /// The maximum number of policies for which we support a zero-allocation implementation. + /// Above this number we use implementation allocating callback actions as array. + /// + /// This value was selected based on the maximum number of layers that are used by us in the standard pipeline. + private const int MaximumPoliciesOptimized = 5; + + private const string ArgsKey = "AsyncPolicyPipeline.PollyExecuteAsyncArguments"; + + private readonly ConcurrentDictionary _executeDelegates = new(); + private readonly IAsyncPolicy[] _frozenPolicies; + private readonly Policies _policies; + + /// + /// Initializes a new instance of the class. + /// + /// The sequence of policies to run. + public AsyncPolicyPipeline(IReadOnlyList policies) + { + if (policies.Count == 0) + { + Throw.ArgumentException(nameof(policies), "Argument is an empty collection"); + } + + _frozenPolicies = policies.ToArray(); + _policies = new Policies(policies); + } + + protected override async Task ImplementationAsync( + Func> action, + Context context, + CancellationToken cancellationToken, + bool continueOnCapturedContext) + { + var args = ArgPool.Instance.Get(); + args.Action = action; + args.Context = context; + args.CancellationToken = cancellationToken; + args.Policies = _policies; + args.ContinueOnCapturedContext = continueOnCapturedContext; + + context[ArgsKey] = args; + + var executeDelegate = GetExecuteDelegate(); + + try + { + return await executeDelegate(args).ConfigureAwait(false); + } + finally + { + ArgPool.Instance.Return(args); + } + } + + private static ExecuteDelegate CreateExecuteDelegate1() + { + return static args => args.Policies!.Policy0!.ExecuteAsync(args.Action, args.Context, args.CancellationToken, args.ContinueOnCapturedContext); + } + + private static ExecuteDelegate CreateExecuteDelegate2() + { + return static args => args.Policies!.Policy0!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy1!.ExecuteAsync(args.Action, context, cancellationToken, args.ContinueOnCapturedContext); + }, + args.Context, args.CancellationToken, args.ContinueOnCapturedContext); + } + + private static ExecuteDelegate CreateExecuteDelegate3() + { + return static args => args.Policies!.Policy0!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy1!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy2!.ExecuteAsync(args.Action, context, cancellationToken, args.ContinueOnCapturedContext); + }, + context, cancellationToken, args.ContinueOnCapturedContext); + }, + args.Context, args.CancellationToken, args.ContinueOnCapturedContext); + } + + private static ExecuteDelegate CreateExecuteDelegate4() + { + return static args => args.Policies!.Policy0!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy1!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy2!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy3!.ExecuteAsync(args.Action, context, cancellationToken, args.ContinueOnCapturedContext); + }, + context, cancellationToken, args.ContinueOnCapturedContext); + }, + context, cancellationToken, args.ContinueOnCapturedContext); + }, + args.Context, args.CancellationToken, args.ContinueOnCapturedContext); + } + + private static ExecuteDelegate CreateExecuteDelegate5() + { + return args => args.Policies!.Policy0!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy1!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy2!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy3!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy4!.ExecuteAsync(args.Action, context, cancellationToken, args.ContinueOnCapturedContext); + }, + context, cancellationToken, args.ContinueOnCapturedContext); + }, + context, cancellationToken, args.ContinueOnCapturedContext); + }, + context, cancellationToken, args.ContinueOnCapturedContext); + }, + args.Context, args.CancellationToken, args.ContinueOnCapturedContext); + } + + private static ExecuteDelegate CreateExecuteDelegateFromManyPolicies(IAsyncPolicy[] policies) + { + policies = policies.Reverse().ToArray(); + + var actions = new ExecuteDelegate[policies.Length]; + actions[0] = args => + policies[0].ExecuteAsync( + args.Action, + args.Context, + args.CancellationToken, + args.ContinueOnCapturedContext); + + for (int i = 1; i < policies.Length; i++) + { + var nextPolicy = policies[i]; + var nextAction = actions[i - 1]; + actions[i] = (args) => + nextPolicy.ExecuteAsync( + (context, _) => nextAction(GetArgs(context)), + args.Context, + args.CancellationToken, + args.ContinueOnCapturedContext); + } + + return actions[actions.Length - 1]; + } + + private static ExecuteDelegate CreateExecuteDelegate(IAsyncPolicy[] policies) + { + return policies.Length switch + { + 1 => CreateExecuteDelegate1(), + 2 => CreateExecuteDelegate2(), + 3 => CreateExecuteDelegate3(), + 4 => CreateExecuteDelegate4(), + MaximumPoliciesOptimized => CreateExecuteDelegate5(), + _ => CreateExecuteDelegateFromManyPolicies(policies) + }; + } + + private static PollyExecuteAsyncArguments GetArgs(Context ctxt) => (PollyExecuteAsyncArguments)ctxt[ArgsKey]; + + private ExecuteDelegate GetExecuteDelegate() + { + return (ExecuteDelegate)_executeDelegates.GetOrAdd(typeof(T), key => CreateExecuteDelegate(_frozenPolicies)); + } +} +#pragma warning restore S109 // Magic numbers should not be used diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncPolicyPipelineT.Args.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncPolicyPipelineT.Args.cs new file mode 100644 index 0000000000..b611ca2aa1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncPolicyPipelineT.Args.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Polly; + +namespace Microsoft.Extensions.Resilience.Internal; + +internal sealed partial class AsyncPolicyPipeline +{ + /// + /// Structure with the arguments of the execute methods. + /// + private sealed class PollyExecuteAsyncArguments + { + public Func>? Action { get; set; } + + public Context? Context { get; set; } + + public bool ContinueOnCapturedContext { get; set; } + + public CancellationToken CancellationToken { get; set; } = CancellationToken.None; + + public Policies? Policies { get; set; } + } + + private sealed class Policies + { + public Policies(IReadOnlyList> policies) + { +#pragma warning disable S109 // Magic numbers should not be used + Policy0 = GetPolicy(policies, 0); + Policy1 = GetPolicy(policies, 1); + Policy2 = GetPolicy(policies, 2); + Policy3 = GetPolicy(policies, 3); + Policy4 = GetPolicy(policies, 4); +#pragma warning restore S109 // Magic numbers should not be used + } + + public IAsyncPolicy? Policy0 { get; } + + public IAsyncPolicy? Policy1 { get; } + + public IAsyncPolicy? Policy2 { get; } + + public IAsyncPolicy? Policy3 { get; } + + public IAsyncPolicy? Policy4 { get; } + + private static IAsyncPolicy? GetPolicy(IReadOnlyList> policies, int index) + { + if (index >= policies.Count) + { + return null; + } + + return policies[index]; + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncPolicyPipelineT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncPolicyPipelineT.cs new file mode 100644 index 0000000000..67db938002 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/AsyncPolicyPipelineT.cs @@ -0,0 +1,201 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Pools; +using Polly; + +namespace Microsoft.Extensions.Resilience.Internal; + +#pragma warning disable S109 // Magic numbers should not be used +/// +/// The Async policy running the sequence of other async policies as pipelines. +/// +/// The type of the result handled by the policy. +internal sealed partial class AsyncPolicyPipeline : AsyncPolicy +{ + private delegate Task ExecuteDelegate(PollyExecuteAsyncArguments args); + + /// + /// The maximum number of policies for which we support a zero-allocation implementation. + /// Above this number we use implementation allocating callback actions as array. + /// + /// This value was selected based on the maximum number of layers that are used by us in the standard pipeline. + private const int MaximumPoliciesOptimized = 5; + + private const string ArgsKey = "AsyncPolicyPipeline.PollyExecuteAsyncArguments"; + + private static readonly ObjectPool _argPool = PoolFactory.CreatePool(); + + private readonly ExecuteDelegate _executeDelegate; + private readonly Policies _policies; + + /// + /// Initializes a new instance of the class. + /// + /// The sequence of policies to run. + public AsyncPolicyPipeline(IReadOnlyList> policies) + { + if (policies.Count == 0) + { + Throw.ArgumentException(nameof(policies), "Argument is an empty collection"); + } + + _executeDelegate = policies.Count switch + { + 1 => CreateExecuteDelegate1(), + 2 => CreateExecuteDelegate2(), + 3 => CreateExecuteDelegate3(), + 4 => CreateExecuteDelegate4(), + MaximumPoliciesOptimized => CreateExecuteDelegate5(), + _ => CreateExecuteDelegate(policies) + }; + _policies = new Policies(policies); + } + + protected override async Task ImplementationAsync( + Func> action, + Context context, + CancellationToken cancellationToken, + bool continueOnCapturedContext) + { + var args = _argPool.Get(); + args.Action = action; + args.Context = context; + args.CancellationToken = cancellationToken; + args.Policies = _policies; + args.ContinueOnCapturedContext = continueOnCapturedContext; + + context[ArgsKey] = args; + + try + { + return await _executeDelegate(args).ConfigureAwait(false); + } + finally + { + _argPool.Return(args); + } + } + + private static ExecuteDelegate CreateExecuteDelegate1() + { + return static args => args.Policies!.Policy0!.ExecuteAsync(args.Action, args.Context, args.CancellationToken, args.ContinueOnCapturedContext); + } + + private static ExecuteDelegate CreateExecuteDelegate2() + { + return static args => args.Policies!.Policy0!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy1!.ExecuteAsync(args.Action, context, cancellationToken, args.ContinueOnCapturedContext); + }, + args.Context, args.CancellationToken, args.ContinueOnCapturedContext); + } + + private static ExecuteDelegate CreateExecuteDelegate3() + { + return static args => args.Policies!.Policy0!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy1!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy2!.ExecuteAsync(args.Action, context, cancellationToken, args.ContinueOnCapturedContext); + }, + context, cancellationToken, args.ContinueOnCapturedContext); + }, + args.Context, args.CancellationToken, args.ContinueOnCapturedContext); + } + + private static ExecuteDelegate CreateExecuteDelegate4() + { + return static args => args.Policies!.Policy0!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy1!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy2!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy3!.ExecuteAsync(args.Action, context, cancellationToken, args.ContinueOnCapturedContext); + }, + context, cancellationToken, args.ContinueOnCapturedContext); + }, + context, cancellationToken, args.ContinueOnCapturedContext); + }, + args.Context, args.CancellationToken, args.ContinueOnCapturedContext); + } + + private static ExecuteDelegate CreateExecuteDelegate5() + { + return args => args.Policies!.Policy0!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy1!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy2!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy3!.ExecuteAsync( + static (context, cancellationToken) => + { + var args = GetArgs(context); + return args.Policies!.Policy4!.ExecuteAsync(args.Action, context, cancellationToken, args.ContinueOnCapturedContext); + }, + context, cancellationToken, args.ContinueOnCapturedContext); + }, + context, cancellationToken, args.ContinueOnCapturedContext); + }, + context, cancellationToken, args.ContinueOnCapturedContext); + }, + args.Context, args.CancellationToken, args.ContinueOnCapturedContext); + } + + private static ExecuteDelegate CreateExecuteDelegate(IReadOnlyList> policies) + { + policies = policies.Reverse().ToArray(); + + var actions = new ExecuteDelegate[policies.Count]; + actions[0] = args => + policies[0].ExecuteAsync( + args.Action, + args.Context, + args.CancellationToken, + args.ContinueOnCapturedContext); + + for (int i = 1; i < policies.Count; i++) + { + var nextPolicy = policies[i]; + var nextAction = actions[i - 1]; + actions[i] = (args) => + nextPolicy.ExecuteAsync( + (context, _) => nextAction(GetArgs(context)), + args.Context, + args.CancellationToken, + args.ContinueOnCapturedContext); + } + + return actions[actions.Length - 1]; + } + + private static PollyExecuteAsyncArguments GetArgs(Context ctxt) => (PollyExecuteAsyncArguments)ctxt[ArgsKey]; +} +#pragma warning restore S109 // Magic numbers should not be used diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IOnChangeListenersHandler.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IOnChangeListenersHandler.cs new file mode 100644 index 0000000000..0bba8e6550 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IOnChangeListenersHandler.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Resilience.Internal; + +/// +/// Interface allowing registration of unique listeners for a given type of options. +/// +internal interface IOnChangeListenersHandler : IDisposable +{ + /// + /// Captures the OnChange event and stores the associated listener for the options instance + /// of type named ensuring only + /// one listener per name and type is created. + /// + /// Type of the options. + /// The name of the options. + /// true if a listener for the same options type and name was not previously registered, otherwise false. + public bool TryCaptureOnChange(string optionsName); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IPipelineMetering.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IPipelineMetering.cs new file mode 100644 index 0000000000..59ea7e4ed2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IPipelineMetering.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Polly; + +namespace Microsoft.Extensions.Resilience.Internal; + +/// +/// Metering support for pipelines. +/// +internal interface IPipelineMetering +{ + /// + /// Initializes the instance. + /// + /// The pipeline id. + void Initialize(PipelineId pipelineId); + + /// + /// Records the pipeline execution. + /// + /// The pipeline execution time. + /// The fault instance. + /// The context associated with the event. + void RecordPipelineExecution(long executionTimeInMs, Exception? fault, Context context); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IPolicyPipelineBuilder.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IPolicyPipelineBuilder.cs new file mode 100644 index 0000000000..68201ab0d0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IPolicyPipelineBuilder.cs @@ -0,0 +1,106 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Resilience.Options; +using Polly; + +namespace Microsoft.Extensions.Resilience.Internal; + +/// +/// Chains policies, returning a policy wrapper to be registered under a unique key. +/// +internal interface IPolicyPipelineBuilder +{ + /// + /// Sets the pipeline identifiers. + /// + /// The pipeline id. + void Initialize(PipelineId pipelineId); + + /// + /// Adds a circuit breaker policy. + /// + /// The policy name. + /// The containing the options for the circuit breaker. + /// + /// Current instance. + /// + IPolicyPipelineBuilder AddCircuitBreakerPolicy( + string policyName, + CircuitBreakerPolicyOptions options); + + /// + /// Adds a retry policy. + /// + /// The policy name. + /// The containing the options for the retry policy. + /// + /// Current instance. + /// + IPolicyPipelineBuilder AddRetryPolicy( + string policyName, + RetryPolicyOptions options); + + /// + /// Adds a timeout policy. + /// + /// The policy name. + /// The options of the policy. + /// + /// Current instance. + /// + IPolicyPipelineBuilder AddTimeoutPolicy( + string policyName, + TimeoutPolicyOptions options); + + /// + /// Adds a fallback policy. + /// + /// The policy name. + /// The task performed in the fallback scenario when the initial execution encounters a transient failure. + /// The options of the fallback policy. + /// + /// Current instance. + /// + IPolicyPipelineBuilder AddFallbackPolicy( + string policyName, + FallbackScenarioTaskProvider provider, + FallbackPolicyOptions options); + + /// + /// Adds a bulkhead policy. + /// + /// The policy name. + /// The options of the policy. + /// + /// Current instance. + /// + IPolicyPipelineBuilder AddBulkheadPolicy( + string policyName, + BulkheadPolicyOptions options); + + /// + /// Adds the hedging policy. + /// + /// The policy name. + /// The hedged task provider. + /// The options. + /// Current instance. + IPolicyPipelineBuilder AddHedgingPolicy( + string policyName, + HedgedTaskProvider provider, + HedgingPolicyOptions options); + + /// + /// Adds a custom policy to a pipeline. + /// + /// The policy instance. + /// Current instance. + IPolicyPipelineBuilder AddPolicy(IAsyncPolicy policy); + + /// + /// Builds an instance. + /// + /// The policy wrap containing all chained policies. + public IAsyncPolicy Build(); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IPolicyPipelineBuilderT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IPolicyPipelineBuilderT.cs new file mode 100644 index 0000000000..b0c77dfab7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IPolicyPipelineBuilderT.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Resilience.Options; +using Polly; + +namespace Microsoft.Extensions.Resilience.Internal; + +#pragma warning disable SA1649 // File name should match first type name + +/// +/// Chains policies, returning a policy wrapper to be registered under a unique key. +/// +/// The type of the result returned by the action executed by the policies. +internal interface IPolicyPipelineBuilder +{ + /// + /// Sets the pipeline identifiers. + /// + /// The pipeline id. + void Initialize(PipelineId pipelineId); + + /// + /// Adds a circuit breaker policy. + /// + /// The policy name. + /// The containing the options for the circuit breaker. + /// + /// Current instance. + /// + IPolicyPipelineBuilder AddCircuitBreakerPolicy( + string policyName, + CircuitBreakerPolicyOptions options); + + /// + /// Adds a retry policy. + /// + /// The policy name. + /// The containing the options for the retry policy. + /// + /// Current instance. + /// + IPolicyPipelineBuilder AddRetryPolicy( + string policyName, + RetryPolicyOptions options); + + /// + /// Adds a timeout policy. + /// + /// The policy name. + /// The options of the policy. + /// + /// Current instance. + /// + IPolicyPipelineBuilder AddTimeoutPolicy( + string policyName, + TimeoutPolicyOptions options); + + /// + /// Adds a fallback policy. + /// + /// The policy name. + /// The task performed in the fallback scenario when the initial execution encounters a transient failure. + /// The options of the fallback policy. + /// + /// Current instance. + /// + IPolicyPipelineBuilder AddFallbackPolicy( + string policyName, + FallbackScenarioTaskProvider provider, + FallbackPolicyOptions options); + + /// + /// Adds a bulkhead policy. + /// + /// The policy name. + /// The options of the policy. + /// + /// Current instance. + /// + IPolicyPipelineBuilder AddBulkheadPolicy( + string policyName, + BulkheadPolicyOptions options); + + /// + /// Adds the hedging policy. + /// + /// The policy name. + /// The hedged task provider. + /// The options. + /// Current instance. + IPolicyPipelineBuilder AddHedgingPolicy( + string policyName, + HedgedTaskProvider provider, + HedgingPolicyOptions options); + + /// + /// Adds a custom policy to a pipeline. + /// + /// The policy instance. + /// Current instance. + IPolicyPipelineBuilder AddPolicy(IAsyncPolicy policy); + + /// + /// Adds a custom policy to a pipeline. + /// + /// The policy instance. + /// Current instance. + IPolicyPipelineBuilder AddPolicy(IAsyncPolicy policy); + + /// + /// Builds an instance. + /// + /// The policy wrap containing all chained policies. + public IAsyncPolicy Build(); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IResiliencePipelineFactory.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IResiliencePipelineFactory.cs new file mode 100644 index 0000000000..6705323675 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/IResiliencePipelineFactory.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Polly; + +namespace Microsoft.Extensions.Resilience.Internal; + +/// +/// Encapsulates the logic for creation of resilience pipelines. +/// +/// +/// Only allows creation of pipelines that were previously added and configured +/// by using the method. +/// +internal interface IResiliencePipelineFactory +{ + /// + /// Creates a new instance of resilience pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The pipeline name. + /// The pipeline key. + /// The policy pipeline. + /// The is null or empty string. + /// The pipeline identified by is not recognized. + IAsyncPolicy CreatePipeline(string pipelineName, string pipelineKey = ""); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/Log.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/Log.cs new file mode 100644 index 0000000000..f077484fb0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/Log.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.Extensions.Resilience.Internal; + +#pragma warning disable S109 // Magic numbers should not be used + +internal static partial class Log +{ + [LogMethod(0, LogLevel.Debug, "Executing pipeline. Pipeline Name: {pipelineName}, Pipeline Key: {pipelineKey}")] + public static partial void ExecutingPipeline(this ILogger logger, string pipelineName, string pipelineKey); + + [LogMethod(1, LogLevel.Debug, "Pipeline executed in {elapsed}ms. Pipeline Name: {pipelineName}, Pipeline Key: {pipelineKey}")] + public static partial void PipelineExecuted(this ILogger logger, string pipelineName, string pipelineKey, long elapsed); + + [LogMethod(2, LogLevel.Warning, "Pipeline execution failed in {elapsed}ms. Pipeline Name: {pipelineName}, Pipeline Key: {pipelineKey}")] + public static partial void PipelineFailed(this ILogger logger, Exception error, string pipelineName, string pipelineKey, long elapsed); + + [LogMethod(3, LogLevel.Debug, "Pipeline {pipelineName} has been updated.")] + public static partial void PipelineUpdated(this ILogger logger, string pipelineName); + + [LogMethod(4, LogLevel.Warning, "Pipeline update failed. Pipeline Name: {pipelineName}.")] + public static partial void PipelineUpdatedFailure(this ILogger logger, string pipelineName, Exception exception); + + [LogMethod(5, LogLevel.Debug, "Configuration for policy {policyName} has been updated.")] + public static partial void PolicyInPipelineUpdated(this ILogger logger, string policyName); + +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/Metric.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/Metric.cs new file mode 100644 index 0000000000..08b2feba10 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/Metric.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +namespace Microsoft.Extensions.Resilience; + +internal static partial class Metric +{ + [Histogram( + ResilienceDimensions.PipelineName, + ResilienceDimensions.PipelineKey, + ResilienceDimensions.ResultType, + ResilienceDimensions.FailureSource, + ResilienceDimensions.FailureReason, + ResilienceDimensions.FailureSummary, + ResilienceDimensions.DependencyName, + ResilienceDimensions.RequestName, + Name = @"R9\Resilience\Pipelines")] + public static partial PipelinesHistogram CreatePipelinesHistogram(Meter meter); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/NoopChangeToken.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/NoopChangeToken.cs new file mode 100644 index 0000000000..32c9b84450 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/NoopChangeToken.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.Resilience.Internal; +#pragma warning disable CA1001 // Types that own disposable fields should be disposable +internal sealed class NoopChangeToken : IChangeToken +#pragma warning restore CA1001 // Types that own disposable fields should be disposable +{ + private readonly NoopDisposable _noopRegistration = new(); + + public bool HasChanged => false; + + public bool ActiveChangeCallbacks => true; + + public IDisposable RegisterChangeCallback(Action callback, object? state) => _noopRegistration; + + private sealed class NoopDisposable : IDisposable + { + public void Dispose() + { + // No op + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/OnChangeListenersHandler.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/OnChangeListenersHandler.cs new file mode 100644 index 0000000000..1fd2eea87b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/OnChangeListenersHandler.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Resilience.Internal; +internal sealed class OnChangeListenersHandler : IOnChangeListenersHandler +{ + private readonly ConcurrentDictionary<(Type optionsType, string optionsName), IDisposable> _listenersByType = new(); + private readonly IServiceProvider _serviceProvider; + + public OnChangeListenersHandler(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public bool TryCaptureOnChange(string optionsName) + { + var optionsMonitor = _serviceProvider.GetRequiredService>(); + var key = (typeof(TOptions), optionsName); + + if (!_listenersByType.ContainsKey(key)) + { + var listener = optionsMonitor.OnChange((_, name) => + { + if (name == optionsName) + { + var logger = _serviceProvider.GetRequiredService>(); + logger.PolicyInPipelineUpdated(optionsName); + } + }); + + if (listener != null) + { + _ = _listenersByType.TryAdd(key, listener); + + return true; + } + } + + return false; + } + + public void Dispose() + { + foreach (var listenerByType in _listenersByType) + { + listenerByType.Value.Dispose(); + } + + _listenersByType.Clear(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/OptionsBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/OptionsBuilderExtensions.cs new file mode 100644 index 0000000000..fbff724a04 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/OptionsBuilderExtensions.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience.Internal; + +/// +/// Extensions for . +/// +[Experimental] +internal static class OptionsBuilderExtensions +{ + /// + /// Configures the options based on the and . + /// + /// The options type. + /// The options builder instance. + /// The section. This parameter can be null. + /// The configure options action. This parameter can be null. + /// The builder instance. + [UnconditionalSuppressMessage("Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed by [DynamicDependency]")] + public static OptionsBuilder Configure<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + this OptionsBuilder builder, + IConfigurationSection? section, + Action? configureOptions) + where T : class, new() + { + _ = Throw.IfNull(builder); + + if (section != null) + { + _ = builder.Bind(section); + } + + if (configureOptions != null) + { + _ = builder.Configure(configureOptions); + } + + return builder; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/OptionsNameHelper.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/OptionsNameHelper.cs new file mode 100644 index 0000000000..058989baff --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/OptionsNameHelper.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Resilience.Internal; +internal static class OptionsNameHelper +{ + public static string GetPolicyOptionsName(this SupportedPolicies type, string pipelineName, string policyName) => $"{pipelineName}-{type}-{policyName}"; +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PipelineConfigurationChangeTokenSource.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PipelineConfigurationChangeTokenSource.cs new file mode 100644 index 0000000000..fb3a4e86f1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PipelineConfigurationChangeTokenSource.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.Resilience.Internal; + +internal sealed class PipelineConfigurationChangeTokenSource : IOptionsChangeTokenSource> +{ + private static readonly NoopChangeToken _noopChangeToken = new(); + private readonly Func _changeTokenProvider; + + /// + /// Initializes a new instance of the class. + /// which provides the s so that the gets + /// notified when the options of a pipeline changed. + /// A pipeline's options change when any component policy has its options changed. + /// + /// The name of the pipeline for which the token is monitored. + /// The instance handling named options with the sources for + /// changed tokens triggered by the modified configurations of a policy. + public PipelineConfigurationChangeTokenSource( + string name, + IOptionsMonitor> optionsMonitor) + { + Name = name; + + var sources = optionsMonitor.Get(name).ChangeTokenSources; + var sourcesCount = sources.Count; + _changeTokenProvider = sourcesCount switch + { + 0 => () => _noopChangeToken, + 1 => sources[0], + _ => () => new CompositeChangeToken(sources.Select(sourceFunction => sourceFunction()).ToList()) + }; + } + + /// + /// Gets the name of the option instance being changed. + /// + public string Name { get; } + + /// + /// Get a the reloaded token generated by a change in the underlying configuration. + /// + /// The reloadToken from the of a policy. + public IChangeToken GetChangeToken() + { + return _changeTokenProvider(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PipelineMetering.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PipelineMetering.cs new file mode 100644 index 0000000000..d625051b35 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PipelineMetering.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Diagnostics.ExceptionSummarization; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Telemetry.Metering; +using Polly; + +namespace Microsoft.Extensions.Resilience.Internal; + +internal sealed class PipelineMetering : IPipelineMetering +{ + private static readonly RequestMetadata _fallbackMetadata = new(); + + private readonly IExceptionSummarizer _exceptionSummarizer; + private readonly IOutgoingRequestContext? _outgoingRequestContext; + private readonly PipelinesHistogram _histogram; + private PipelineId? _pipelineId; + + public PipelineMetering(Meter meter, IExceptionSummarizer exceptionSummarizer, IEnumerable outgoingContexts) + { + _histogram = Metric.CreatePipelinesHistogram(meter); + _exceptionSummarizer = exceptionSummarizer; + _outgoingRequestContext = outgoingContexts.FirstOrDefault(); + } + + private bool IsInitialized => _pipelineId != null; + + public void Initialize(PipelineId pipelineId) + { + if (IsInitialized) + { + throw new InvalidOperationException("This instance is already initialized."); + } + + _pipelineId = pipelineId; + } + + public void RecordPipelineExecution(long executionTimeInMs, Exception? fault, Context context) + { + if (!IsInitialized) + { + throw new InvalidOperationException("This instance is not initialized."); + } + + string? failureSource = null; + string? failureReason = null; + string? failureSummary = null; + + if (fault != null) + { + failureSource = fault.Source; + failureReason = fault.GetType().Name; + failureSummary = _exceptionSummarizer.Summarize(fault).ToString(); + } + + var requestMetadata = GetRequestMetadata(context); + + _histogram.Record( + executionTimeInMs, + _pipelineId!.PipelineName, + _pipelineId!.PipelineKey.GetDimensionOrUnknown(), + _pipelineId!.ResultType.GetDimensionOrUnknown(), + failureSource.GetDimensionOrUnknown(), + failureReason.GetDimensionOrUnknown(), + failureSummary.GetDimensionOrUnknown(), + requestMetadata.DependencyName, + requestMetadata.RequestName); + } + + private RequestMetadata GetRequestMetadata(Context context) + { + if (context.TryGetValue(TelemetryConstants.RequestMetadataKey, out var val) && val is RequestMetadata requestMetadata) + { + return requestMetadata; + } + + return _outgoingRequestContext?.RequestMetadata ?? _fallbackMetadata; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PipelineTelemetry.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PipelineTelemetry.cs new file mode 100644 index 0000000000..f6842a06e2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PipelineTelemetry.cs @@ -0,0 +1,119 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Resilience.Internal; +using Polly; + +namespace Microsoft.Extensions.Resilience; + +/// +/// The helper for pipeline telemetry. +/// +internal sealed class PipelineTelemetry +{ + public static IAsyncPolicy Create( + PipelineId pipelineId, + IAsyncPolicy policy, + IPipelineMetering metering, + ILogger logger, + TimeProvider timeProvider) => new TelemetryPolicy(pipelineId, policy, metering, logger, timeProvider); + + public static IAsyncPolicy Create( + PipelineId pipelineId, + IAsyncPolicy policy, + IPipelineMetering metering, + ILogger logger, + TimeProvider timeProvider) => new TelemetryPolicy(pipelineId, policy, metering, logger, timeProvider); + + internal sealed class TelemetryPolicy : AsyncPolicy + { + private readonly PipelineId _pipelineId; + private readonly IAsyncPolicy _policy; + private readonly IPipelineMetering _metering; + private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; + + public TelemetryPolicy(PipelineId pipelineId, IAsyncPolicy policy, IPipelineMetering metering, ILogger logger, TimeProvider timeProvider) + { + _pipelineId = pipelineId; + _policy = policy; + _metering = metering; + _logger = logger; + _timeProvider = timeProvider; + } + + protected override async Task ImplementationAsync(Func> action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) + { + var start = _timeProvider.GetTimestamp(); + + try + { + _logger.ExecutingPipeline(_pipelineId.PipelineName, _pipelineId.PipelineKey.GetDimensionOrUnknown()); + + var result = await _policy.ExecuteAsync(action, context, cancellationToken, continueOnCapturedContext).ConfigureAwait(false); + + _metering.RecordPipelineExecution(GetElapsedTime(start, _timeProvider), null, context); + + _logger.PipelineExecuted(_pipelineId.PipelineName, _pipelineId.PipelineKey.GetDimensionOrUnknown(), GetElapsedTime(start, _timeProvider)); + + return result; + } + catch (Exception e) + { + _metering.RecordPipelineExecution(GetElapsedTime(start, _timeProvider), e, context); + _logger.PipelineFailed(e, _pipelineId.PipelineName, _pipelineId.PipelineKey.GetDimensionOrUnknown(), GetElapsedTime(start, _timeProvider)); + + throw; + } + } + } + + internal static long GetElapsedTime(long startingTimestamp, TimeProvider timeProvider) => (long)timeProvider.GetElapsedTime(startingTimestamp, timeProvider.GetTimestamp()).TotalMilliseconds; + + internal sealed class TelemetryPolicy : AsyncPolicy + { + private readonly PipelineId _pipelineId; + private readonly IAsyncPolicy _policy; + private readonly IPipelineMetering _metering; + private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; + + public TelemetryPolicy(PipelineId pipelineId, IAsyncPolicy policy, IPipelineMetering metering, ILogger logger, TimeProvider timeProvider) + { + _pipelineId = pipelineId; + _policy = policy; + _metering = metering; + _logger = logger; + _timeProvider = timeProvider; + } + + protected override async Task ImplementationAsync(Func> action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) + { + var start = _timeProvider.GetTimestamp(); + + try + { + _logger.ExecutingPipeline(_pipelineId.PipelineName, _pipelineId.PipelineKey.GetDimensionOrUnknown()); + + var result = await _policy.ExecuteAsync(action, context, cancellationToken, continueOnCapturedContext).ConfigureAwait(false); + + _metering.RecordPipelineExecution(GetElapsedTime(start, _timeProvider), null, context); + + _logger.PipelineExecuted(_pipelineId.PipelineName, _pipelineId.PipelineKey.GetDimensionOrUnknown(), GetElapsedTime(start, _timeProvider)); + + return result; + } + catch (Exception e) + { + _metering.RecordPipelineExecution(GetElapsedTime(start, _timeProvider), e, context); + _logger.PipelineFailed(e, _pipelineId.PipelineName, _pipelineId.PipelineKey.GetDimensionOrUnknown(), GetElapsedTime(start, _timeProvider)); + + throw; + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PolicyPipelineBuilder.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PolicyPipelineBuilder.cs new file mode 100644 index 0000000000..65f4ae81ee --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PolicyPipelineBuilder.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience.Internal; + +/// +/// Builder instance which chains policies returning a policy wrap to be registered under unique key. +/// +/// +internal sealed class PolicyPipelineBuilder : IPolicyPipelineBuilder +{ + private readonly IPolicyFactory _policyFactory; + private readonly IPipelineMetering _metering; + private readonly ILogger _logger; + private readonly TimeProvider _timeProvider = TimeProvider.System; + private PipelineId? _pipelineId; + + internal List Policies { get; } = new(); + + public PolicyPipelineBuilder(IPolicyFactory policyFactory, IPipelineMetering metering, ILogger logger) + { + _policyFactory = policyFactory; + _metering = metering; + _logger = logger; + } + + public void Initialize(PipelineId pipelineId) + { + _pipelineId = pipelineId; + _policyFactory.Initialize(pipelineId); + _metering.Initialize(pipelineId); + } + + /// + public IPolicyPipelineBuilder AddCircuitBreakerPolicy( + string policyName, + CircuitBreakerPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(options); + + return AddPolicy(_policyFactory.CreateCircuitBreakerPolicy(policyName, options)); + } + + /// + public IPolicyPipelineBuilder AddRetryPolicy(string policyName, RetryPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(options); + + return AddPolicy(_policyFactory.CreateRetryPolicy(policyName, options)); + } + + /// + public IPolicyPipelineBuilder AddTimeoutPolicy(string policyName, TimeoutPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(options); + + return AddPolicy(_policyFactory.CreateTimeoutPolicy(policyName, options)); + } + + /// + public IPolicyPipelineBuilder AddBulkheadPolicy(string policyName, BulkheadPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(options); + + return AddPolicy(_policyFactory.CreateBulkheadPolicy(policyName, options)); + } + + /// + public IPolicyPipelineBuilder AddFallbackPolicy( + string policyName, + FallbackScenarioTaskProvider provider, + FallbackPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(options); + _ = Throw.IfNull(provider); + + return AddPolicy(_policyFactory.CreateFallbackPolicy(policyName, provider, options)); + } + + /// + public IPolicyPipelineBuilder AddHedgingPolicy( + string policyName, + HedgedTaskProvider provider, + HedgingPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(options); + _ = Throw.IfNull(provider); + + return AddPolicy(_policyFactory.CreateHedgingPolicy(policyName, provider, options)); + } + + public IPolicyPipelineBuilder AddPolicy(IAsyncPolicy policy) + { + Policies.Add(policy); + + return this; + } + + /// + /// At least one policy must be configured. + public IAsyncPolicy Build() + { + if (Policies.Count == 0) + { + Throw.InvalidOperationException("At least one policy must be configured."); + } + + var policy = Policies.Count > 1 ? + new AsyncPolicyPipeline(Policies) : + Policies[0]; + + if (_pipelineId != null) + { + policy = PipelineTelemetry.Create(_pipelineId, policy, _metering, _logger, _timeProvider).WithPolicyKey(_pipelineId.PolicyPipelineKey); + } + + return policy; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PolicyPipelineBuilderT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PolicyPipelineBuilderT.cs new file mode 100644 index 0000000000..a0ffc83aeb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/PolicyPipelineBuilderT.cs @@ -0,0 +1,142 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience.Internal; + +#pragma warning disable SA1649 // File name should match first type name + +/// +/// Builder instance which chains policies returning a policy wrap to be registered under unique key. +/// +/// The type of the result returned by the action executed by the policies. +/// +internal sealed class PolicyPipelineBuilder : IPolicyPipelineBuilder +{ + private readonly IPolicyFactory _policyFactory; + private readonly IPipelineMetering _metering; + private readonly ILogger _logger; + private readonly TimeProvider _timeProvider = TimeProvider.System; + private PipelineId? _pipelineId; + + internal List> Policies { get; } = new(); + + public PolicyPipelineBuilder(IPolicyFactory policyFactory, IPipelineMetering metering, ILogger logger) + { + _policyFactory = policyFactory; + _metering = metering; + _logger = logger; + } + + public void Initialize(PipelineId pipelineId) + { + _pipelineId = pipelineId; + _policyFactory.Initialize(pipelineId); + _metering.Initialize(pipelineId); + } + + /// + public IPolicyPipelineBuilder AddCircuitBreakerPolicy( + string policyName, + CircuitBreakerPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(options); + + return AddPolicy(_policyFactory.CreateCircuitBreakerPolicy(policyName, options)); + } + + /// + public IPolicyPipelineBuilder AddRetryPolicy(string policyName, RetryPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(options); + + return AddPolicy(_policyFactory.CreateRetryPolicy(policyName, options)); + } + + /// + public IPolicyPipelineBuilder AddTimeoutPolicy(string policyName, TimeoutPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(options); + + return AddPolicy(_policyFactory.CreateTimeoutPolicy(policyName, options).AsAsyncPolicy()); + } + + /// + public IPolicyPipelineBuilder AddBulkheadPolicy(string policyName, BulkheadPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(options); + + return AddPolicy(_policyFactory.CreateBulkheadPolicy(policyName, options).AsAsyncPolicy()); + } + + /// + public IPolicyPipelineBuilder AddFallbackPolicy( + string policyName, + FallbackScenarioTaskProvider provider, + FallbackPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(options); + _ = Throw.IfNull(provider); + + return AddPolicy(_policyFactory.CreateFallbackPolicy(policyName, provider, options)); + } + + /// + public IPolicyPipelineBuilder AddHedgingPolicy( + string policyName, + HedgedTaskProvider provider, + HedgingPolicyOptions options) + { + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(options); + _ = Throw.IfNull(provider); + + return AddPolicy(_policyFactory.CreateHedgingPolicy(policyName, provider, options)); + } + + public IPolicyPipelineBuilder AddPolicy(IAsyncPolicy policy) + { + Policies.Add(policy); + + return this; + } + + public IPolicyPipelineBuilder AddPolicy(IAsyncPolicy policy) + { + Policies.Add(policy.AsAsyncPolicy()); + + return this; + } + + /// + /// At least one policy must be configured. + public IAsyncPolicy Build() + { + if (Policies.Count == 0) + { + Throw.InvalidOperationException("At least one policy must be configured."); + } + + var policy = Policies.Count > 1 ? + new AsyncPolicyPipeline(Policies) : + Policies[0]; + + if (_pipelineId != null) + { + policy = PipelineTelemetry.Create(_pipelineId, policy, _metering, _logger, _timeProvider).WithPolicyKey(_pipelineId.PolicyPipelineKey); + } + + return policy; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineBuilder.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineBuilder.cs new file mode 100644 index 0000000000..4316f6dd63 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineBuilder.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience.Internal; + +internal sealed class ResiliencePipelineBuilder : IResiliencePipelineBuilder +{ + internal ResiliencePipelineBuilder(IServiceCollection services, string pipelineName) + { + _ = Throw.IfNull(services); + _ = Throw.IfNullOrEmpty(pipelineName); + + Services = services; + PipelineName = pipelineName; + } + + public string PipelineName { get; } + + public IServiceCollection Services { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineBuilderExtensions.cs new file mode 100644 index 0000000000..a7e5103102 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineBuilderExtensions.cs @@ -0,0 +1,154 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience.Internal; + +/// +/// Pub-internal extension methods for the . +/// +/// Do not use this class directly, it's reserved for internal use and can change at any time. +[Experimental] +internal static class ResiliencePipelineBuilderExtensions +{ + /// + /// Adds a supported policy to a pipeline using the specified options. + /// + /// The type of the result returned by the action executed by the policies. + /// The type of policy options. + /// Validator that validates . + /// The policy pipeline builder. + /// The policy type. + /// The policy name that will be included in the options name. + /// The configure options delegate. + /// The configure pipeline delegate. + /// Current instance. + public static IResiliencePipelineBuilder AddPolicy( + this IResiliencePipelineBuilder builder, + SupportedPolicies policyType, + string policyName, + Action> configureOptions, + Action, TOptions> configurePipeline) + where TOptions : class, new() + where TOptionsValidator : class, IValidateOptions + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(configureOptions); + _ = Throw.IfNull(configurePipeline); + + return builder.AddPolicy( + policyType.GetPolicyOptionsName(builder.PipelineName, policyName), + configureOptions, + (b, o, _) => configurePipeline(b, o)); + } + + /// + /// Adds a supported policy to a pipeline using the specified options. + /// + /// The type of the result returned by the action executed by the policies. + /// The type of policy options. + /// Validator that validates . + /// The policy pipeline builder. + /// The options name. + /// The configure options delegate. + /// The configure pipeline delegate. + /// Current instance. + public static IResiliencePipelineBuilder AddPolicy( + this IResiliencePipelineBuilder builder, + string optionsName, + Action> configureOptions, + Action, TOptions, IServiceProvider> configurePipeline) + where TOptions : class, new() + where TOptionsValidator : class, IValidateOptions + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(optionsName); + _ = Throw.IfNull(configureOptions); + _ = Throw.IfNull(configurePipeline); + + var services = builder.Services; + var optionsBuilder = services.AddValidatedOptions(optionsName); + + configureOptions(optionsBuilder); + return builder.AddDynamicPolicy(optionsName, (policyBuilder, serviceProvider) => + { + var optionsListenersHandler = serviceProvider.GetRequiredService(); + var optionsMonitor = serviceProvider.GetRequiredService>(); + var options = optionsMonitor.Get(optionsName); + + _ = optionsListenersHandler.TryCaptureOnChange(optionsName); + configurePipeline(policyBuilder, options, serviceProvider); + }); + } + + /// + /// Adds a policy. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The action that configures the pipeline builder instance. + /// Current instance. + public static IResiliencePipelineBuilder AddPolicy( + this IResiliencePipelineBuilder builder, + Action, IServiceProvider> configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + _ = builder.Services + .AddOptions>(builder.PipelineName) + .Configure((options, serviceProvider) => + { + options.BuilderActions.Add((builder) => configure(builder, serviceProvider)); + }); + + return builder; + } + + /// + /// Adds a policy. + /// + /// The type of the result returned by the action executed by the policies. + /// The type of policy options. + /// The policy pipeline builder. + /// The name of the options of the individual policy added. + /// The action that configures the pipeline builder instance. + /// Current instance. + private static IResiliencePipelineBuilder AddDynamicPolicy( + this IResiliencePipelineBuilder builder, + string policyOptionsName, + Action, IServiceProvider> configure) + where TOptions : class, new() + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + _ = builder.Services + .AddOptions>(builder.PipelineName) + .Configure((options, sp) => + { + var source = sp.GetServices>() + .FirstOrDefault(source => source.Name == policyOptionsName); + + if (source != null) + { + options.ChangeTokenSources.Add(() => source.GetChangeToken()); + } + }); + + return builder.AddPolicy(configure); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineFactory.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineFactory.cs new file mode 100644 index 0000000000..4ab5431ea1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineFactory.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience; + +internal sealed class ResiliencePipelineFactory : IResiliencePipelineFactory +{ + private readonly IServiceProvider _serviceProvider; + + public ResiliencePipelineFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public IAsyncPolicy CreatePipeline(string pipelineName, string pipelineKey = "") + { + _ = Throw.IfNullOrEmpty(pipelineName); + + // these options are automatically validated on access + var optionsMonitor = _serviceProvider.GetRequiredService>>(); + var logger = _serviceProvider.GetRequiredService>>(); + + return new AsyncDynamicPipeline( + pipelineName, + optionsMonitor, + (options) => + { + // The IPolicyPipelineBuilder is transient service, so we always create a new instance + var builder = _serviceProvider.GetRequiredService>(); + + builder.Initialize(PipelineId.Create(pipelineName, pipelineKey)); + + foreach (var action in options.BuilderActions) + { + action(builder); + } + + return builder.Build(); + }, + logger); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineFactoryOptions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineFactoryOptions.cs new file mode 100644 index 0000000000..87f6999d9e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineFactoryOptions.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Extensions.Resilience.Internal; + +/// +/// Encapsulates options for . +/// +/// The type of the result of action executed in pipeline. +/// Scope of an instance of this class is limited per pipeline and configured using pipeline name. +internal sealed class ResiliencePipelineFactoryOptions +{ + /// + /// Gets the actions used to configure . + /// + [Required] + [Microsoft.Shared.Data.Validation.Length(1, ErrorMessage = "This resilience pipeline is not configured. Each resilience pipeline must include at least one policy. Field path: {0}")] +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + public List>> BuilderActions { get; } = new(); +#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineFactoryOptionsValidatorT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineFactoryOptionsValidatorT.cs new file mode 100644 index 0000000000..12b23fc858 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineFactoryOptionsValidatorT.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Resilience.Internal; + +[OptionsValidator] +internal sealed partial class ResiliencePipelineFactoryOptionsValidator : IValidateOptions> +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineFactoryTokenSourceOptions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineFactoryTokenSourceOptions.cs new file mode 100644 index 0000000000..c514325ee2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineFactoryTokenSourceOptions.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.Resilience.Internal; +internal sealed class ResiliencePipelineFactoryTokenSourceOptions +{ + [Required] + public List> ChangeTokenSources { get; } = new(); +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineProvider.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineProvider.cs new file mode 100644 index 0000000000..307a9133f6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/ResiliencePipelineProvider.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Resilience.Internal; + +internal sealed class ResiliencePipelineProvider : IResiliencePipelineProvider, IDisposable +{ + private readonly IResiliencePipelineFactory _factory; + private readonly ConcurrentDictionary<(Type resultType, string pipelineName, string pipelineKey), IsPolicy> _policies = new(); + + public ResiliencePipelineProvider(IResiliencePipelineFactory factory) + { + _factory = factory; + } + + public IAsyncPolicy GetPipeline(string pipelineName) + { + _ = Throw.IfNullOrEmpty(pipelineName); + + // Stryker disable once all + var key = (typeof(TResult), pipelineName, string.Empty); + + return (IAsyncPolicy)_policies.GetOrAdd(key, static (key, factory) => factory.CreatePipeline(key.pipelineName, string.Empty), _factory); + } + + public IAsyncPolicy GetPipeline(string pipelineName, string pipelineKey) + { + _ = Throw.IfNullOrEmpty(pipelineName); + _ = Throw.IfNullOrEmpty(pipelineKey); + + var key = (typeof(TResult), pipelineName, pipelineKey); + + return (IAsyncPolicy)_policies.GetOrAdd(key, static (key, factory) => factory.CreatePipeline(key.pipelineName, key.pipelineKey), _factory); + } + + /// + /// Removes all change registration subscriptions. + /// + public void Dispose() + { + foreach (var policyByKey in _policies) + { + if (policyByKey.Value is IDisposable disposable) + { + disposable.Dispose(); + } + } + + _policies.Clear(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/SupportedPolicies.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/SupportedPolicies.cs new file mode 100644 index 0000000000..b9a33d98a6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/SupportedPolicies.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Resilience.Internal; + +/// +/// Represents the policies supported by R9. +/// +internal enum SupportedPolicies +{ + /// + /// The circuit breaker policy type. + /// + CircuitBreaker = 0, + + /// + /// The retry policy type. + /// + RetryPolicy = 1, + + /// + /// The timeout policy type. + /// + TimeoutPolicy = 2, + + /// + /// The fallback policy type. + /// + FallbackPolicy = 3, + + /// + /// The bulkhead policy type. + /// + BulkheadPolicy = 4, + + /// + /// The hedging policy type. + /// + HedgingPolicy = 5 +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/TelemetryHelper.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/TelemetryHelper.cs new file mode 100644 index 0000000000..f16d1fb574 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/Internal/TelemetryHelper.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Http.Telemetry; + +namespace Microsoft.Extensions.Resilience; + +internal static class TelemetryHelper +{ + internal static string GetDimensionOrUnknown(this string? dimension) + { + return string.IsNullOrEmpty(dimension) ? TelemetryConstants.Unknown : dimension!; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.BulkheadT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.BulkheadT.cs new file mode 100644 index 0000000000..2b8f2127d6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.BulkheadT.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience; + +public static partial class ResiliencePipelineBuilderExtensions +{ + /// + /// Adds a bulkhead policy with default options to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// Current instance. + public static IResiliencePipelineBuilder AddBulkheadPolicy( + this IResiliencePipelineBuilder builder, + string policyName) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + + return builder.AddBulkheadPolicyInternal(policyName, null, null); + } + + /// + /// Adds a bulkhead policy to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The action that configures the default policy options. + /// Current instance. + public static IResiliencePipelineBuilder AddBulkheadPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(configure); + + return builder.AddBulkheadPolicyInternal(policyName, null, configure); + } + + /// + /// Adds a bulkhead policy to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The configuration that the options will bind against. + /// Current instance. + public static IResiliencePipelineBuilder AddBulkheadPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(section); + + return builder.AddBulkheadPolicyInternal(policyName, section, null); + } + + /// + /// Adds a bulkhead policy to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The configuration that the options will bind against. + /// The action that configures the policy options after is applied. + /// Current instance. + /// + /// Keep in mind that the delegate will override anything that was configured using . + /// + public static IResiliencePipelineBuilder AddBulkheadPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + IConfigurationSection section, + Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(section); + _ = Throw.IfNull(configure); + + return builder.AddBulkheadPolicyInternal(policyName, section, configure); + } + + private static IResiliencePipelineBuilder AddBulkheadPolicyInternal( + this IResiliencePipelineBuilder builder, + string policyName, + IConfigurationSection? section, + Action? configure) + { + return builder.AddPolicy( + SupportedPolicies.BulkheadPolicy, + policyName, + options => options.Configure(section, configure), + (builder, options) => builder.AddBulkheadPolicy(policyName, options)); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.CircuitBreakerT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.CircuitBreakerT.cs new file mode 100644 index 0000000000..4e1ee5212a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.CircuitBreakerT.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience; + +public static partial class ResiliencePipelineBuilderExtensions +{ + /// + /// Adds a circuit breaker policy with default options to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// Current instance. + public static IResiliencePipelineBuilder AddCircuitBreakerPolicy( + this IResiliencePipelineBuilder builder, + string policyName) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + + return builder.AddCircuitBreakerPolicyInternal(policyName, null, null); + } + + /// + /// Adds a circuit breaker policy to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The action that configures the default policy options. + /// Current instance. + public static IResiliencePipelineBuilder AddCircuitBreakerPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + Action> configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(configure); + + return builder.AddCircuitBreakerPolicyInternal(policyName, null, configure); + } + + /// + /// Adds a circuit breaker policy to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The configuration that the options will bind against. + /// Current instance. + public static IResiliencePipelineBuilder AddCircuitBreakerPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(section); + + return builder.AddCircuitBreakerPolicyInternal(policyName, section, null); + } + + /// + /// Adds a circuit breaker policy to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The configuration that the options will bind against. + /// The action that configures the policy options after is applied. + /// Current instance. + /// + /// Keep in mind that the delegate will override anything that was configured using . + /// + public static IResiliencePipelineBuilder AddCircuitBreakerPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + IConfigurationSection section, + Action> configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(section); + _ = Throw.IfNull(configure); + + return builder.AddCircuitBreakerPolicyInternal(policyName, section, configure); + } + + private static IResiliencePipelineBuilder AddCircuitBreakerPolicyInternal( + this IResiliencePipelineBuilder builder, + string policyName, + IConfigurationSection? section, + Action>? configure) + { + return builder.AddPolicy, CircuitBreakerPolicyOptionsValidator>( + SupportedPolicies.CircuitBreaker, + policyName, + options => options.Configure(section, configure), + (builder, options) => builder.AddCircuitBreakerPolicy(policyName, options)); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.FallbackT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.FallbackT.cs new file mode 100644 index 0000000000..8d77619b7d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.FallbackT.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience; + +public static partial class ResiliencePipelineBuilderExtensions +{ + /// + /// Adds a fallback policy with default options to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The task performed in the fallback scenario when the initial execution encounters a transient failure. + /// Current instance. + public static IResiliencePipelineBuilder AddFallbackPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + FallbackScenarioTaskProvider provider) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(provider); + + return builder.AddFallbackPolicyInternal(policyName, provider, null, null); + } + + /// + /// Adds a fallback policy to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The task performed in the fallback scenario when the initial execution encounters a transient failure. + /// The action that configures the default policy options. + /// Current instance. + public static IResiliencePipelineBuilder AddFallbackPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + FallbackScenarioTaskProvider provider, + Action> configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(provider); + _ = Throw.IfNull(configure); + + return builder.AddFallbackPolicyInternal(policyName, provider, null, configure); + } + + /// + /// Adds a fallback policy to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The task performed in the fallback scenario when the initial execution encounters a transient failure. + /// The configuration that the options will bind against. + /// Current instance. + public static IResiliencePipelineBuilder AddFallbackPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + FallbackScenarioTaskProvider provider, + IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(provider); + _ = Throw.IfNull(section); + + return builder.AddFallbackPolicyInternal(policyName, provider, section, null); + } + + /// + /// Adds a fallback policy to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The task performed in the fallback scenario when the initial execution encounters a transient failure. + /// The configuration that the options will bind against. + /// The action that configures the policy options after is applied. + /// Current instance. + /// + /// Keep in mind that the delegate will override anything that was configured using . + /// + public static IResiliencePipelineBuilder AddFallbackPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + FallbackScenarioTaskProvider provider, + IConfigurationSection section, + Action> configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(provider); + _ = Throw.IfNull(section); + _ = Throw.IfNull(configure); + + return builder.AddFallbackPolicyInternal(policyName, provider, section, configure); + } + + private static IResiliencePipelineBuilder AddFallbackPolicyInternal( + this IResiliencePipelineBuilder builder, + string policyName, + FallbackScenarioTaskProvider provider, + IConfigurationSection? section, + Action>? configure) + { + return builder.AddPolicy, EmptyFallbackPolicyOptionsValidator>( + SupportedPolicies.FallbackPolicy, + policyName, + options => options.Configure(section, configure), + (builder, options) => builder.AddFallbackPolicy(policyName, provider, options)); + } + + private sealed class EmptyFallbackPolicyOptionsValidator : IValidateOptions> + { + public ValidateOptionsResult Validate(string? name, FallbackPolicyOptions options) => ValidateOptionsResult.Skip; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.HedgingT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.HedgingT.cs new file mode 100644 index 0000000000..d62e2e5d59 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.HedgingT.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience; + +public static partial class ResiliencePipelineBuilderExtensions +{ + /// + /// Adds a hedging policy with default options to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The hedged task provider. + /// Current instance. + public static IResiliencePipelineBuilder AddHedgingPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + HedgedTaskProvider provider) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(provider); + + return builder.AddHedgingPolicyInternal(policyName, provider, null, null); + } + + /// + /// Adds a hedging policy to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The hedged task provider. + /// The action that configures the default policy options. + /// Current instance. + public static IResiliencePipelineBuilder AddHedgingPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + HedgedTaskProvider provider, + Action> configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(configure); + _ = Throw.IfNull(provider); + + return builder.AddHedgingPolicyInternal(policyName, provider, null, configure); + } + + /// + /// Adds a hedging policy to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The hedged task provider. + /// The configuration that the options will bind against. + /// Current instance. + public static IResiliencePipelineBuilder AddHedgingPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + HedgedTaskProvider provider, + IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(provider); + _ = Throw.IfNull(section); + + return builder.AddHedgingPolicyInternal(policyName, provider, section, null); + } + + /// + /// Adds a hedging policy to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The hedged task provider. + /// The configuration that the options will bind against. + /// The action that configures the policy options after is applied. + /// Current instance. + /// + /// Keep in mind that the delegate will override anything that was configured using . + /// + public static IResiliencePipelineBuilder AddHedgingPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + HedgedTaskProvider provider, + IConfigurationSection section, + Action> configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(provider); + _ = Throw.IfNull(section); + _ = Throw.IfNull(configure); + + return builder.AddHedgingPolicyInternal(policyName, provider, section, configure); + } + + private static IResiliencePipelineBuilder AddHedgingPolicyInternal( + this IResiliencePipelineBuilder builder, + string policyName, + HedgedTaskProvider provider, + IConfigurationSection? section, + Action>? configure) + { + return builder.AddPolicy, HedgingPolicyOptionsValidator>( + SupportedPolicies.HedgingPolicy, + policyName, + options => options.Configure(section, configure), + (builder, options) => builder.AddHedgingPolicy(policyName, provider, options)); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.RetryT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.RetryT.cs new file mode 100644 index 0000000000..ded6e4d9d0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.RetryT.cs @@ -0,0 +1,112 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience; + +public static partial class ResiliencePipelineBuilderExtensions +{ + /// + /// Adds a retry policy with default options to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// Current instance. + public static IResiliencePipelineBuilder AddRetryPolicy( + this IResiliencePipelineBuilder builder, + string policyName) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + + return builder.AddRetryPolicyInternal(policyName, null, null); + } + + /// + /// Adds a retry policy to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The action that configures the default policy options. + /// Current instance. + public static IResiliencePipelineBuilder AddRetryPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + Action> configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(configure); + + return builder.AddRetryPolicyInternal(policyName, null, configure); + } + + /// + /// Adds a retry policy to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The configuration that the options will bind against. + /// Current instance. + public static IResiliencePipelineBuilder AddRetryPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(section); + + return builder.AddRetryPolicyInternal(policyName, section, null); + } + + /// + /// Adds a retry policy to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The configuration that the options will bind against. + /// The action that configures the policy options after is applied. + /// Current instance. + /// + /// Keep in mind that the delegate will override anything that was configured using . + /// + public static IResiliencePipelineBuilder AddRetryPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + IConfigurationSection section, + Action> configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(section); + _ = Throw.IfNull(configure); + + return builder.AddRetryPolicyInternal(policyName, section, configure); + } + + private static IResiliencePipelineBuilder AddRetryPolicyInternal( + this IResiliencePipelineBuilder builder, + string policyName, + IConfigurationSection? section, + Action>? configure) + { + _ = builder.Services.AddValidatedOptions>(SupportedPolicies.RetryPolicy.GetPolicyOptionsName(builder.PipelineName, policyName)); + + return builder.AddPolicy, RetryPolicyOptionsValidator>( + SupportedPolicies.RetryPolicy, + policyName, + options => options.Configure(section, configure), + (builder, options) => builder.AddRetryPolicy(policyName, options)); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.TimeoutT.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.TimeoutT.cs new file mode 100644 index 0000000000..a200312092 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResiliencePipelineBuilderExtensions.TimeoutT.cs @@ -0,0 +1,112 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience; + +/// +/// Extension methods for pipeline builders. +/// +public static partial class ResiliencePipelineBuilderExtensions +{ + /// + /// Adds a timeout policy with default options to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// Current instance. + public static IResiliencePipelineBuilder AddTimeoutPolicy( + this IResiliencePipelineBuilder builder, + string policyName) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + + return builder.AddTimeoutPolicyInternal(policyName, null, null); + } + + /// + /// Adds a timeout policy to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The action that configures the default policy options. + /// Current instance. + public static IResiliencePipelineBuilder AddTimeoutPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(configure); + + return builder.AddTimeoutPolicyInternal(policyName, null, configure); + } + + /// + /// Adds a timeout policy to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The configuration that the options will bind against. + /// Current instance. + public static IResiliencePipelineBuilder AddTimeoutPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(section); + + return builder.AddTimeoutPolicyInternal(policyName, section, null); + } + + /// + /// Adds a timeout policy to a pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The policy pipeline builder. + /// The policy name. + /// The configuration that the options will bind against. + /// The action that configures the policy options after is applied. + /// Current instance. + /// + /// Keep in mind that the delegate will override anything that was configured using . + /// + public static IResiliencePipelineBuilder AddTimeoutPolicy( + this IResiliencePipelineBuilder builder, + string policyName, + IConfigurationSection section, + Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(policyName); + _ = Throw.IfNull(section); + _ = Throw.IfNull(configure); + + return builder.AddTimeoutPolicyInternal(policyName, section, configure); + } + + private static IResiliencePipelineBuilder AddTimeoutPolicyInternal( + this IResiliencePipelineBuilder builder, + string policyName, + IConfigurationSection? section, + Action? configure) + { + return builder.AddPolicy( + SupportedPolicies.TimeoutPolicy, + policyName, + options => options.Configure(section, configure), + (pipelineBuilder, options) => pipelineBuilder.AddTimeoutPolicy(policyName, options)); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResilienceWrapperAttribute.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResilienceWrapperAttribute.cs new file mode 100644 index 0000000000..05e194c769 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ResilienceWrapperAttribute.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Resilience; + +/// +/// This attribute is used by the source generator +/// in order to generate code for a resilient cache. +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class ResilienceWrapperAttribute : Attribute +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..87722fde42 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Resilience/Resilience/ServiceCollectionExtensions.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Resilience; + +/// +/// Extension class for the Service Collection DI container. +/// +public static class ServiceCollectionExtensions +{ + /// + /// Returns a generic that is used to configure the new resilience pipeline. + /// + /// The type of the result returned by the action executed by the policies. + /// The DI container. + /// The pipeline name. + /// The input . + /// cannot be null. + public static IResiliencePipelineBuilder AddResiliencePipeline( + this IServiceCollection services, + string pipelineName) + { + _ = Throw.IfNull(services); + _ = Throw.IfNullOrEmpty(pipelineName); + + _ = PolicyFactoryServiceCollectionExtensions.AddPolicyFactory(services); + + _ = services.AddValidatedOptions, ResiliencePipelineFactoryOptionsValidator>(pipelineName); + _ = services.AddOptions>(pipelineName); + _ = services.AddSingleton>>(sp => + ActivatorUtilities.CreateInstance>(sp, pipelineName)); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddTransient, PolicyPipelineBuilder>(); + services.TryAddTransient(); + services.TryAddSingleton(); + + _ = services.RegisterMetering(); + + return new ResiliencePipelineBuilder(services, pipelineName); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/EnricherExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/EnricherExtensions.cs new file mode 100644 index 0000000000..e18123eb46 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/EnricherExtensions.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Enrichment; + +/// +/// Lets you register telemetry enrichers in a dependency injection container. +/// +public static class EnricherExtensions +{ + /// + /// Registers a log enricher type. + /// + /// The dependency injection container to add the enricher type to. + /// Enricher type. + /// The value of . + /// When is . + public static IServiceCollection AddLogEnricher(this IServiceCollection services) + where T : class, ILogEnricher + { + _ = Throw.IfNull(services); + + return services.AddSingleton(); + } + + /// + /// Registers a log enricher instance. + /// + /// The dependency injection container to add the enricher instance to. + /// The enricher instance to add. + /// The value of . + /// When or are . + public static IServiceCollection AddLogEnricher(this IServiceCollection services, ILogEnricher enricher) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(enricher); + + return services.AddSingleton(enricher); + } + + /// + /// Registers a metric enricher type. + /// + /// Enricher type. + /// The dependency injection container to add the enricher type to. + /// The value of . + /// When is . + public static IServiceCollection AddMetricEnricher(this IServiceCollection services) + where T : class, IMetricEnricher + { + _ = Throw.IfNull(services); + + return services.AddSingleton(); + } + + /// + /// Registers a metric enricher instance. + /// + /// The dependency injection container to add the enricher instance to. + /// The enricher instance to add. + /// The value of . + /// When or are . + public static IServiceCollection AddMetricEnricher(this IServiceCollection services, IMetricEnricher enricher) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(enricher); + + return services.AddSingleton(enricher); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/IEnrichmentPropertyBag.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/IEnrichmentPropertyBag.cs new file mode 100644 index 0000000000..c29b59883a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/IEnrichmentPropertyBag.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Telemetry.Enrichment; + +/// +/// Allows enrichers to report enrichment properties. +/// +public interface IEnrichmentPropertyBag +{ + /// + /// Add a property in form of a key/value pair to the bag of enrichment properties. + /// + /// Enrichment property key. + /// Enrichment property value. + /// When is an empty string. + /// + /// Either or is . + /// + /// + /// For log enrichment, is serialized as per the rules below: + /// + /// + /// Primitive types + /// recognized and efficiently serialized. + /// + /// + /// Arrays + /// recognized and serialized in a loop. + /// + /// + /// + /// recognized as IDictionary<string, object> and serialized in a loop. + /// + /// + /// + /// recognized and serialized after converting to . + /// + /// + /// All the rest + /// converted to as is and serialized. + /// + /// + /// For metric enrichment, is converted to format using method. + /// + void Add(string key, object value); + + /// + /// Add a property in form of a key/value pair to the bag of enrichment properties. + /// + /// Enrichment property key. + /// Enrichment property value. + /// When is an empty string. + /// + /// Either or is . + /// + void Add(string key, string value); + + /// + /// Adds a series of properties to the bag of enrichment properties. + /// + /// The properties to add. + /// Refer to the overload for a description of the serialization semantics. + void Add(ReadOnlySpan> properties); + + /// + /// Adds a series of properties to the bag of enrichment properties. + /// + /// The properties to add. + void Add(ReadOnlySpan> properties); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/ILogEnricher.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/ILogEnricher.cs new file mode 100644 index 0000000000..1ed89c24a8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/ILogEnricher.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Telemetry.Enrichment; + +/// +/// A component that augments log records with additional properties. +/// +public interface ILogEnricher +{ + /// + /// Called to generate properties for a log record. + /// + /// Where the enricher puts the properties it is producing. + void Enrich(IEnrichmentPropertyBag bag); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/IMetricEnricher.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/IMetricEnricher.cs new file mode 100644 index 0000000000..c2b25c5b10 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/IMetricEnricher.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Telemetry.Enrichment; + +/// +/// A component that augments metric state with additional properties. +/// +public interface IMetricEnricher +{ + /// + /// Called to generate properties for metrics. + /// + /// Where the enricher puts the properties it is producing. + void Enrich(IEnrichmentPropertyBag bag); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/ITraceEnricher.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/ITraceEnricher.cs new file mode 100644 index 0000000000..9bdc816f19 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/ITraceEnricher.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Enrichment; + +/// +/// A component that augments tracing state with additional tags. +/// +public interface ITraceEnricher +{ + /// + /// Called to let the component add tags to a tracing activity. + /// + /// The activity to add the tags to. + void Enrich(Activity activity); + + /// + /// Called to let the component add tags to the start event of a tracing activity. + /// + /// The activity to add the tags to. + void EnrichOnActivityStart(Activity activity); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/HttpRouteParameterRedactionMode.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/HttpRouteParameterRedactionMode.cs new file mode 100644 index 0000000000..218d29d1ca --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/HttpRouteParameterRedactionMode.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Http.Telemetry; + +/// +/// Strategy to decide how HTTP request path parameters are redacted. +/// +public enum HttpRouteParameterRedactionMode +{ + /// + /// All parameters are considered as sensitive and are required to be explicitly annotated with a data classification. + /// + /// + /// UNannotated parameters are always redacted with the erasing redactor. + /// + Strict, + + /// + /// All parameters are considered as non-sensitive and included as-is by default. + /// + /// + /// Only parameters explicitly annotaed with a data classification are redacted. + /// + Loose, + + /// + /// Route parameters are not redacted regardless of the presence of data classifcation annotations. + /// + None, +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/IDownstreamDependencyMetadata.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/IDownstreamDependencyMetadata.cs new file mode 100644 index 0000000000..22ba3b5d77 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/IDownstreamDependencyMetadata.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Extensions.Http.Telemetry; + +/// +/// Interface for passing dependency metadata. +/// +public interface IDownstreamDependencyMetadata +{ + /// + /// Gets the name of the dependent service. + /// + public string DependencyName { get; } + + /// + /// Gets the list of host name suffixes that can uniquely identify a host as this dependency. + /// + public ISet UniqueHostNameSuffixes { get; } + + /// + /// Gets the list of all metadata for all routes to the dependency service. + /// + public ISet RequestMetadata { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/IOutgoingRequestContext.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/IOutgoingRequestContext.cs new file mode 100644 index 0000000000..c9c2b77c0f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/IOutgoingRequestContext.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Http.Telemetry; + +/// +/// Interface that holds outgoing request metadata. +/// +public interface IOutgoingRequestContext +{ + /// + /// Gets or sets the metadata for outgoing requests. + /// + RequestMetadata? RequestMetadata { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/RequestMetadata.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/RequestMetadata.cs new file mode 100644 index 0000000000..0e9126320e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/RequestMetadata.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Http.Telemetry; + +/// +/// Holds request metadata for use by the telemetry system. +/// +public class RequestMetadata +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// This constructor initializes to GET, and all other properties to "unknown". + /// + public RequestMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Http method type of the request. + /// Route of the request. + /// Name of the request. + /// + /// The property is initialized to "unknown". + /// + /// When any of the parameters are . + public RequestMetadata(string methodType, string requestRoute, string requestName = TelemetryConstants.Unknown) + { + MethodType = Throw.IfNull(methodType); + RequestRoute = Throw.IfNull(requestRoute); + RequestName = Throw.IfNull(requestName); + } + + /// + /// Gets or sets request's route template. + /// + /// + /// Request Route is used for multiple use cases. + /// - For outgoing request metrics, it is used as the request name dimension (if RequestName is not provided). + /// - For Logs and traces, it is used to identify sensitive parameters from the path and redact them in the exported path, so sensitive data leakage can be avoided. + /// If you are using redaction the template should be accurate for the request else redaction won't be applied to sensitive parameters. + /// e.g. A template would look something like /v1/users/{userId}/chats/{chatId}/messages. The sensitive parameter names should match exactly as provided + /// in configuration for outgoing tracing and outgoing logging autocollectors for parameters to be redacted. + /// + public string RequestRoute { get; set; } = TelemetryConstants.Unknown; + + /// + /// Gets or sets name to be logged for the request. + /// + /// + /// RequestName is used in the following manner by outgoing http request auto collectors: + /// - For outgoing request metrics: RequestName is used as the request name dimension if present, if not provided RequestRoute value would be used instead. + /// - For outgoing request traces: It is used as the Display name for the activity i.e. When looking at the E2E trace flow this name is used in the Tree view of traces. + /// if not provided RequestRoute value would be used instead. + /// - For outgoing request logs: When present it would be added as an additional dimension to logs. + /// + public string RequestName { get; set; } = TelemetryConstants.Unknown; + + /// + /// Gets or sets name of the dependency to which the outgoing request is being made. + /// + /// + /// DependencyName is used in the following manner by outgoing http request auto collectors: + /// - For outgoing request metrics: This is added as dependency name dimension so metrics can be pivoted based on the dependency. + /// - For outgoing request traces and logs: This is added as dependency name dimension for better diagnosability. + /// + public string DependencyName { get; set; } = TelemetryConstants.Unknown; + + /// + /// Gets or sets the http method type of the request. + /// + /// + /// Supported types are GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, TRACE. + /// + public string MethodType { get; set; } = "GET"; +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/TelemetryConstants.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/TelemetryConstants.cs new file mode 100644 index 0000000000..7ccb595318 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Http/TelemetryConstants.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Http.Telemetry; + +/// +/// Common telemetry constants used by various telemetry libraries. +/// +public static class TelemetryConstants +{ + /// + /// Request metadata key that is used when storing request metadata object. + /// + public const string RequestMetadataKey = "R9-RequestMetadata"; + + /// + /// Placeholder string for unknown request name, dependency name etc. in telemetry. + /// + public const string Unknown = "unknown"; + + /// + /// Placeholder string used for redacted data where needed. + /// + public const string Redacted = "REDACTED"; + + /// + /// Header for client application name, sent on an outgoing http call. + /// + [Experimental] + public const string ClientApplicationNameHeader = "X-ClientApplication"; + + /// + /// Header for server application name, sent on a http request. + /// + [Experimental] + public const string ServerApplicationNameHeader = "X-ServerApplication"; +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Checkpoint.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Checkpoint.cs new file mode 100644 index 0000000000..a2a262224e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Checkpoint.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Telemetry.Latency; + +/// +/// Represents an event and the time it occurred relative to a well-known starting point. +/// +/// +/// Related checkpoints are used to capture when sequential points in time are reached in an +/// operation like request execution. They are measured relative to the start of an operation and +/// hence capture latency as well as operation flow. +/// +public readonly struct Checkpoint : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// Name of the checkpoint. + /// Elapsed time since start. + /// Frequency of the elapsed time. + public Checkpoint(string name, long elapsed, long frequency) + { + Name = name; + Elapsed = elapsed; + Frequency = frequency; + } + + /// + /// Gets the name of the checkpoint. + /// + public string Name { get; } + + /// + /// Gets the relative time since the begining of the associated operation at which the checkpoint was created. + /// + public long Elapsed { get; } + + /// + /// Gets the frequency of the timestamp value. + /// + public long Frequency { get; } + + /// + /// Determines whether this and a specified object are identical. + /// + /// The object to compare. + /// if identical; otherwise. + public override bool Equals(object? obj) => obj is Checkpoint m && Equals(m); + + /// + /// Determines whether this and a specified checkpoint are identical. + /// + /// The other checkpoint. + /// if identical; otherwise. + public bool Equals(Checkpoint other) + => Elapsed == other.Elapsed && Frequency == other.Frequency && Name.Equals(other.Name, StringComparison.Ordinal); + + /// + /// Gets a hash code for this object. + /// + /// A hash code for the current object. + public override int GetHashCode() + => HashCode.Combine(Name, Elapsed, Frequency); + + /// + /// Equality operator. + /// + /// First value. + /// Second value. + /// if its operands are equal, otherwise. + public static bool operator ==(Checkpoint left, Checkpoint right) + { + return left.Equals(right); + } + + /// + /// Inequality operator. + /// + /// First value. + /// Second value. + /// if its operands are inequal, otherwise. + public static bool operator !=(Checkpoint left, Checkpoint right) + { + return !(left == right); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/ILatencyContext.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/ILatencyContext.cs new file mode 100644 index 0000000000..a51141d681 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/ILatencyContext.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Telemetry.Latency; + +/// +/// Abstraction that provides the context for latency measurement and diagnostics. +/// +/// +/// The context ties in latency signals such as checkpoints and measures for a scope along with +/// mechanisms such as tags that allow describing the scope. For example, a context lets you record +/// tags, checkpoints and measures within the scope of a web request. +/// +public interface ILatencyContext : IDisposable +{ + /// + /// Adds a tag to the context. + /// + /// Tag token. + /// Value of the tag. + /// + /// Tags are used to provide metadata to the context. These are pivots that are useful to + /// slice and dice the data for analysis. Examples include API, Client, UserType etc. + /// Setting a tag with same name overrides its prior value i.e., last call wins. + /// + /// When is . + void SetTag(TagToken token, string value); + + /// + /// Adds a checkpoint to the context. + /// + /// Checkpoint token. + /// + /// A checkpoint can be added only once per context. Use checkpoints for + /// code that is non-reentrant per context. + /// + void AddCheckpoint(CheckpointToken token); + + /// + /// Adds to a measure. + /// + /// Measure token. + /// Value to add. + /// + /// Adds the value to a measure. Measures are used for tracking total latency + /// or the count for repeating operations. Example: Latency for all database calls, + /// number of calls to an external service, etc. + void AddMeasure(MeasureToken token, long value); + + /// + /// Sets a measure to an absolute value. + /// + /// Measure token. + /// Value to set. + void RecordMeasure(MeasureToken token, long value); + + /// + /// Stops the latency measurement. + /// + /// This prevents any state change to the context. + void Freeze(); + + /// + /// Gets the accumulated latency data. + /// + LatencyData LatencyData { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/ILatencyContextProvider.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/ILatencyContextProvider.cs new file mode 100644 index 0000000000..0aa6d5125c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/ILatencyContextProvider.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Telemetry.Latency; + +/// +/// A factory of latency contextts. +/// +public interface ILatencyContextProvider +{ + /// + /// Creates a new . + /// + /// A new latency context. + ILatencyContext CreateContext(); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/ILatencyDataExporter.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/ILatencyDataExporter.cs new file mode 100644 index 0000000000..0bb1d8bcad --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/ILatencyDataExporter.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Telemetry.Latency; + +/// +/// Abstraction that is used to export latency data. +/// +/// This is called when latency context is frozen to export the context's data. +public interface ILatencyDataExporter +{ + /// + /// Function called to export latency data. + /// + /// A latency context's latency data. + /// Cancellation token. + /// A that represents the export operation. + Task ExportAsync(LatencyData data, CancellationToken cancellationToken); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/LatencyData.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/LatencyData.cs new file mode 100644 index 0000000000..68a253cedc --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/LatencyData.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Telemetry.Latency; + +/// +/// Encapsulates the state accumulated while measuring the latency of an operaiton. +/// +[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Comparing instances is not an expected scenario")] +public readonly struct LatencyData +{ + private readonly ArraySegment _tags; + private readonly ArraySegment _checkpoints; + private readonly ArraySegment _measures; + + /// + /// Initializes a new instance of the struct. + /// + /// List of tags. + /// List of checkpoints. + /// List of measures. + /// Total duration of the operation that is represented by this data. + /// Frequency of the duration timestamp. + public LatencyData(ArraySegment tags, ArraySegment checkpoints, ArraySegment measures, long durationTimestamp, long durationTimestampFrequency) + { + _tags = tags; + _checkpoints = checkpoints; + _measures = measures; + DurationTimestamp = durationTimestamp; + DurationTimestampFrequency = durationTimestampFrequency; + } + + /// + /// Gets the list of checkpoints added while measuring the operation's latency. + /// + public ReadOnlySpan Checkpoints => _checkpoints; + + /// + /// Gets the list of tags added to provide metadata about the operation being measured. + /// + public ReadOnlySpan Tags => _tags; + + /// + /// Gets the list of measures added. + /// + public ReadOnlySpan Measures => _measures; + + /// + /// Gets the total time measured by the latency context. + /// + public long DurationTimestamp { get; } + + /// + /// Gets the frequency of the duration timestamp. + /// + public long DurationTimestampFrequency { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Measure.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Measure.cs new file mode 100644 index 0000000000..d2ac70be9d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Measure.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Telemetry.Latency; + +/// +/// Represents a measure. +/// +/// +/// Measures are used to aggregate or record values. They are used to track +/// statistics about recurring operations. Example: number of calls to +/// a database, total latency of database calls etc. +/// +public readonly struct Measure : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// Name of the counter. + /// Value of the counter. + public Measure(string name, long value) + { + Name = name; + Value = value; + } + + /// + /// Gets the name of the measure. + /// + public string Name { get; } + + /// + /// Gets the value of the measure. + /// + public long Value { get; } + + /// + /// Determines whether this and a specified object are identical. + /// + /// /// The object to compare. + /// if identical; otherwise. + public override bool Equals(object? obj) => obj is Measure m && Equals(m); + + /// + /// Determines whether this and a specified measure are identical. + /// + /// The other measure. + /// if identical; otherwise. + public bool Equals(Measure other) => Value == other.Value && Name.Equals(other.Name, StringComparison.Ordinal); + + /// + /// Gets a hash code for this object. + /// + /// A hash code for the current object. + public override int GetHashCode() => HashCode.Combine(Name, Value); + + /// + /// Equality operator. + /// + /// First value. + /// Second value. + /// if its operands are equal, otherwise. + public static bool operator ==(Measure left, Measure right) + { + return left.Equals(right); + } + + /// + /// Inequality operator. + /// + /// First value. + /// Second value. + /// if its operands are inequal, otherwise. + public static bool operator !=(Measure left, Measure right) + { + return !(left == right); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/NullLatencyContext.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/NullLatencyContext.cs new file mode 100644 index 0000000000..ec8a8d986c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/NullLatencyContext.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Telemetry.Latency; + +/// +/// No-op implementation of a latency context. +/// +internal sealed class NullLatencyContext : ILatencyContext, ILatencyContextProvider, ILatencyContextTokenIssuer +{ + private readonly ArraySegment _checkpoints = new(Array.Empty()); + private readonly ArraySegment _tags = new(Array.Empty()); + private readonly ArraySegment _measures = new(Array.Empty()); + + public LatencyData LatencyData => new(_tags, _checkpoints, _measures, 0, TimeSpan.TicksPerSecond); + + public void Freeze() + { + // Nothing to do on Stop. + } + + public ILatencyContext CreateContext() => this; + + public void Dispose() + { + // Method intentionally left empty. + } + + public void SetTag(TagToken token, string value) + { + // Method intentionally left empty. + } + + public void AddCheckpoint(CheckpointToken token) + { + // Method intentionally left empty. + } + + public void AddMeasure(MeasureToken name, long value) + { + // Method intentionally left empty. + } + + public void RecordMeasure(MeasureToken name, long value) + { + // Method intentionally left empty. + } + + public TagToken GetTagToken(string name) => default; + public CheckpointToken GetCheckpointToken(string name) => default; + public MeasureToken GetMeasureToken(string name) => default; +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/NullLatencyContextExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/NullLatencyContextExtensions.cs new file mode 100644 index 0000000000..3d43a15b91 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/NullLatencyContextExtensions.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Latency; + +/// +/// Extensions to add a no-op latency context. +/// +public static class NullLatencyContextExtensions +{ + /// + /// Add a no-op latency context to a dependency injection container. + /// + /// The dependency injection container to add the context to. + /// The value of . + /// When is . + public static IServiceCollection AddNullLatencyContext(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + services.TryAddSingleton(); + services.TryAddSingleton(); + + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/CheckpointToken.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/CheckpointToken.cs new file mode 100644 index 0000000000..45b72a66a2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/CheckpointToken.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Latency; + +/// +/// Token representing a registered checkpoint. +/// +[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Comparing instances is not an expected scenario")] +public readonly struct CheckpointToken +{ + /// + /// Gets the name of the checkpoint. + /// + public string Name { get; } + + /// + /// Gets the position of token in the token table. + /// + public int Position { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// Name of the checkpoint. + /// Position of the token in the token table. + /// When is null. + public CheckpointToken(string name, int position) + { + Name = Throw.IfNullOrWhitespace(name); + Position = position; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/ILatencyContextTokenIssuer.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/ILatencyContextTokenIssuer.cs new file mode 100644 index 0000000000..716076a232 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/ILatencyContextTokenIssuer.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Telemetry.Latency; + +/// +/// Issues tokens for various object types. +/// +public interface ILatencyContextTokenIssuer +{ + /// + /// Gets a token for a named tag. + /// + /// Name of the tag. + /// Token to use with . + /// When is . + TagToken GetTagToken(string name); + + /// + /// Gets a token for a named checkpoint. + /// + /// Name of the checkpoint. + /// Token to use with . + /// When is . + CheckpointToken GetCheckpointToken(string name); + + /// + /// Gets a token for a named measure. + /// + /// Name of the measure. + /// Token to use with + /// and . + /// When is . + MeasureToken GetMeasureToken(string name); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/LatencyContextRegistrationOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/LatencyContextRegistrationOptions.cs new file mode 100644 index 0000000000..c32db9efba --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/LatencyContextRegistrationOptions.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Extensions.Telemetry.Latency; + +/// +/// Registered names for . +/// +public class LatencyContextRegistrationOptions +{ + /// + /// Gets or sets the list of registered checkpoint names. + /// + [Required] + public IReadOnlyList CheckpointNames { get; set; } = new List(); + + /// + /// Gets or sets the list of registered measure names. + /// + [Required] + public IReadOnlyList MeasureNames { get; set; } = new List(); + + /// + /// Gets or sets the list of registered tag names. + /// + [Required] + public IReadOnlyList TagNames { get; set; } = new List(); + + internal void AddTagNames(string[] names) => AddToList(TagNames, names); + internal void AddCheckpointNames(string[] names) => AddToList(CheckpointNames, names); + internal void AddMeasureNames(string[] names) => AddToList(MeasureNames, names); + + private static void AddToList(IReadOnlyList list, string[] names) + { + if (list is List l) + { + l.AddRange(names); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/LatencyRegistryExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/LatencyRegistryExtensions.cs new file mode 100644 index 0000000000..85818cdf32 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/LatencyRegistryExtensions.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Latency; + +/// +/// Extensions to configure a latency context. +/// +public static class LatencyRegistryExtensions +{ + /// + /// Registers a set of checkpoint names for a latency context. + /// + /// The dependency injection container to add the names to. + /// Set of checkpoint names. + /// The value of . + /// When or are . + public static IServiceCollection RegisterCheckpointNames(this IServiceCollection services, params string[] names) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(names); + + CheckNames(names); + services.ConfigureOption(o => o.AddCheckpointNames(names)); + + return services; + } + + /// + /// Registers a set of measure names for a latency context. + /// + /// The dependency injection container to add the names to. + /// Set of measure names. + /// Provided service collection. + /// When or are . + public static IServiceCollection RegisterMeasureNames(this IServiceCollection services, params string[] names) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(names); + + CheckNames(names); + services.ConfigureOption(o => o.AddMeasureNames(names)); + + return services; + } + + /// + /// Registers a set of tag names for a latency context. + /// + /// The dependency injection container to add the names to. + /// Set of tag names. + /// Provided service collection. + /// When or are . + public static IServiceCollection RegisterTagNames(this IServiceCollection services, params string[] names) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(names); + + CheckNames(names); + services.ConfigureOption(o => o.AddTagNames(names)); + + return services; + } + + private static void CheckNames(string[] names) + { + foreach (var name in names) + { + if (string.IsNullOrWhiteSpace(name)) + { + Throw.ArgumentException(nameof(names), "Name is either null or whitespace"); + } + } + } + + private static void ConfigureOption(this IServiceCollection services, Action action) + { + _ = services.AddOptions(); + _ = services.Configure(action); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/MeasureToken.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/MeasureToken.cs new file mode 100644 index 0000000000..d8657c2306 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/MeasureToken.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Latency; + +/// +/// Token representing a registered measure. +/// +[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Comparing instances is not an expected scenario")] +public readonly struct MeasureToken +{ + /// + /// Gets the name of the measure. + /// + public string Name { get; } + + /// + /// Gets the position of the token in the token table. + /// + public int Position { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// Name of the measure. + /// Position of the token in the token table. + /// When is null. + public MeasureToken(string name, int position) + { + Name = Throw.IfNullOrWhitespace(name); + Position = position; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/TagToken.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/TagToken.cs new file mode 100644 index 0000000000..2b9dbb9440 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Registration/TagToken.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Latency; + +/// +/// Token representing a registered tag. +/// +[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Comparing instances is not an expected scenario")] +public readonly struct TagToken +{ + /// + /// Gets the name of the tag. + /// + public string Name { get; } + + /// + /// Gets the position of the token in the token table. + /// + public int Position { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// Name of the tag. + /// Position of the token in the token table. + /// When is null. + public TagToken(string name, int position) + { + Name = Throw.IfNullOrWhitespace(name); + Position = position; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Tag.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Tag.cs new file mode 100644 index 0000000000..be4463cb9a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Latency/Tag.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Telemetry.Latency; + +/// +/// Name and value pair to provide metadata about a operation being measured. +/// +[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Comparing instances is not an expected scenario")] +public readonly struct Tag +{ + /// + /// Initializes a new instance of the struct. + /// + /// Name of the tag. + /// Value of the tag. + public Tag(string name, string value) + { + Name = name; + Value = value; + } + + /// + /// Gets the name of the tag. + /// + public string Name { get; } + + /// + /// Gets the value of the tag. + /// + public string Value { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/ILogPropertyCollector.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/ILogPropertyCollector.cs new file mode 100644 index 0000000000..a39d40b6d9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/ILogPropertyCollector.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Telemetry.Logging; + +/// +/// Interface enabling custom providers of logging properties to report properties. +/// +/// +/// See for details on how this interface is used. +/// +public interface ILogPropertyCollector +{ + /// + /// Adds a property to the current log record. + /// + /// The name of the property to add. + /// The value of the property to add. + /// When is . + /// When is empty or contains exclusively whitespace, + /// or when a property of the same name has already been added. + /// + void Add(string propertyName, object? propertyValue); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogMethodAttribute.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogMethodAttribute.cs new file mode 100644 index 0000000000..b18ecec7f4 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogMethodAttribute.cs @@ -0,0 +1,314 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Telemetry.Logging; + +/// +/// Provides information to guide the production of a strongly-typed logging method. +/// +[AttributeUsage(AttributeTargets.Method)] +[Conditional("CODE_GENERATION_ATTRIBUTES")] +public sealed class LogMethodAttribute : Attribute +{ + /// + /// Initializes a new instance of the class + /// which is used to guide the production of a strongly-typed logging method. + /// + /// The stable event id for this log message. + /// The logging level produced when invoking the strongly-typed logging method. + /// The message text output by the logging method. This string is a template that can contain any of the method's parameters. + /// + /// The method this attribute is applied to has some constraints + /// - Logging methods must be partial, and return void. + /// - Logging methods cannot be generic or accept any generic parameters. + /// - Logging method names must not start with an underscore. + /// - Parameter names of logging methods must not start with an underscore. + /// - If the logging method is static, one of its parameters must be of type , or a type that implements the interface. + /// - If the logging method is an instance method, one of the fields of the containing type must be of type . + /// + /// + /// + /// static partial class Log + /// { + /// [LogMethod(0, LogLevel.Critical, "Could not open socket for {hostName}")] + /// static partial void CouldNotOpenSocket(ILogger logger, string hostName); + /// } + /// + /// + public LogMethodAttribute(int eventId, LogLevel level, string message) + { + EventId = eventId; + Level = level; + Message = message; + } + + /// + /// Initializes a new instance of the class + /// which is used to guide the production of a strongly-typed logging method. + /// + /// The stable event id for this log message. + /// The logging level produced when invoking the strongly-typed logging method. + /// + /// The method this attribute is applied to has some constraints + /// - Logging methods must be partial, and return void. + /// - Logging methods cannot be generic or accept any generic parameters. + /// - Logging method names must not start with an underscore. + /// - Parameter names of logging methods must not start with an underscore. + /// - If the logging method is static, one of its parameters must be of type , or a type that implements the interface. + /// - If the logging method is an instance method, one of the fields of the containing type must be of type . + /// + /// + /// + /// static partial class Log + /// { + /// [LogMethod(0, LogLevel.Critical)] + /// static partial void CouldNotOpenSocket(ILogger logger, string hostName); + /// } + /// + /// + public LogMethodAttribute(int eventId, LogLevel level) + { + EventId = eventId; + Level = level; + Message = string.Empty; + } + + /// + /// Initializes a new instance of the class + /// which is used to guide the production of a strongly-typed logging method. + /// + /// The logging level produced when invoking the strongly-typed logging method. + /// The message text output by the logging method. This string is a template that can contain any of the method's parameters. Defaults to empty. + /// + /// The method this attribute is applied to has some constraints + /// - Logging methods must be partial, and return void. + /// - Logging methods cannot be generic or accept any generic parameters. + /// - Logging method names must not start with an underscore. + /// - Parameter names of logging methods must not start with an underscore. + /// - If the logging method is static, one of its parameters must be of type , or a type that implements the interface. + /// - If the logging method is an instance method, one of the fields of the containing type must be of type . + /// + /// This overload doesn't specify an event id, it is set to 0. + /// + /// + /// + /// static partial class Log + /// { + /// [LogMethod(LogLevel.Critical, "Could not open socket for {hostName}")] + /// static partial void CouldNotOpenSocket(ILogger logger, string hostName); + /// } + /// + /// + public LogMethodAttribute(LogLevel level, string message) + { + EventId = 0; + Level = level; + Message = message; + } + + /// + /// Initializes a new instance of the class + /// which is used to guide the production of a strongly-typed logging method. + /// + /// The logging level produced when invoking the strongly-typed logging method. + /// + /// The method this attribute is applied to has some constraints + /// - Logging methods must be partial, and return void. + /// - Logging methods cannot be generic or accept any generic parameters. + /// - Logging method names must not start with an underscore. + /// - Parameter names of logging methods must not start with an underscore. + /// - If the logging method is static, one of its parameters must be of type , or a type that implements the interface. + /// - If the logging method is an instance method, one of the fields of the containing type must be of type . + /// + /// This overload doesn't specify an event id, it is set to 0. + /// + /// + /// + /// static partial class Log + /// { + /// [LogMethod(LogLevel.Critical)] + /// static partial void CouldNotOpenSocket(ILogger logger, string hostName); + /// } + /// + /// + public LogMethodAttribute(LogLevel level) + { + EventId = 0; + Level = level; + Message = string.Empty; + } + + /// + /// Initializes a new instance of the class + /// which is used to guide the production of a strongly-typed logging method. + /// + /// The message text output by the logging method. This string is a template that can contain any of the method's parameters. Defaults to empty. + /// + /// The method this attribute is applied to has some constraints + /// - Logging methods must be partial, and return void. + /// - Logging methods cannot be generic or accept any generic parameters. + /// - Logging method names must not start with an underscore. + /// - Parameter names of logging methods must not start with an underscore. + /// - If the logging method is static, one of its parameters must be of type , or a type that implements the interface. + /// - If the logging method is an instance method, one of the fields of the containing type must be of type . + /// + /// This overload doesn't specify an event id, it is set to 0. + /// + /// + /// + /// static partial class Log + /// { + /// [LogMethod("Could not open socket for {hostName}")] + /// static partial void CouldNotOpenSocket(ILogger logger, LogLevel level, string hostName); + /// } + /// + /// + public LogMethodAttribute(string message) + { + EventId = 0; + Message = message; + } + + /// + /// Initializes a new instance of the class + /// which is used to guide the production of a strongly-typed logging method. + /// + /// The stable event id for this log message. + /// The message text output by the logging method. This string is a template that can contain any of the method's parameters. + /// + /// This overload is not commonly used. In general, the overload that accepts a + /// value is preferred. + /// + /// The method this attribute is applied to has some constraints + /// - Logging methods must be partial, and return void. + /// - Logging methods cannot be generic or accept any generic parameters. + /// - Logging method names must not start with an underscore. + /// - Parameter names of logging methods must not start with an underscore. + /// - If the logging method is static, one of its parameters must be of type , or a type that implements the interface. + /// - If the logging method is an instance method, one of the fields of the containing type must be of type . + /// + /// + /// + /// static partial class Log + /// { + /// [LogMethod(0, "Could not open socket for {hostName}")] + /// static partial void CouldNotOpenSocket(ILogger logger, LogLevel level, string hostName); + /// } + /// + /// + public LogMethodAttribute(int eventId, string message) + { + EventId = eventId; + Message = message; + } + + /// + /// Initializes a new instance of the class + /// which is used to guide the production of a strongly-typed logging method. + /// + /// The stable event id for this log message. + /// + /// This overload is not commonly used. In general, the overload that accepts a + /// value is preferred. + /// + /// The method this attribute is applied to has some constraints + /// - Logging methods must be partial, and return void. + /// - Logging methods cannot be generic or accept any generic parameters. + /// - Logging method names must not start with an underscore. + /// - Parameter names of logging methods must not start with an underscore. + /// - If the logging method is static, one of its parameters must be of type , or a type that implements the interface. + /// - If the logging method is an instance method, one of the fields of the containing type must be of type . + /// + /// + /// + /// static partial class Log + /// { + /// [LogMethod(0)] + /// static partial void CouldNotOpenSocket(ILogger logger, LogLevel level, string hostName); + /// } + /// + /// + public LogMethodAttribute(int eventId) + { + EventId = eventId; + Message = string.Empty; + } + + /// + /// Initializes a new instance of the class + /// which is used to guide the production of a strongly-typed logging method. + /// + /// + /// This overload is not commonly used. In general, the overload that accepts a + /// value is preferred. + /// + /// The method this attribute is applied to has some constraints: + /// - Logging methods must be partial, and return void. + /// - Logging methods cannot be generic or accept any generic parameters. + /// - Logging method names must not start with an underscore. + /// - Parameter names of logging methods must not start with an underscore. + /// - If the logging method is static, one of its parameters must be of type , or a type that implements the interface. + /// - If the logging method is an instance method, one of the fields of the containing type must be of type . + /// + /// This overload doesn't specify an event id, it is set to 0, nor it specifies a message template - it is an empty string. + /// + /// + /// + /// static partial class Log + /// { + /// [LogMethod] + /// static partial void CouldNotOpenSocket(ILogger logger, LogLevel level, string hostName); + /// } + /// + /// + [Experimental] + public LogMethodAttribute() + { + EventId = 0; + Message = string.Empty; + } + + /// + /// Gets the logging event id for the logging method. + /// + /// + /// This is 0 if the logging method doesn't have a stable event id. + /// + public int EventId { get; } + + /// + /// Gets or sets the logging event name for the logging method. + /// + /// + /// This will equal the method name if not specified. + /// + public string? EventName { get; set; } + + /// + /// Gets the logging level for the logging method. + /// + public LogLevel? Level { get; } + + /// + /// Gets the message text for the logging method. + /// + public string Message { get; } + + /// + /// Gets or sets a value indicating whether the generated code should omit the logic to check whether a log level is enabled. + /// + /// + /// The generated code contains an optimization to avoid calling into the underlying if the log method's log level + /// is currently not enabled. If your application is already performing this check before calling the logging method, then you + /// can remove the redundant check performed in the generated code by setting this option to . + /// + /// This defaults to if the log method's logging level is Error or Critical, otherwise it defaults + /// to . + /// + public bool SkipEnabledCheck { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogMethodHelper.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogMethodHelper.cs new file mode 100644 index 0000000000..bfa1c1ce49 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogMethodHelper.cs @@ -0,0 +1,211 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +#if NET6_0_OR_GREATER +using Microsoft.Extensions.Logging; +#endif +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Telemetry.Logging; + +/// +/// Utility type to support generated logging methods. +/// +/// +/// This type is not intended to be directly invoked by application code, +/// it is intended to be invoked by generated logging method code. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class LogMethodHelper : List>, ILogPropertyCollector, IEnrichmentPropertyBag, IResettable +{ + private const string Separator = "_"; + + /// + public void Add(string propertyName, object? propertyValue) + { + _ = Throw.IfNull(propertyName); + + string fullName = ParameterName.Length > 0 ? ParameterName + Separator + propertyName : propertyName; + Add(new KeyValuePair(fullName, propertyValue)); + } + + /// + /// Resets state of this container as described in . + /// + /// + /// if the object successfully reset and can be reused. + /// + public bool TryReset() + { + Clear(); + ParameterName = string.Empty; + return true; + } + + /// + /// Gets or sets the name of the logging method parameter for which to collect properties. + /// + public string ParameterName { get; set; } = string.Empty; + + /// + /// Enumerates an enumerable into a string. + /// + /// The enumerable object. + /// + /// A string representation of the enumerable. + /// + public static string Stringify(IEnumerable? enumerable) + { + if (enumerable == null) + { + return "null"; + } + + var sb = PoolFactory.SharedStringBuilderPool.Get(); + _ = sb.Append('['); + + bool first = true; + foreach (object? e in enumerable) + { + if (!first) + { + _ = sb.Append(','); + } + + if (e == null) + { + _ = sb.Append("null"); + } + else + { + _ = sb.Append(FormattableString.Invariant($"\"{e}\"")); + } + + first = false; + } + + _ = sb.Append(']'); + var result = sb.ToString(); + PoolFactory.SharedStringBuilderPool.Return(sb); + return result; + } + + /// + /// Enumerates an enumerable of key/value pairs into a string. + /// + /// Type of keys. + /// Type of values. + /// The enumerable object. + /// + /// A string representation of the enumerable. + /// + public static string Stringify(IEnumerable>? enumerable) + { + if (enumerable == null) + { + return "null"; + } + + var sb = PoolFactory.SharedStringBuilderPool.Get(); + _ = sb.Append('{'); + + bool first = true; + foreach (var kvp in enumerable) + { + if (!first) + { + _ = sb.Append(','); + } + + if (typeof(TKey).IsValueType || kvp.Key is not null) + { + _ = sb.Append(FormattableString.Invariant($"\"{kvp.Key}\"=")); + } + else + { + _ = sb.Append("null="); + } + + if (typeof(TValue).IsValueType || kvp.Value is not null) + { + _ = sb.Append(FormattableString.Invariant($"\"{kvp.Value}\"")); + } + else + { + _ = sb.Append("null"); + } + + first = false; + } + + _ = sb.Append('}'); + var result = sb.ToString(); + PoolFactory.SharedStringBuilderPool.Return(sb); + return result; + } + + private static readonly ObjectPool _helpers = PoolFactory.CreateResettingPool(); + + /// + /// Gets an instance of a helper from the global pool. + /// + /// A usable instance. + [SuppressMessage("Minor Code Smell", "S4049:Properties should be preferred", Justification = "Not appropriate")] + public static LogMethodHelper GetHelper() => _helpers.Get(); + + /// + /// Returns a helper instance to the global pool. + /// + /// The helper instance. + public static void ReturnHelper(LogMethodHelper helper) => _helpers.Return(helper); + + /// + void IEnrichmentPropertyBag.Add(string key, object value) + { + _ = Throw.IfNullOrEmpty(key); + Add(new KeyValuePair(key, value)); + } + + /// + void IEnrichmentPropertyBag.Add(string key, string value) + { + _ = Throw.IfNullOrEmpty(key); + Add(new KeyValuePair(key, value)); + } + + /// + void IEnrichmentPropertyBag.Add(ReadOnlySpan> properties) + { + foreach (var p in properties) + { + // we're going from KVP to KVP which is strictly correct, so ignore the complaint +#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + Add(p); +#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + } + } + + /// + void IEnrichmentPropertyBag.Add(ReadOnlySpan> properties) + { + foreach (var p in properties) + { + Add(new KeyValuePair(p.Key, p.Value)); + } + } + +#if NET6_0_OR_GREATER + /// + /// Gets log define options configured to skip the log level enablement check. + /// + public static LogDefineOptions SkipEnabledCheckOptions { get; } = new() { SkipEnabledCheck = true }; +#endif +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogPropertiesAttribute.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogPropertiesAttribute.cs new file mode 100644 index 0000000000..946ad39d07 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogPropertiesAttribute.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Logging; + +/// +/// Marks a logging method parameter whose public properties need to be logged. +/// +/// +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] +[Conditional("CODE_GENERATION_ATTRIBUTES")] +public sealed class LogPropertiesAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// Use this parameterless constructor if you want + /// to get a source-generated set of properties to be logged. + /// In case you need to provide your own set of properties or their custom names, please + /// use the constructor overload instead. + /// + /// + /// + /// [LogMethod(1, LogLevel.Warning, "Logging complex object here.")] + /// static partial void LogMethod(ILogger logger, [LogProperties] ClassToLog param); + /// + /// + public LogPropertiesAttribute() + { + } + + /// + /// Initializes a new instance of the class with custom properties provider. + /// + /// A type containing a method that provides a custom set of properties to log. + /// The name of a method on the provider type which generates a custom set of properties to log. + /// + /// When or are . + /// + /// + /// When is either an empty string or contains only whitespace. + /// + /// + /// You can create your own method that will generate the exact set of properties to log + /// for a given input object. + /// + /// Do NOT use this constructor overload if you want to have a default source-generated set of properties to log, + /// Use the parameterless constructor in that case. + /// + /// The method referenced by this constructor should be non-generic, static, public and it should have two parameters: + /// + /// + /// First one of type + /// + /// + /// + /// Second one of T? type, where T is a type of logging method parameter that you want to log. + /// + /// + /// + /// + /// + /// + /// [LogMethod(1, LogLevel.Warning, "Custom properties for {Param}.")] + /// static partial void LogMethod(ILogger logger, + /// [LogProperties(typeof(CustomProvider), nameof(CustomProvider.GetPropertiesToLog))] ClassToLog param); + /// + /// public static class CustomProvider + /// { + /// public static void GetPropertiesToLog(ILogPropertyCollector props, ClassToLog? param) + /// { + /// props.Add("Custom_property_name", param?.MyProperty); + /// props.Add(nameof(ClassToLog.AnotherProperty), param?.AnotherProperty); + /// // ... + /// } + /// } + /// + /// + /// + public LogPropertiesAttribute(Type providerType, string providerMethod) + { + ProviderType = Throw.IfNull(providerType); + ProviderMethod = Throw.IfNullOrWhitespace(providerMethod); + } + + /// + /// Gets the containing the method that provides properties to be logged. + /// + public Type? ProviderType { get; } + + /// + /// Gets the name of the method that provides properties to be logged. + /// + public string? ProviderMethod { get; } + + /// + /// Gets or sets a value indicating whether null properties are logged. + /// + /// + /// Defaults to . + /// + public bool SkipNullProperties { get; set; } + + /// + /// Gets or sets a value indicating whether to prefix the name of the logging method parameter to the generated name of each property being logged. + /// + /// + /// Defaults to . + /// + public bool OmitParameterName { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogPropertyIgnoreAttribute.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogPropertyIgnoreAttribute.cs new file mode 100644 index 0000000000..14acee3d42 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogPropertyIgnoreAttribute.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Logging; + +/// +/// Indicates that a property should not be logged. +/// +/// . +[AttributeUsage(AttributeTargets.Property)] +[Conditional("CODE_GENERATION_ATTRIBUTES")] +public sealed class LogPropertyIgnoreAttribute : Attribute +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/CounterAttribute.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/CounterAttribute.cs new file mode 100644 index 0000000000..4756beb6de --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/CounterAttribute.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Telemetry.Metering; + +/// +/// Provides information to guide the production of a strongly-typed 64 bit integer counter metric factory method and associated type. +/// +/// +/// This attribute is applied to a method which has the following constraints: +/// +/// Must be a partial method. +/// Must return metricName as the type. A class with that name will be generated. +/// Must not be generic. +/// Must have System.Diagnostics.Metrics.Meter as first parameter. +/// Must have all the keys provided in staticDimensions as string type parameters. +/// +/// +/// +/// +/// static partial class Metric +/// { +/// [Counter("RequestName", "RequestStatusCode")] +/// static partial RequestCounter CreateRequestCounter(Meter meter); +/// } +/// +/// +[AttributeUsage(AttributeTargets.Method)] +public sealed class CounterAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// Dimension names. + public CounterAttribute(params string[] dimensions) + { + Dimensions = dimensions; + } + + /// + /// Initializes a new instance of the class. + /// + /// A type providing the metric dimensions. The dimensions are taken from the type's public fields and properties. + public CounterAttribute(Type type) + { + Type = type; + } + + /// + /// Gets or sets the name of the metric. + /// + /// + /// + /// static partial class Metric + /// { + /// [Counter("RequestName", "RequestStatusCode", Name="SampleMetric")] + /// static partial RequestCounter CreateRequestCounter(Meter meter); + /// } + /// + /// + /// + /// In this example the metric name is SampleMetric. When Name is not provided + /// the return type of the method is used as metric name. In this example, this would + /// be RequestCounter if Name wasn't provided. + /// + public string? Name { get; set; } + + /// + /// Gets the metric's dimensions. + /// + public string[]? Dimensions { get; } + + /// + /// Gets the type that supplies metric dimensions. + /// + public Type? Type { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/CounterAttributeT.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/CounterAttributeT.cs new file mode 100644 index 0000000000..10d853fbc7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/CounterAttributeT.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Telemetry.Metering; + +#pragma warning disable SA1649 // File name should match first type name + +/// +/// Provides information to guide the production of a strongly-typed 64 bit integer counter metric factory method and associated type. +/// +/// +/// The type of value the counter will hold, which is limited to , , , , +/// , , or . +/// +/// +/// This attribute is applied to a method which has the following constraints: +/// +/// Must be a partial method. +/// Must return metricName as the type. A class with that name will be generated. +/// Must not be generic. +/// Must have System.Diagnostics.Metrics.Meter as first parameter. +/// Must have all the keys provided in staticDimensions as string type parameters. +/// +/// +/// +/// +/// static partial class Metric +/// { +/// [Counter<int>("RequestName", "RequestStatusCode")] +/// static partial RequestCounter CreateRequestCounter(Meter meter); +/// } +/// +/// +[AttributeUsage(AttributeTargets.Method)] +public sealed class CounterAttribute : Attribute + where T : struct +{ + /// + /// Initializes a new instance of the class. + /// + /// variable array of dimension names. + public CounterAttribute(params string[] dimensions) + { + Dimensions = dimensions; + } + + /// + /// Initializes a new instance of the class. + /// + /// A type providing the metric dimensions. The dimensions are taken from the type's public fields and properties. + public CounterAttribute(Type type) + { + Type = type; + } + + /// + /// Gets or sets the name of the metric. + /// + /// + /// + /// static partial class Metric + /// { + /// [Counter<int>("RequestName", "RequestStatusCode", Name="SampleMetric")] + /// static partial RequestCounter CreateRequestCounter(Meter meter); + /// } + /// + /// + /// + /// In this example the metric name is SampleMetric. When Name is not provided + /// the return type of the method is used as metric name. In this example, this would + /// be RequestCounter if Name wasn't provided. + /// + public string? Name { get; set; } + + /// + /// Gets the metric's dimensions. + /// + public string[]? Dimensions { get; } + + /// + /// Gets the type that supplies metric dimensions. + /// + public Type? Type { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/DimensionAttribute.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/DimensionAttribute.cs new file mode 100644 index 0000000000..c95cb0e568 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/DimensionAttribute.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Telemetry.Metering; + +/// +/// Provides dimension information for strongly-typed metrics. +/// +/// +/// This attribute is applied to fields or properties of a metric class to override default dimension names. By default, +/// the dimension name is the same as the respective field or property. Using this attribute you can override the default +/// and provide a custom dimension name. +/// +/// +/// +/// public class MyStrongTypeMetric +/// { +/// [Dimension("dimension_name_as_per_some_convention1")] +/// public string Dimension1 { get; set; } +/// +/// [Dimension("dimension_name_as_per_some_convention2")] +/// public string Dimension2; +/// } +/// +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public sealed class DimensionAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// Dimension name. + public DimensionAttribute(string name) + { + Name = name; + } + + /// + /// Gets the name of the dimension. + /// + public string Name { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/GaugeAttribute.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/GaugeAttribute.cs new file mode 100644 index 0000000000..65828940f7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/GaugeAttribute.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Telemetry.Metering; + +/// +/// Provides information to guide the production of a strongly-typed gauge metric factory method and associated type. +/// +/// +/// This attribute is applied to a method which has the following constraints: +/// +/// Must be a partial method. +/// Must return metricName as the type. A class with that name will be generated. +/// Must not be generic. +/// Must have System.Diagnostics.Metrics.Meter as first parameter. +/// Must have all the keys provided in staticDimensions as string type parameters. +/// +/// +/// +/// +/// static partial class Metric +/// { +/// [Gauge] +/// static partial MemoryUsage CreateMemoryUsage(IMeter meter); +/// } +/// +/// +[AttributeUsage(AttributeTargets.Method)] +public sealed class GaugeAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// Variable array of dimension names. + public GaugeAttribute(params string[] dimensions) + { + Dimensions = dimensions; + } + + /// + /// Initializes a new instance of the class. + /// + /// A type providing the metric dimensions. The dimensions are taken from the type's public fields and properties. + public GaugeAttribute(Type type) + { + Type = type; + } + + /// + /// Gets or sets the name of the metric. + /// + /// + /// + /// static partial class Metric + /// { + /// [Gauge(Name="SampleMetric")] + /// static partial MemoryUsage CreateMemoryUsage(IMeter meter); + /// } + /// + /// + /// + /// In this example the metric name is SampleMetric. When Name is not provided + /// the return type of the method is used as metric name. In this example, this would + /// be MemoryUsage if Name wasn't provided. + /// + public string? Name { get; set; } + + /// + /// Gets the metric's dimensions. + /// + public string[]? Dimensions { get; } + + /// + /// Gets the type that supplies metric dimensions. + /// + public Type? Type { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/HistogramAttribute.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/HistogramAttribute.cs new file mode 100644 index 0000000000..218923891c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/HistogramAttribute.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Telemetry.Metering; + +/// +/// Provides information to guide the production of a strongly-typed histogram metric factory method and associated type. +/// +/// +/// This attribute is applied to a method which has the following constraints: +/// +/// Must be a partial method. +/// Must return metricName as the type. A class with that name will be generated. +/// Must not be generic. +/// Must have System.Diagnostics.Metrics.Meter as first parameter. +/// Must have all the keys provided in staticDimensions as string type parameters. +/// +/// +/// +/// +/// static partial class Metric +/// { +/// [Histogram("RequestName", "RequestStatusCode")] +/// static partial RequestLatency CreateRequestLatency(IMeter meter); +/// } +/// +/// +[AttributeUsage(AttributeTargets.Method)] +public sealed class HistogramAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// variable array of dimension names. + public HistogramAttribute(params string[] dimensions) + { + Dimensions = dimensions; + } + + /// + /// Initializes a new instance of the class. + /// + /// A type providing the metric dimensions. The dimensions are taken from the type's public fields and properties. + public HistogramAttribute(Type type) + { + Type = type; + } + + /// + /// Gets or sets the name of the metric. + /// + /// + /// + /// static partial class Metric + /// { + /// [Histogram("RequestName", "RequestStatusCode", Name="SampleMetric")] + /// static partial RequestLatency CreateRequestLatency(IMeter meter); + /// } + /// + /// + /// + /// In this example the metric name is SampleMetric. When Name is not provided + /// the return type of the method is used as metric name. In this example, this would + /// be RequestLatency if Name wasn't provided. + /// + public string? Name { get; set; } + + /// + /// Gets the metric's dimensions. + /// + public string[]? Dimensions { get; } + + /// + /// Gets the type that supplies metric dimensions. + /// + public Type? Type { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/HistogramAttributeT.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/HistogramAttributeT.cs new file mode 100644 index 0000000000..02ab4c6804 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/HistogramAttributeT.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Telemetry.Metering; + +#pragma warning disable SA1649 // File name should match first type name + +/// +/// Provides information to guide the production of a strongly-typed histogram metric factory method and associated type. +/// +/// +/// The type of value the histogram will hold, which is limited to , , , , +/// , , or . +/// +/// +/// This attribute is applied to a method which has the following constraints: +/// +/// Must be a partial method. +/// Must return metricName as the type. A class with that name will be generated. +/// Must not be generic. +/// Must have System.Diagnostics.Metrics.Meter as first parameter. +/// Must have all the keys provided in staticDimensions as string type parameters. +/// +/// +/// +/// +/// static partial class Metric +/// { +/// [Histogram<int>("RequestName", "RequestStatusCode")] +/// static partial RequestLatency CreateRequestLatency(Meter meter); +/// } +/// +/// +[Experimental] +[AttributeUsage(AttributeTargets.Method)] +public sealed class HistogramAttribute : Attribute + where T : struct +{ + /// + /// Initializes a new instance of the class. + /// + /// variable array of dimension names. + public HistogramAttribute(params string[] dimensions) + { + Dimensions = dimensions; + } + + /// + /// Initializes a new instance of the class. + /// + /// A type providing the metric dimensions. The dimensions are taken from the type's public fields and properties. + public HistogramAttribute(Type type) + { + Type = type; + } + + /// + /// Gets or sets the name of the metric. + /// + /// + /// In this example metric name is SampleMetric. + /// If Name wasn't passed, it would be RequestLatency. + /// + /// static partial class Metric + /// { + /// [Histogram<int>("RequestName", "RequestStatusCode", Name = "SampleMetric")] + /// static partial RequestLatency CreateRequestLatency(Meter meter); + /// } + /// + /// + /// + /// In this example the metric name is SampleMetric. When Name is not provided + /// the return type of the method is used as metric name. In this example, this would + /// be RequestLatency if Name wasn't provided. + /// + public string? Name { get; set; } + + /// + /// Gets the metric's dimensions. + /// + public string[]? Dimensions { get; } + + /// + /// Gets the type that supplies metric dimensions. + /// + public Type? Type { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/MeterT.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/MeterT.cs new file mode 100644 index 0000000000..6726b6cd6d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/MeterT.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; + +namespace Microsoft.Extensions.Telemetry.Metering; + +#pragma warning disable SA1649 // File name should match first type name + +/// +/// A meter class where the meter name is derived from the specified type name. +/// +/// The type whose name is used as the meter name. +[Experimental] +public class Meter : Meter +{ + /// + /// Initializes a new instance of the class. + /// + [Experimental] + public Meter() + : base(typeof(TMeterName).FullName!) + { + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/MeteringExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/MeteringExtensions.cs new file mode 100644 index 0000000000..87e705ca2e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Metering/MeteringExtensions.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.Telemetry.Metering; + +/// +/// Extensions to control metering integration. +/// +public static class MeteringExtensions +{ + /// + /// Registers to a dependency injecion container. + /// + /// The dependency injection container to register metering into. + /// The value of . + [Experimental] + public static IServiceCollection RegisterMetering(this IServiceCollection services) + { + services.TryAdd(ServiceDescriptor.Singleton(typeof(Meter<>), typeof(Meter<>))); + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.csproj new file mode 100644 index 0000000000..4edcb8dc6b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.csproj @@ -0,0 +1,42 @@ + + + Microsoft.Extensions.Telemetry + Common abstractions for high-level telemetry primitives. + Telemetry + + + + true + true + DisableMicrosoftExtensionsTelemetrySourceGeneration + true + + + + normal + 100 + 100 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/buildTransitive/Microsoft.Extensions.Telemetry.Abstractions.props b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/buildTransitive/Microsoft.Extensions.Telemetry.Abstractions.props new file mode 100644 index 0000000000..7bc91e4438 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/buildTransitive/Microsoft.Extensions.Telemetry.Abstractions.props @@ -0,0 +1,2 @@ + + diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/buildTransitive/Microsoft.Extensions.Telemetry.Abstractions.targets b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/buildTransitive/Microsoft.Extensions.Telemetry.Abstractions.targets new file mode 100644 index 0000000000..ceadfacb28 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/buildTransitive/Microsoft.Extensions.Telemetry.Abstractions.targets @@ -0,0 +1,3 @@ + + + diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Console/Latency/LarencyConsoleOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Latency/LarencyConsoleOptions.cs new file mode 100644 index 0000000000..676cdd0d79 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Latency/LarencyConsoleOptions.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Telemetry.Console; + +/// +/// Options for console latency data exporter. +/// +public class LarencyConsoleOptions +{ + /// + /// Gets or sets a value indicating whether to emit latency checkpoint information to the console. + /// + /// + /// Defaults to . + /// + public bool OutputCheckpoints { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to emit latency tag information to the console. + /// + /// + /// Defaults to . + /// + public bool OutputTags { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to emit latency measure information to the console. + /// + /// + /// Defaults to . + /// + public bool OutputMeasures { get; set; } = true; +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Console/Latency/LatencyConsoleExporter.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Latency/LatencyConsoleExporter.cs new file mode 100644 index 0000000000..2bf5fb3de2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Latency/LatencyConsoleExporter.cs @@ -0,0 +1,135 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Shared.Memoization; +using Microsoft.Shared.Pools; +using Microsoft.Shared.Text; +using CF = Microsoft.Shared.Text.CompositeFormat; + +namespace Microsoft.Extensions.Telemetry.Console; + +internal sealed class LatencyConsoleExporter : ILatencyDataExporter +{ + private const int MillisPerSecond = 1000; + + private static readonly CF _title = CF.Parse("Latency sample #{0}: {1}ms, {2} checkpoints, {3} tags, {4} measures" + Environment.NewLine); + private static readonly Func _rows = Memoize.Function(nameColumnWidth => CF.Parse($" {{0,-{nameColumnWidth}}} | {{1}}" + Environment.NewLine)); + private static readonly Func _dashes = Memoize.Function(num => new('-', num)); + + private readonly bool _outputCheckpoints; + private readonly bool _outputTags; + private readonly bool _outputMeasures; + private long _sampleCount = -1; + + public LatencyConsoleExporter(IOptions options) + { + var o = options.Value; + _outputCheckpoints = o.OutputCheckpoints; + _outputTags = o.OutputTags; + _outputMeasures = o.OutputMeasures; + } + + public Task ExportAsync(LatencyData latencyData, CancellationToken cancellationToken) + { + var sb = PoolFactory.SharedStringBuilderPool.Get(); + try + { + var cnt = Interlocked.Increment(ref _sampleCount); + + _ = sb.AppendFormat( + _title, + null, + cnt, + ((double)latencyData.DurationTimestamp / latencyData.DurationTimestampFrequency) * MillisPerSecond, + latencyData.Checkpoints.Length, + latencyData.Tags.Length, + latencyData.Measures.Length); + + bool needBlankLine = false; + if (_outputCheckpoints && latencyData.Checkpoints.Length > 0) + { + int nameColumnWidth = 0; + for (int i = 0; i < latencyData.Checkpoints.Length; i++) + { + nameColumnWidth = Math.Max(nameColumnWidth, latencyData.Checkpoints[i].Name.Length); + } + + var fmt = StartTable(sb, "Checkpoint", "Value (ms)", nameColumnWidth, ref needBlankLine); + for (int i = 0; i < latencyData.Checkpoints.Length; i++) + { + var c = latencyData.Checkpoints[i]; + _ = sb.AppendFormat(fmt, null, c.Name, ((double)c.Elapsed / c.Frequency) * MillisPerSecond); + } + } + + if (_outputTags && latencyData.Tags.Length > 0) + { + int nameColumnWidth = 0; + for (int i = 0; i < latencyData.Tags.Length; i++) + { + nameColumnWidth = Math.Max(nameColumnWidth, latencyData.Tags[i].Name.Length); + } + + var fmt = StartTable(sb, "Tag", "Value", nameColumnWidth, ref needBlankLine); + for (int i = 0; i < latencyData.Tags.Length; i++) + { + var t = latencyData.Tags[i]; + _ = sb.AppendFormat(fmt, null, t.Name, t.Value); + } + } + + if (_outputMeasures && latencyData.Measures.Length > 0) + { + int nameColumnWidth = 0; + for (int i = 0; i < latencyData.Measures.Length; i++) + { + nameColumnWidth = Math.Max(nameColumnWidth, latencyData.Measures[i].Name.Length); + } + + var fmt = StartTable(sb, "Measure", "Value", nameColumnWidth, ref needBlankLine); + for (int i = 0; i < latencyData.Measures.Length; i++) + { + var m = latencyData.Measures[i]; + _ = sb.AppendFormat(fmt, null, m.Name, m.Value); + } + } + + // the whole sample is output in a single shot so it won't be interrupted with conflicting output + return System.Console.Out.WriteAsync(sb.ToString()); + } + finally + { + PoolFactory.SharedStringBuilderPool.Return(sb); + } + } + + private static CF StartTable(StringBuilder sb, string nameHeader, string valueHeader, int nameColumnWidth, ref bool needBlankLine) + { + if (needBlankLine) + { + _ = sb.AppendLine(); + } + else + { + needBlankLine = true; + } + + nameColumnWidth = Math.Max(nameColumnWidth, nameHeader.Length); + var fmt = _rows(nameColumnWidth); + _ = sb.AppendFormat(fmt, null, nameHeader, valueHeader); + + _ = sb.Append(" "); + _ = sb.Append(_dashes(nameColumnWidth + 1)); + _ = sb.Append('|'); + _ = sb.AppendLine(_dashes(valueHeader.Length + 1)); + + return fmt; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Console/Latency/LatencyConsoleExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Latency/LatencyConsoleExtensions.cs new file mode 100644 index 0000000000..0683a3c0d7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Latency/LatencyConsoleExtensions.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Console; + +/// +/// Extensions to add console latency data exporter. +/// +public static class LatencyConsoleExtensions +{ + /// + /// Add latency data exporter for the console. + /// + /// Dependency injection container. + /// Provided service collection with added. + /// When is . + public static IServiceCollection AddConsoleLatencyDataExporter(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + _ = services.AddOptions(); + services.TryAddSingleton(); + + return services; + } + + /// + /// Add latency data exporter for the console. + /// + /// Dependency injection container. + /// configuration delegate. + /// Provided service collection with added. + /// Either or is . + public static IServiceCollection AddConsoleLatencyDataExporter(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + _ = services.Configure(configure); + + return AddConsoleLatencyDataExporter(services); + } + + /// + /// Add latency data exporter for the console. + /// + /// Dependency injection container. + /// Configuration of . + /// Provided service collection with added. + /// Either or is . + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(LarencyConsoleOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed by [DynamicDependency]")] + public static IServiceCollection AddConsoleLatencyDataExporter(this IServiceCollection services, IConfigurationSection section) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(section); + + _ = services.Configure(section); + + return AddConsoleLatencyDataExporter(services); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/ColorSet.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/ColorSet.cs new file mode 100644 index 0000000000..d1e13c2248 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/ColorSet.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +using System; +using Microsoft.Extensions.EnumStrings; + +[assembly: EnumStrings(typeof(ConsoleColor))] + +namespace Microsoft.Extensions.Telemetry.Console.Internal; + +// An internal class from https://github.com/dotnet/runtime/blob/57e1c232ee4ce5a5a4413de4fc66544e4e346a62/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatter.cs#L205 +internal readonly struct ColorSet : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// Foreground color. + /// Background color. + public ColorSet(ConsoleColor? foreground, ConsoleColor? background) + { + Foreground = foreground; + Background = background; + } + + internal ColorSet(ConsoleColor? background) + { + Foreground = null; + Background = background; + } + + /// + /// Gets foreground color. + /// + public ConsoleColor? Foreground { get; } + + /// + /// Gets background color. + /// + public ConsoleColor? Background { get; } + + /// + /// Compares two colors. + /// + /// Other color. + /// if equal. + public override bool Equals(object? obj) + { + return obj is ColorSet set && Equals(set); + } + + /// + /// Compares two colors. + /// + /// Other color. + /// if equal. + public bool Equals(ColorSet other) + { + return Foreground == other.Foreground && + Background == other.Background; + } + + /// + /// Get a unique hashcode for the color. + /// + /// Hash code. + public override int GetHashCode() => HashCode.Combine(Foreground, Background); + + /// + /// Compares two color sets for equality. + /// + /// Left color set. + /// Right color set. + /// if equal. + public static bool operator ==(ColorSet left, ColorSet right) + { + return left.Equals(right); + } + + /// + /// Compares two color sets for inequality. + /// + /// Left color set. + /// Right color set. + /// if not equal. + public static bool operator !=(ColorSet left, ColorSet right) + { + return !(left == right); + } + + /// + /// Returns a string representation of this instance. + /// + /// A that represents current color set. + public override string ToString() + { + var foreground = Foreground?.ToInvariantString() ?? "None"; + var background = Background?.ToInvariantString() ?? "None"; + return foreground + " on " + background; + } +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/Colors.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/Colors.cs new file mode 100644 index 0000000000..68ce6cf763 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/Colors.cs @@ -0,0 +1,832 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +using System; + +namespace Microsoft.Extensions.Telemetry.Console.Internal; + +/// +/// The set of predefined values of type. +/// +internal static class Colors +{ + public static readonly ColorSet None = new(null, null); + + public static readonly ColorSet BlackOnDarkBlue = + new(ConsoleColor.Black, ConsoleColor.DarkBlue); + + public static readonly ColorSet BlackOnDarkGreen = + new(ConsoleColor.Black, ConsoleColor.DarkGreen); + + public static readonly ColorSet BlackOnDarkCyan = + new(ConsoleColor.Black, ConsoleColor.DarkCyan); + + public static readonly ColorSet BlackOnDarkRed = + new(ConsoleColor.Black, ConsoleColor.DarkRed); + + public static readonly ColorSet BlackOnDarkMagenta = + new(ConsoleColor.Black, ConsoleColor.DarkMagenta); + + public static readonly ColorSet BlackOnDarkYellow = + new(ConsoleColor.Black, ConsoleColor.DarkYellow); + + public static readonly ColorSet BlackOnGray = + new(ConsoleColor.Black, ConsoleColor.Gray); + + public static readonly ColorSet BlackOnDarkGray = + new(ConsoleColor.Black, ConsoleColor.DarkGray); + + public static readonly ColorSet BlackOnBlue = + new(ConsoleColor.Black, ConsoleColor.Blue); + + public static readonly ColorSet BlackOnGreen = + new(ConsoleColor.Black, ConsoleColor.Green); + + public static readonly ColorSet BlackOnCyan = + new(ConsoleColor.Black, ConsoleColor.Cyan); + + public static readonly ColorSet BlackOnRed = + new(ConsoleColor.Black, ConsoleColor.Red); + + public static readonly ColorSet BlackOnMagenta = + new(ConsoleColor.Black, ConsoleColor.Magenta); + + public static readonly ColorSet BlackOnYellow = + new(ConsoleColor.Black, ConsoleColor.Yellow); + + public static readonly ColorSet BlackOnWhite = + new(ConsoleColor.Black, ConsoleColor.White); + + public static readonly ColorSet BlackOnNone = + new(ConsoleColor.Black, null); + + public static readonly ColorSet DarkBlueOnBlack = + new(ConsoleColor.DarkBlue, ConsoleColor.Black); + + public static readonly ColorSet DarkBlueOnDarkGreen = + new(ConsoleColor.DarkBlue, ConsoleColor.DarkGreen); + + public static readonly ColorSet DarkBlueOnDarkCyan = + new(ConsoleColor.DarkBlue, ConsoleColor.DarkCyan); + + public static readonly ColorSet DarkBlueOnDarkRed = + new(ConsoleColor.DarkBlue, ConsoleColor.DarkRed); + + public static readonly ColorSet DarkBlueOnDarkMagenta = + new(ConsoleColor.DarkBlue, ConsoleColor.DarkMagenta); + + public static readonly ColorSet DarkBlueOnDarkYellow = + new(ConsoleColor.DarkBlue, ConsoleColor.DarkYellow); + + public static readonly ColorSet DarkBlueOnGray = + new(ConsoleColor.DarkBlue, ConsoleColor.Gray); + + public static readonly ColorSet DarkBlueOnDarkGray = + new(ConsoleColor.DarkBlue, ConsoleColor.DarkGray); + + public static readonly ColorSet DarkBlueOnBlue = + new(ConsoleColor.DarkBlue, ConsoleColor.Blue); + + public static readonly ColorSet DarkBlueOnGreen = + new(ConsoleColor.DarkBlue, ConsoleColor.Green); + + public static readonly ColorSet DarkBlueOnCyan = + new(ConsoleColor.DarkBlue, ConsoleColor.Cyan); + + public static readonly ColorSet DarkBlueOnRed = + new(ConsoleColor.DarkBlue, ConsoleColor.Red); + + public static readonly ColorSet DarkBlueOnMagenta = + new(ConsoleColor.DarkBlue, ConsoleColor.Magenta); + + public static readonly ColorSet DarkBlueOnYellow = + new(ConsoleColor.DarkBlue, ConsoleColor.Yellow); + + public static readonly ColorSet DarkBlueOnWhite = + new(ConsoleColor.DarkBlue, ConsoleColor.White); + + public static readonly ColorSet DarkBlueOnNone = + new(ConsoleColor.DarkBlue, null); + + public static readonly ColorSet DarkGreenOnBlack = + new(ConsoleColor.DarkGreen, ConsoleColor.Black); + + public static readonly ColorSet DarkGreenOnDarkBlue = + new(ConsoleColor.DarkGreen, ConsoleColor.DarkBlue); + + public static readonly ColorSet DarkGreenOnDarkCyan = + new(ConsoleColor.DarkGreen, ConsoleColor.DarkCyan); + + public static readonly ColorSet DarkGreenOnDarkRed = + new(ConsoleColor.DarkGreen, ConsoleColor.DarkRed); + + public static readonly ColorSet DarkGreenOnDarkMagenta = + new(ConsoleColor.DarkGreen, ConsoleColor.DarkMagenta); + + public static readonly ColorSet DarkGreenOnDarkYellow = + new(ConsoleColor.DarkGreen, ConsoleColor.DarkYellow); + + public static readonly ColorSet DarkGreenOnGray = + new(ConsoleColor.DarkGreen, ConsoleColor.Gray); + + public static readonly ColorSet DarkGreenOnDarkGray = + new(ConsoleColor.DarkGreen, ConsoleColor.DarkGray); + + public static readonly ColorSet DarkGreenOnBlue = + new(ConsoleColor.DarkGreen, ConsoleColor.Blue); + + public static readonly ColorSet DarkGreenOnGreen = + new(ConsoleColor.DarkGreen, ConsoleColor.Green); + + public static readonly ColorSet DarkGreenOnCyan = + new(ConsoleColor.DarkGreen, ConsoleColor.Cyan); + + public static readonly ColorSet DarkGreenOnRed = + new(ConsoleColor.DarkGreen, ConsoleColor.Red); + + public static readonly ColorSet DarkGreenOnMagenta = + new(ConsoleColor.DarkGreen, ConsoleColor.Magenta); + + public static readonly ColorSet DarkGreenOnYellow = + new(ConsoleColor.DarkGreen, ConsoleColor.Yellow); + + public static readonly ColorSet DarkGreenOnWhite = + new(ConsoleColor.DarkGreen, ConsoleColor.White); + + public static readonly ColorSet DarkGreenOnNone = + new(ConsoleColor.DarkGreen, null); + + public static readonly ColorSet DarkCyanOnBlack = + new(ConsoleColor.DarkCyan, ConsoleColor.Black); + + public static readonly ColorSet DarkCyanOnDarkBlue = + new(ConsoleColor.DarkCyan, ConsoleColor.DarkBlue); + + public static readonly ColorSet DarkCyanOnDarkGreen = + new(ConsoleColor.DarkCyan, ConsoleColor.DarkGreen); + + public static readonly ColorSet DarkCyanOnDarkRed = + new(ConsoleColor.DarkCyan, ConsoleColor.DarkRed); + + public static readonly ColorSet DarkCyanOnDarkMagenta = + new(ConsoleColor.DarkCyan, ConsoleColor.DarkMagenta); + + public static readonly ColorSet DarkCyanOnDarkYellow = + new(ConsoleColor.DarkCyan, ConsoleColor.DarkYellow); + + public static readonly ColorSet DarkCyanOnGray = + new(ConsoleColor.DarkCyan, ConsoleColor.Gray); + + public static readonly ColorSet DarkCyanOnDarkGray = + new(ConsoleColor.DarkCyan, ConsoleColor.DarkGray); + + public static readonly ColorSet DarkCyanOnBlue = + new(ConsoleColor.DarkCyan, ConsoleColor.Blue); + + public static readonly ColorSet DarkCyanOnGreen = + new(ConsoleColor.DarkCyan, ConsoleColor.Green); + + public static readonly ColorSet DarkCyanOnCyan = + new(ConsoleColor.DarkCyan, ConsoleColor.Cyan); + + public static readonly ColorSet DarkCyanOnRed = + new(ConsoleColor.DarkCyan, ConsoleColor.Red); + + public static readonly ColorSet DarkCyanOnMagenta = + new(ConsoleColor.DarkCyan, ConsoleColor.Magenta); + + public static readonly ColorSet DarkCyanOnYellow = + new(ConsoleColor.DarkCyan, ConsoleColor.Yellow); + + public static readonly ColorSet DarkCyanOnWhite = + new(ConsoleColor.DarkCyan, ConsoleColor.White); + + public static readonly ColorSet DarkCyanOnNone = + new(ConsoleColor.DarkCyan, null); + + public static readonly ColorSet DarkRedOnBlack = + new(ConsoleColor.DarkRed, ConsoleColor.Black); + + public static readonly ColorSet DarkRedOnDarkBlue = + new(ConsoleColor.DarkRed, ConsoleColor.DarkBlue); + + public static readonly ColorSet DarkRedOnDarkGreen = + new(ConsoleColor.DarkRed, ConsoleColor.DarkGreen); + + public static readonly ColorSet DarkRedOnDarkCyan = + new(ConsoleColor.DarkRed, ConsoleColor.DarkCyan); + + public static readonly ColorSet DarkRedOnDarkMagenta = + new(ConsoleColor.DarkRed, ConsoleColor.DarkMagenta); + + public static readonly ColorSet DarkRedOnDarkYellow = + new(ConsoleColor.DarkRed, ConsoleColor.DarkYellow); + + public static readonly ColorSet DarkRedOnGray = + new(ConsoleColor.DarkRed, ConsoleColor.Gray); + + public static readonly ColorSet DarkRedOnDarkGray = + new(ConsoleColor.DarkRed, ConsoleColor.DarkGray); + + public static readonly ColorSet DarkRedOnBlue = + new(ConsoleColor.DarkRed, ConsoleColor.Blue); + + public static readonly ColorSet DarkRedOnGreen = + new(ConsoleColor.DarkRed, ConsoleColor.Green); + + public static readonly ColorSet DarkRedOnCyan = + new(ConsoleColor.DarkRed, ConsoleColor.Cyan); + + public static readonly ColorSet DarkRedOnRed = + new(ConsoleColor.DarkRed, ConsoleColor.Red); + + public static readonly ColorSet DarkRedOnMagenta = + new(ConsoleColor.DarkRed, ConsoleColor.Magenta); + + public static readonly ColorSet DarkRedOnYellow = + new(ConsoleColor.DarkRed, ConsoleColor.Yellow); + + public static readonly ColorSet DarkRedOnWhite = + new(ConsoleColor.DarkRed, ConsoleColor.White); + + public static readonly ColorSet DarkRedOnNone = + new(ConsoleColor.DarkRed, null); + + public static readonly ColorSet DarkMagentaOnBlack = + new(ConsoleColor.DarkMagenta, ConsoleColor.Black); + + public static readonly ColorSet DarkMagentaOnDarkBlue = + new(ConsoleColor.DarkMagenta, ConsoleColor.DarkBlue); + + public static readonly ColorSet DarkMagentaOnDarkGreen = + new(ConsoleColor.DarkMagenta, ConsoleColor.DarkGreen); + + public static readonly ColorSet DarkMagentaOnDarkCyan = + new(ConsoleColor.DarkMagenta, ConsoleColor.DarkCyan); + + public static readonly ColorSet DarkMagentaOnDarkRed = + new(ConsoleColor.DarkMagenta, ConsoleColor.DarkRed); + + public static readonly ColorSet DarkMagentaOnDarkYellow = + new(ConsoleColor.DarkMagenta, ConsoleColor.DarkYellow); + + public static readonly ColorSet DarkMagentaOnGray = + new(ConsoleColor.DarkMagenta, ConsoleColor.Gray); + + public static readonly ColorSet DarkMagentaOnDarkGray = + new(ConsoleColor.DarkMagenta, ConsoleColor.DarkGray); + + public static readonly ColorSet DarkMagentaOnBlue = + new(ConsoleColor.DarkMagenta, ConsoleColor.Blue); + + public static readonly ColorSet DarkMagentaOnGreen = + new(ConsoleColor.DarkMagenta, ConsoleColor.Green); + + public static readonly ColorSet DarkMagentaOnCyan = + new(ConsoleColor.DarkMagenta, ConsoleColor.Cyan); + + public static readonly ColorSet DarkMagentaOnRed = + new(ConsoleColor.DarkMagenta, ConsoleColor.Red); + + public static readonly ColorSet DarkMagentaOnMagenta = + new(ConsoleColor.DarkMagenta, ConsoleColor.Magenta); + + public static readonly ColorSet DarkMagentaOnYellow = + new(ConsoleColor.DarkMagenta, ConsoleColor.Yellow); + + public static readonly ColorSet DarkMagentaOnWhite = + new(ConsoleColor.DarkMagenta, ConsoleColor.White); + + public static readonly ColorSet DarkMagentaOnNone = + new(ConsoleColor.DarkMagenta, null); + + public static readonly ColorSet DarkYellowOnBlack = + new(ConsoleColor.DarkYellow, ConsoleColor.Black); + + public static readonly ColorSet DarkYellowOnDarkBlue = + new(ConsoleColor.DarkYellow, ConsoleColor.DarkBlue); + + public static readonly ColorSet DarkYellowOnDarkGreen = + new(ConsoleColor.DarkYellow, ConsoleColor.DarkGreen); + + public static readonly ColorSet DarkYellowOnDarkCyan = + new(ConsoleColor.DarkYellow, ConsoleColor.DarkCyan); + + public static readonly ColorSet DarkYellowOnDarkRed = + new(ConsoleColor.DarkYellow, ConsoleColor.DarkRed); + + public static readonly ColorSet DarkYellowOnDarkMagenta = + new(ConsoleColor.DarkYellow, ConsoleColor.DarkMagenta); + + public static readonly ColorSet DarkYellowOnGray = + new(ConsoleColor.DarkYellow, ConsoleColor.Gray); + + public static readonly ColorSet DarkYellowOnDarkGray = + new(ConsoleColor.DarkYellow, ConsoleColor.DarkGray); + + public static readonly ColorSet DarkYellowOnBlue = + new(ConsoleColor.DarkYellow, ConsoleColor.Blue); + + public static readonly ColorSet DarkYellowOnGreen = + new(ConsoleColor.DarkYellow, ConsoleColor.Green); + + public static readonly ColorSet DarkYellowOnCyan = + new(ConsoleColor.DarkYellow, ConsoleColor.Cyan); + + public static readonly ColorSet DarkYellowOnRed = + new(ConsoleColor.DarkYellow, ConsoleColor.Red); + + public static readonly ColorSet DarkYellowOnMagenta = + new(ConsoleColor.DarkYellow, ConsoleColor.Magenta); + + public static readonly ColorSet DarkYellowOnYellow = + new(ConsoleColor.DarkYellow, ConsoleColor.Yellow); + + public static readonly ColorSet DarkYellowOnWhite = + new(ConsoleColor.DarkYellow, ConsoleColor.White); + + public static readonly ColorSet DarkYellowOnNone = + new(ConsoleColor.DarkYellow, null); + + public static readonly ColorSet GrayOnBlack = + new(ConsoleColor.Gray, ConsoleColor.Black); + + public static readonly ColorSet GrayOnDarkBlue = + new(ConsoleColor.Gray, ConsoleColor.DarkBlue); + + public static readonly ColorSet GrayOnDarkGreen = + new(ConsoleColor.Gray, ConsoleColor.DarkGreen); + + public static readonly ColorSet GrayOnDarkCyan = + new(ConsoleColor.Gray, ConsoleColor.DarkCyan); + + public static readonly ColorSet GrayOnDarkRed = + new(ConsoleColor.Gray, ConsoleColor.DarkRed); + + public static readonly ColorSet GrayOnDarkMagenta = + new(ConsoleColor.Gray, ConsoleColor.DarkMagenta); + + public static readonly ColorSet GrayOnDarkYellow = + new(ConsoleColor.Gray, ConsoleColor.DarkYellow); + + public static readonly ColorSet GrayOnDarkGray = + new(ConsoleColor.Gray, ConsoleColor.DarkGray); + + public static readonly ColorSet GrayOnBlue = + new(ConsoleColor.Gray, ConsoleColor.Blue); + + public static readonly ColorSet GrayOnGreen = + new(ConsoleColor.Gray, ConsoleColor.Green); + + public static readonly ColorSet GrayOnCyan = + new(ConsoleColor.Gray, ConsoleColor.Cyan); + + public static readonly ColorSet GrayOnRed = + new(ConsoleColor.Gray, ConsoleColor.Red); + + public static readonly ColorSet GrayOnMagenta = + new(ConsoleColor.Gray, ConsoleColor.Magenta); + + public static readonly ColorSet GrayOnYellow = + new(ConsoleColor.Gray, ConsoleColor.Yellow); + + public static readonly ColorSet GrayOnWhite = + new(ConsoleColor.Gray, ConsoleColor.White); + + public static readonly ColorSet GrayOnNone = + new(ConsoleColor.Gray, null); + + public static readonly ColorSet DarkGrayOnBlack = + new(ConsoleColor.DarkGray, ConsoleColor.Black); + + public static readonly ColorSet DarkGrayOnDarkBlue = + new(ConsoleColor.DarkGray, ConsoleColor.DarkBlue); + + public static readonly ColorSet DarkGrayOnDarkGreen = + new(ConsoleColor.DarkGray, ConsoleColor.DarkGreen); + + public static readonly ColorSet DarkGrayOnDarkCyan = + new(ConsoleColor.DarkGray, ConsoleColor.DarkCyan); + + public static readonly ColorSet DarkGrayOnDarkRed = + new(ConsoleColor.DarkGray, ConsoleColor.DarkRed); + + public static readonly ColorSet DarkGrayOnDarkMagenta = + new(ConsoleColor.DarkGray, ConsoleColor.DarkMagenta); + + public static readonly ColorSet DarkGrayOnDarkYellow = + new(ConsoleColor.DarkGray, ConsoleColor.DarkYellow); + + public static readonly ColorSet DarkGrayOnGray = + new(ConsoleColor.DarkGray, ConsoleColor.Gray); + + public static readonly ColorSet DarkGrayOnBlue = + new(ConsoleColor.DarkGray, ConsoleColor.Blue); + + public static readonly ColorSet DarkGrayOnGreen = + new(ConsoleColor.DarkGray, ConsoleColor.Green); + + public static readonly ColorSet DarkGrayOnCyan = + new(ConsoleColor.DarkGray, ConsoleColor.Cyan); + + public static readonly ColorSet DarkGrayOnRed = + new(ConsoleColor.DarkGray, ConsoleColor.Red); + + public static readonly ColorSet DarkGrayOnMagenta = + new(ConsoleColor.DarkGray, ConsoleColor.Magenta); + + public static readonly ColorSet DarkGrayOnYellow = + new(ConsoleColor.DarkGray, ConsoleColor.Yellow); + + public static readonly ColorSet DarkGrayOnWhite = + new(ConsoleColor.DarkGray, ConsoleColor.White); + + public static readonly ColorSet DarkGrayOnNone = + new(ConsoleColor.DarkGray, null); + + public static readonly ColorSet BlueOnBlack = + new(ConsoleColor.Blue, ConsoleColor.Black); + + public static readonly ColorSet BlueOnDarkBlue = + new(ConsoleColor.Blue, ConsoleColor.DarkBlue); + + public static readonly ColorSet BlueOnDarkGreen = + new(ConsoleColor.Blue, ConsoleColor.DarkGreen); + + public static readonly ColorSet BlueOnDarkCyan = + new(ConsoleColor.Blue, ConsoleColor.DarkCyan); + + public static readonly ColorSet BlueOnDarkRed = + new(ConsoleColor.Blue, ConsoleColor.DarkRed); + + public static readonly ColorSet BlueOnDarkMagenta = + new(ConsoleColor.Blue, ConsoleColor.DarkMagenta); + + public static readonly ColorSet BlueOnDarkYellow = + new(ConsoleColor.Blue, ConsoleColor.DarkYellow); + + public static readonly ColorSet BlueOnGray = + new(ConsoleColor.Blue, ConsoleColor.Gray); + + public static readonly ColorSet BlueOnDarkGray = + new(ConsoleColor.Blue, ConsoleColor.DarkGray); + + public static readonly ColorSet BlueOnGreen = + new(ConsoleColor.Blue, ConsoleColor.Green); + + public static readonly ColorSet BlueOnCyan = + new(ConsoleColor.Blue, ConsoleColor.Cyan); + + public static readonly ColorSet BlueOnRed = + new(ConsoleColor.Blue, ConsoleColor.Red); + + public static readonly ColorSet BlueOnMagenta = + new(ConsoleColor.Blue, ConsoleColor.Magenta); + + public static readonly ColorSet BlueOnYellow = + new(ConsoleColor.Blue, ConsoleColor.Yellow); + + public static readonly ColorSet BlueOnWhite = + new(ConsoleColor.Blue, ConsoleColor.White); + + public static readonly ColorSet BlueOnNone = + new(ConsoleColor.Blue, null); + + public static readonly ColorSet GreenOnBlack = + new(ConsoleColor.Green, ConsoleColor.Black); + + public static readonly ColorSet GreenOnDarkBlue = + new(ConsoleColor.Green, ConsoleColor.DarkBlue); + + public static readonly ColorSet GreenOnDarkGreen = + new(ConsoleColor.Green, ConsoleColor.DarkGreen); + + public static readonly ColorSet GreenOnDarkCyan = + new(ConsoleColor.Green, ConsoleColor.DarkCyan); + + public static readonly ColorSet GreenOnDarkRed = + new(ConsoleColor.Green, ConsoleColor.DarkRed); + + public static readonly ColorSet GreenOnDarkMagenta = + new(ConsoleColor.Green, ConsoleColor.DarkMagenta); + + public static readonly ColorSet GreenOnDarkYellow = + new(ConsoleColor.Green, ConsoleColor.DarkYellow); + + public static readonly ColorSet GreenOnGray = + new(ConsoleColor.Green, ConsoleColor.Gray); + + public static readonly ColorSet GreenOnDarkGray = + new(ConsoleColor.Green, ConsoleColor.DarkGray); + + public static readonly ColorSet GreenOnBlue = + new(ConsoleColor.Green, ConsoleColor.Blue); + + public static readonly ColorSet GreenOnCyan = + new(ConsoleColor.Green, ConsoleColor.Cyan); + + public static readonly ColorSet GreenOnRed = + new(ConsoleColor.Green, ConsoleColor.Red); + + public static readonly ColorSet GreenOnMagenta = + new(ConsoleColor.Green, ConsoleColor.Magenta); + + public static readonly ColorSet GreenOnYellow = + new(ConsoleColor.Green, ConsoleColor.Yellow); + + public static readonly ColorSet GreenOnWhite = + new(ConsoleColor.Green, ConsoleColor.White); + + public static readonly ColorSet GreenOnNone = + new(ConsoleColor.Green, null); + + public static readonly ColorSet CyanOnBlack = + new(ConsoleColor.Cyan, ConsoleColor.Black); + + public static readonly ColorSet CyanOnDarkBlue = + new(ConsoleColor.Cyan, ConsoleColor.DarkBlue); + + public static readonly ColorSet CyanOnDarkGreen = + new(ConsoleColor.Cyan, ConsoleColor.DarkGreen); + + public static readonly ColorSet CyanOnDarkCyan = + new(ConsoleColor.Cyan, ConsoleColor.DarkCyan); + + public static readonly ColorSet CyanOnDarkRed = + new(ConsoleColor.Cyan, ConsoleColor.DarkRed); + + public static readonly ColorSet CyanOnDarkMagenta = + new(ConsoleColor.Cyan, ConsoleColor.DarkMagenta); + + public static readonly ColorSet CyanOnDarkYellow = + new(ConsoleColor.Cyan, ConsoleColor.DarkYellow); + + public static readonly ColorSet CyanOnGray = + new(ConsoleColor.Cyan, ConsoleColor.Gray); + + public static readonly ColorSet CyanOnDarkGray = + new(ConsoleColor.Cyan, ConsoleColor.DarkGray); + + public static readonly ColorSet CyanOnBlue = + new(ConsoleColor.Cyan, ConsoleColor.Blue); + + public static readonly ColorSet CyanOnGreen = + new(ConsoleColor.Cyan, ConsoleColor.Green); + + public static readonly ColorSet CyanOnRed = + new(ConsoleColor.Cyan, ConsoleColor.Red); + + public static readonly ColorSet CyanOnMagenta = + new(ConsoleColor.Cyan, ConsoleColor.Magenta); + + public static readonly ColorSet CyanOnYellow = + new(ConsoleColor.Cyan, ConsoleColor.Yellow); + + public static readonly ColorSet CyanOnWhite = + new(ConsoleColor.Cyan, ConsoleColor.White); + + public static readonly ColorSet CyanOnNone = + new(ConsoleColor.Cyan, null); + + public static readonly ColorSet RedOnBlack = + new(ConsoleColor.Red, ConsoleColor.Black); + + public static readonly ColorSet RedOnDarkBlue = + new(ConsoleColor.Red, ConsoleColor.DarkBlue); + + public static readonly ColorSet RedOnDarkGreen = + new(ConsoleColor.Red, ConsoleColor.DarkGreen); + + public static readonly ColorSet RedOnDarkCyan = + new(ConsoleColor.Red, ConsoleColor.DarkCyan); + + public static readonly ColorSet RedOnDarkRed = + new(ConsoleColor.Red, ConsoleColor.DarkRed); + + public static readonly ColorSet RedOnDarkMagenta = + new(ConsoleColor.Red, ConsoleColor.DarkMagenta); + + public static readonly ColorSet RedOnDarkYellow = + new(ConsoleColor.Red, ConsoleColor.DarkYellow); + + public static readonly ColorSet RedOnGray = + new(ConsoleColor.Red, ConsoleColor.Gray); + + public static readonly ColorSet RedOnDarkGray = + new(ConsoleColor.Red, ConsoleColor.DarkGray); + + public static readonly ColorSet RedOnBlue = + new(ConsoleColor.Red, ConsoleColor.Blue); + + public static readonly ColorSet RedOnGreen = + new(ConsoleColor.Red, ConsoleColor.Green); + + public static readonly ColorSet RedOnCyan = + new(ConsoleColor.Red, ConsoleColor.Cyan); + + public static readonly ColorSet RedOnMagenta = + new(ConsoleColor.Red, ConsoleColor.Magenta); + + public static readonly ColorSet RedOnYellow = + new(ConsoleColor.Red, ConsoleColor.Yellow); + + public static readonly ColorSet RedOnWhite = + new(ConsoleColor.Red, ConsoleColor.White); + + public static readonly ColorSet RedOnNone = + new(ConsoleColor.Red, null); + + public static readonly ColorSet MagentaOnBlack = + new(ConsoleColor.Magenta, ConsoleColor.Black); + + public static readonly ColorSet MagentaOnDarkBlue = + new(ConsoleColor.Magenta, ConsoleColor.DarkBlue); + + public static readonly ColorSet MagentaOnDarkGreen = + new(ConsoleColor.Magenta, ConsoleColor.DarkGreen); + + public static readonly ColorSet MagentaOnDarkCyan = + new(ConsoleColor.Magenta, ConsoleColor.DarkCyan); + + public static readonly ColorSet MagentaOnDarkRed = + new(ConsoleColor.Magenta, ConsoleColor.DarkRed); + + public static readonly ColorSet MagentaOnDarkMagenta = + new(ConsoleColor.Magenta, ConsoleColor.DarkMagenta); + + public static readonly ColorSet MagentaOnDarkYellow = + new(ConsoleColor.Magenta, ConsoleColor.DarkYellow); + + public static readonly ColorSet MagentaOnGray = + new(ConsoleColor.Magenta, ConsoleColor.Gray); + + public static readonly ColorSet MagentaOnDarkGray = + new(ConsoleColor.Magenta, ConsoleColor.DarkGray); + + public static readonly ColorSet MagentaOnBlue = + new(ConsoleColor.Magenta, ConsoleColor.Blue); + + public static readonly ColorSet MagentaOnGreen = + new(ConsoleColor.Magenta, ConsoleColor.Green); + + public static readonly ColorSet MagentaOnCyan = + new(ConsoleColor.Magenta, ConsoleColor.Cyan); + + public static readonly ColorSet MagentaOnRed = + new(ConsoleColor.Magenta, ConsoleColor.Red); + + public static readonly ColorSet MagentaOnYellow = + new(ConsoleColor.Magenta, ConsoleColor.Yellow); + + public static readonly ColorSet MagentaOnWhite = + new(ConsoleColor.Magenta, ConsoleColor.White); + + public static readonly ColorSet MagentaOnNone = + new(ConsoleColor.Magenta, null); + + public static readonly ColorSet YellowOnBlack = + new(ConsoleColor.Yellow, ConsoleColor.Black); + + public static readonly ColorSet YellowOnDarkBlue = + new(ConsoleColor.Yellow, ConsoleColor.DarkBlue); + + public static readonly ColorSet YellowOnDarkGreen = + new(ConsoleColor.Yellow, ConsoleColor.DarkGreen); + + public static readonly ColorSet YellowOnDarkCyan = + new(ConsoleColor.Yellow, ConsoleColor.DarkCyan); + + public static readonly ColorSet YellowOnDarkRed = + new(ConsoleColor.Yellow, ConsoleColor.DarkRed); + + public static readonly ColorSet YellowOnDarkMagenta = + new(ConsoleColor.Yellow, ConsoleColor.DarkMagenta); + + public static readonly ColorSet YellowOnDarkYellow = + new(ConsoleColor.Yellow, ConsoleColor.DarkYellow); + + public static readonly ColorSet YellowOnGray = + new(ConsoleColor.Yellow, ConsoleColor.Gray); + + public static readonly ColorSet YellowOnDarkGray = + new(ConsoleColor.Yellow, ConsoleColor.DarkGray); + + public static readonly ColorSet YellowOnBlue = + new(ConsoleColor.Yellow, ConsoleColor.Blue); + + public static readonly ColorSet YellowOnGreen = + new(ConsoleColor.Yellow, ConsoleColor.Green); + + public static readonly ColorSet YellowOnCyan = + new(ConsoleColor.Yellow, ConsoleColor.Cyan); + + public static readonly ColorSet YellowOnRed = + new(ConsoleColor.Yellow, ConsoleColor.Red); + + public static readonly ColorSet YellowOnMagenta = + new(ConsoleColor.Yellow, ConsoleColor.Magenta); + + public static readonly ColorSet YellowOnWhite = + new(ConsoleColor.Yellow, ConsoleColor.White); + + public static readonly ColorSet YellowOnNone = + new(ConsoleColor.Yellow, null); + + public static readonly ColorSet WhiteOnBlack = + new(ConsoleColor.White, ConsoleColor.Black); + + public static readonly ColorSet WhiteOnDarkBlue = + new(ConsoleColor.White, ConsoleColor.DarkBlue); + + public static readonly ColorSet WhiteOnDarkGreen = + new(ConsoleColor.White, ConsoleColor.DarkGreen); + + public static readonly ColorSet WhiteOnDarkCyan = + new(ConsoleColor.White, ConsoleColor.DarkCyan); + + public static readonly ColorSet WhiteOnDarkRed = + new(ConsoleColor.White, ConsoleColor.DarkRed); + + public static readonly ColorSet WhiteOnDarkMagenta = + new(ConsoleColor.White, ConsoleColor.DarkMagenta); + + public static readonly ColorSet WhiteOnDarkYellow = + new(ConsoleColor.White, ConsoleColor.DarkYellow); + + public static readonly ColorSet WhiteOnGray = + new(ConsoleColor.White, ConsoleColor.Gray); + + public static readonly ColorSet WhiteOnDarkGray = + new(ConsoleColor.White, ConsoleColor.DarkGray); + + public static readonly ColorSet WhiteOnBlue = + new(ConsoleColor.White, ConsoleColor.Blue); + + public static readonly ColorSet WhiteOnGreen = + new(ConsoleColor.White, ConsoleColor.Green); + + public static readonly ColorSet WhiteOnCyan = + new(ConsoleColor.White, ConsoleColor.Cyan); + + public static readonly ColorSet WhiteOnRed = + new(ConsoleColor.White, ConsoleColor.Red); + + public static readonly ColorSet WhiteOnMagenta = + new(ConsoleColor.White, ConsoleColor.Magenta); + + public static readonly ColorSet WhiteOnYellow = + new(ConsoleColor.White, ConsoleColor.Yellow); + + public static readonly ColorSet WhiteOnNone = + new(ConsoleColor.White, null); + + public static readonly ColorSet NoneOnBlack = + new(ConsoleColor.Black); + + public static readonly ColorSet NoneOnDarkBlue = + new(ConsoleColor.DarkBlue); + + public static readonly ColorSet NoneOnDarkGreen = + new(ConsoleColor.DarkGreen); + + public static readonly ColorSet NoneOnDarkCyan = + new(ConsoleColor.DarkCyan); + + public static readonly ColorSet NoneOnDarkRed = + new(ConsoleColor.DarkRed); + + public static readonly ColorSet NoneOnDarkMagenta = + new(ConsoleColor.DarkMagenta); + + public static readonly ColorSet NoneOnDarkYellow = + new(ConsoleColor.DarkYellow); + + public static readonly ColorSet NoneOnGray = + new(ConsoleColor.Gray); + + public static readonly ColorSet NoneOnDarkGray = + new(ConsoleColor.DarkGray); + + public static readonly ColorSet NoneOnBlue = + new(ConsoleColor.Blue); + + public static readonly ColorSet NoneOnGreen = + new(ConsoleColor.Green); + + public static readonly ColorSet NoneOnCyan = + new(ConsoleColor.Cyan); + + public static readonly ColorSet NoneOnRed = + new(ConsoleColor.Red); + + public static readonly ColorSet NoneOnMagenta = + new(ConsoleColor.Magenta); + + public static readonly ColorSet NoneOnYellow = + new(ConsoleColor.Yellow); + + public static readonly ColorSet NoneOnWhite = + new(ConsoleColor.White); +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogEntryCompositeState.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogEntryCompositeState.cs new file mode 100644 index 0000000000..0010a71d4b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogEntryCompositeState.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Console.Internal; +internal readonly struct LogEntryCompositeState +{ + public LogEntryCompositeState( + IReadOnlyCollection>? state, + ActivityTraceId traceId, + ActivitySpanId spanId) + { + State = state; + TraceId = traceId; + SpanId = spanId; + } + + public IReadOnlyCollection>? State { get; } + + public ActivityTraceId TraceId { get; } + + public ActivitySpanId SpanId { get; } +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogFormatter.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogFormatter.cs new file mode 100644 index 0000000000..08081f6455 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogFormatter.cs @@ -0,0 +1,207 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Console; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Console.Internal; + +/// +/// This type is a variation of the official +/// SimpleLogFormatter. +/// +/// +/// Contrary to the reference implementation, (1) it does not support padding and instead uses +/// coordinates (e.g. 0:0:1) similarly to how manages array keys +/// in configuration dictionary. This is arguably more readable especially when understanding +/// relationship between top-and-nested and/or with enumerations. (2), it is much more colorful +/// to increase readability (e.g. less important information is dimmed, yet still present). And +/// (3), it provides knobs to turn individual elements on/off based on developers preference. +/// +internal sealed class LogFormatter : IDisposable +{ + private readonly LogFormatterOptions _options; + private readonly LogFormatterTheme _theme; + + internal TimeProvider TimeProvider { get; set; } = TimeProvider.System; + + public LogFormatter(IOptions options, + IOptions theme) + { + _options = Throw.IfMemberNull(options, options.Value); + _theme = Throw.IfMemberNull(theme, theme.Value); + } + + public void Write(LogEntry logEntry, + IExternalScopeProvider? scopeProvider, + TextWriter textWriter) + { + _ = Throw.IfNull(scopeProvider); + + var writer = Throw.IfNull(textWriter); + var message = logEntry.Formatter?.Invoke(logEntry.State, logEntry.Exception); + + if (_options.IncludeScopes) + { + if (WriteScopes(writer, scopeProvider)) + { + writer.WriteLine(); + } + } + + if (_options.IncludeTimestamp) + { + WriteTimestamp(writer); + writer.WriteSpace(); + } + + if (_options.IncludeLogLevel) + { + WriteLogLevel(writer, logEntry.LogLevel); + } + + if (_options.IncludeTraceId) + { + writer.Write(logEntry.State.TraceId); + writer.WriteSpace(); + } + + if (_options.IncludeSpanId) + { + writer.Write(logEntry.State.SpanId); + writer.WriteSpace(); + } + + if (!string.IsNullOrEmpty(message)) + { + writer.Write(message.Trim()); + writer.WriteSpace(); + } + + if (_options.IncludeCategory) + { + WriteCategory(writer, logEntry.Category, logEntry.EventId); + } + + writer.WriteLine(); + + if (logEntry.Exception != null) + { + WriteException(writer, logEntry.Exception); + writer.WriteLine(); + } + } + + public void Dispose() + { + // Nothing to dispose. + } + + internal bool WriteScopes(TextWriter writer, IExternalScopeProvider scopeProvider) + { + var isOneOrMultipleScopes = false; + var writeScope = WriteScope; + + void WriteScope(object? scope, TextWriter state) + { + if (!isOneOrMultipleScopes) + { + // Unfortunately there is no way how to know upfront if there + // is any scope to iterate over, so formatting has to be done + // from within the for each loop.. + state.WriteLine(); + isOneOrMultipleScopes = true; + } + else + { + writer.WriteSpace(); + } + + writer.Colorize("{{0}}", _theme.Dimmed, scope); + } + + scopeProvider.ForEachScope(writeScope, writer); + + return isOneOrMultipleScopes; + } + + internal void WriteTimestamp(TextWriter writer) + { + var now = TimeProvider.GetUtcNow(); + var dateTime = _options.UseUtcTimestamp ? now.DateTime : now.LocalDateTime; + + writer.Write(dateTime.ToString(_options.TimestampFormat, CultureInfo.InvariantCulture)); + } + + internal void WriteLogLevel(TextWriter writer, LogLevel logLevel) + { + var color = _theme.ColorsEnabled ? logLevel.InColor() : Colors.None; + + writer.Colorize("{({0})}", color, logLevel.InShortString()); + writer.WriteSpace(); + } + + internal void WriteCategory(TextWriter writer, string category, EventId eventId) + { + writer.Colorize("{({0}/{1})}", _theme.Dimmed, category, eventId); + } + + internal void WriteException(TextWriter writer, Exception? e) + { + WriteException(writer, e, new List()); + } + + internal void WriteException(TextWriter writer, Exception? e, IList coordinate) + { + if (e == null) + { + // This should not happen but lets guard against it. + return; + } + + var isTopLevel = coordinate.Count == 0; + if (isTopLevel) + { + coordinate.Add(0); + } + + writer.WriteCoordinate(coordinate, _theme.Dimmed); + writer.Colorize("Exception: {{0} ({1})}", _theme.Exception, e.Message, e.GetType().FullName); + + var isStackTrace = !string.IsNullOrWhiteSpace(e.StackTrace); + if (_options.IncludeExceptionStacktrace && isStackTrace) + { + writer.WriteLine(); + writer.Colorize("{{0}}", _theme.ExceptionStackTrace, e.StackTrace); + } + + if (e is AggregateException ae) + { + for (var i = 0; i < ae.InnerExceptions.Count; i++) + { + coordinate.Add(i); + WriteException(writer, ae.InnerExceptions[i], coordinate); + coordinate.RemoveAt(coordinate.Count - 1); + } + + return; // Aggregate exception contains synthetic inner exception + } + + if (e.InnerException != null) + { + coordinate.Add(0); + WriteException(writer, e.InnerException, coordinate); + coordinate.RemoveAt(coordinate.Count - 1); + } + } +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogFormatterOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogFormatterOptions.cs new file mode 100644 index 0000000000..dab00206d8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogFormatterOptions.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER + +using Microsoft.Extensions.Logging.Console; + +namespace Microsoft.Extensions.Telemetry.Console.Internal; + +/// +/// Options to configure logging formatter. +/// +internal sealed class LogFormatterOptions : ConsoleFormatterOptions +{ + /// + /// Initializes a new instance of the class. + /// + public LogFormatterOptions() + { + IncludeScopes = true; + TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + UseUtcTimestamp = false; + } + + /// + /// Gets or sets a value indicating whether to display timestamp. + /// + /// + /// Default set to . + /// + public bool IncludeTimestamp { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to display log level. + /// + /// + /// Default set to . + /// + public bool IncludeLogLevel { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to display activity TraceId. + /// + /// + /// Default set to . + /// + public bool IncludeTraceId { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to display activity SpanId. + /// + /// + /// Default set to . + /// + public bool IncludeSpanId { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to display category. + /// + /// + /// Default set to . + /// + public bool IncludeCategory { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to display stack trace. + /// + /// + /// Default set to . + /// + public bool IncludeExceptionStacktrace { get; set; } = true; +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogFormatterTheme.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogFormatterTheme.cs new file mode 100644 index 0000000000..4b7a0bfab0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogFormatterTheme.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER + +namespace Microsoft.Extensions.Telemetry.Console.Internal; + +/// +/// Theme for logging formatter. +/// +internal sealed class LogFormatterTheme +{ + /// + /// Gets or sets a value indicating whether colors are enabled or not. + /// + /// Default is . + public bool ColorsEnabled { get; set; } = true; + + /// + /// Gets or sets a value indicating what color to use for dimmed text. + /// + /// Default is . + public ColorSet Dimmed { get; set; } = Colors.DarkGrayOnNone; + + /// + /// Gets or sets a value indicating what color to use for exception text. + /// + /// Default is . + public ColorSet Exception { get; set; } = Colors.RedOnNone; + + /// + /// Gets or sets a value indicating what color to use for exception stack trace. + /// + /// Default is . + public ColorSet ExceptionStackTrace { get; set; } = Colors.DarkRedOnNone; +} + +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogLevelExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogLevelExtensions.cs new file mode 100644 index 0000000000..ae5c1e09d3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/LogLevelExtensions.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Telemetry.Console.Internal; + +/// +/// An internal class of dotnet: +/// SimpleConsoleFormatter. +/// +internal static class LogLevelExtensions +{ + public static string InShortString(this LogLevel logLevel) + { + return logLevel switch + { + LogLevel.Trace => "trce", + LogLevel.Debug => "dbug", + LogLevel.Information => "info", + LogLevel.Warning => "warn", + LogLevel.Error => "eror", + LogLevel.Critical => "crit", + _ => "none" + }; + } + + public static ColorSet InColor(this LogLevel logLevel) + { + return logLevel switch + { + LogLevel.Trace => Colors.GrayOnBlack, + LogLevel.Debug => Colors.GrayOnBlack, + LogLevel.Information => Colors.DarkGreenOnBlack, + LogLevel.Warning => Colors.YellowOnBlack, + LogLevel.Error => Colors.BlackOnDarkRed, + LogLevel.Critical => Colors.WhiteOnDarkRed, + _ => Colors.None + }; + } +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/TextWriterExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/TextWriterExtensions.cs new file mode 100644 index 0000000000..4ccd186701 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/Internal/TextWriterExtensions.cs @@ -0,0 +1,221 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text; + +namespace Microsoft.Extensions.Telemetry.Console.Internal; + +/// +/// Extended version of +/// SimpleConsoleFormatter. +/// +internal static class TextWriterExtensions +{ + private const string ResetForegroundColor = "\x1B[39m\x1B[22m"; + private const string ResetBackgroundColor = "\x1B[49m"; + + private const char Space = ' '; + + private static readonly char[] _digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; + + public static void Colorize(this TextWriter writer, string template, in ColorSet color, params object?[] param) + { + var span = template.AsSpan(); + var templateLength = template.Length; + + int? colorizeBegin = null; + int? colorizeEnd = null; + + static string ReplaceParameters(ReadOnlySpan message, params object?[] parameters) + { + var replaced = new StringBuilder(); + var expanding = false; + var index = -1; + + foreach (var character in message) + { + if (character == '{') + { + expanding = true; + continue; + } + + if (expanding) + { + if (character == '}') + { + if (index >= 0 && + parameters.Length > index) + { + _ = replaced.Append(parameters[index]); + } + + index = -1; + expanding = false; + continue; + } + + if (char.IsNumber(character)) + { + index = character - '0'; + continue; + } + + expanding = false; + } + + _ = replaced.Append(character); + } + + return replaced.ToString(); + } + + for (var i = 0; i < templateLength; i++) + { + if (span[i] == '{') + { + var isLookAheadPossible = i + 1 < templateLength; + if (!isLookAheadPossible) + { + continue; + } + + var isNextDigit = _digits.Contains(span[i + 1]); + if (isNextDigit) + { + continue; + } + + colorizeBegin = i; + } + + if (!colorizeBegin.HasValue) + { + continue; + } + + if (span[i] == '}') + { + var isLookBackPossible = i - 1 > 0; + if (!isLookBackPossible) + { + continue; + } + + var previousIsDigit = _digits.Contains(span[i - 1]); + if (previousIsDigit) + { + continue; + } + + colorizeEnd = i; + } + } + + if (colorizeBegin.HasValue && colorizeEnd.HasValue) + { + if (colorizeBegin.Value > 0) + { + var slice1 = span.Slice(0, colorizeBegin.Value); + writer.Write(ReplaceParameters(slice1, param)); + } + + var slice2 = span.Slice(colorizeBegin.Value + 1, colorizeEnd.Value - colorizeBegin.Value - 1); + writer.WriteColorImpl(ReplaceParameters(slice2, param), color); + + if (colorizeEnd < (templateLength - 1)) + { + var slice3 = span.Slice(colorizeEnd.Value + 1, templateLength - colorizeEnd.Value - 1); + writer.Write(ReplaceParameters(slice3, param)); + } + } + else + { + writer.Write(ReplaceParameters(span, param)); + } + } + + public static void WriteCoordinate(this TextWriter writer, IEnumerable enumerable, in ColorSet color) + { + writer.WriteLine(); + writer.Colorize("{{0}:}", color, string.Join(":", enumerable)); + writer.WriteSpace(); + } + + public static void WriteSpace(this TextWriter writer) => writer.Write(Space); + + private static void WriteColorImpl(this TextWriter writer, T message, in ColorSet colors) + where T : notnull + { + if (colors.Foreground.HasValue) + { + writer.Write(GetForegroundColorEscapeCode(colors.Foreground.Value)); + } + + if (colors.Background.HasValue) + { + writer.Write(GetBackgroundColorEscapeCode(colors.Background.Value)); + } + + writer.Write(message.ToString()); + + if (colors.Background.HasValue) + { + writer.Write(ResetBackgroundColor); + } + + if (colors.Foreground.HasValue) + { + writer.Write(ResetForegroundColor); + } + } + + [ExcludeFromCodeCoverage] + private static string GetForegroundColorEscapeCode(ConsoleColor color) + { + return color switch + { + ConsoleColor.Black => "\x1B[30m", + ConsoleColor.DarkRed => "\x1B[31m", + ConsoleColor.DarkGreen => "\x1B[32m", + ConsoleColor.DarkYellow => "\x1B[33m", + ConsoleColor.DarkBlue => "\x1B[34m", + ConsoleColor.DarkMagenta => "\x1B[35m", + ConsoleColor.DarkCyan => "\x1B[36m", + ConsoleColor.Gray => "\x1B[37m", + ConsoleColor.DarkGray => "\x1B[1m\x1B[30m", + ConsoleColor.Red => "\x1B[1m\x1B[31m", + ConsoleColor.Green => "\x1B[1m\x1B[32m", + ConsoleColor.Yellow => "\x1B[1m\x1B[33m", + ConsoleColor.Blue => "\x1B[1m\x1B[34m", + ConsoleColor.Magenta => "\x1B[1m\x1B[35m", + ConsoleColor.Cyan => "\x1B[1m\x1B[36m", + ConsoleColor.White => "\x1B[1m\x1B[37m", + _ => ResetForegroundColor + }; + } + + [ExcludeFromCodeCoverage] + private static string GetBackgroundColorEscapeCode(ConsoleColor color) + { + return color switch + { + ConsoleColor.Black => "\x1B[40m", + ConsoleColor.DarkRed => "\x1B[41m", + ConsoleColor.DarkGreen => "\x1B[42m", + ConsoleColor.DarkYellow => "\x1B[43m", + ConsoleColor.DarkBlue => "\x1B[44m", + ConsoleColor.DarkMagenta => "\x1B[45m", + ConsoleColor.DarkCyan => "\x1B[46m", + ConsoleColor.Gray => "\x1B[47m", + _ => ResetBackgroundColor + }; + } +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/LoggingConsoleExporter.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/LoggingConsoleExporter.cs new file mode 100644 index 0000000000..7879d8d1f2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/LoggingConsoleExporter.cs @@ -0,0 +1,243 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +#if !NET5_0_OR_GREATER +using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.Pools; +#endif + +using OpenTelemetry; +using OpenTelemetry.Logs; + +#if NET5_0_OR_GREATER +using System.IO; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Telemetry.Console.Internal; +using Microsoft.Shared.Diagnostics; +using MSOptions = Microsoft.Extensions.Options; +#endif + +using static System.Console; + +namespace Microsoft.Extensions.Telemetry.Console; + +[SuppressMessage("Minor Vulnerability", "S2228:Console logging should not be used", Justification = "We need to use here console logging.")] +[SuppressMessage("Major Code Smell", "S106:Standard outputs should not be used directly to log anything", Justification = "We need to use here console logging.")] +internal sealed class LoggingConsoleExporter : BaseExporter +{ + private const string OriginalFormat = "{OriginalFormat}"; + +#if NET5_0_OR_GREATER + private readonly LogFormatter _consoleFormatter; + + public LoggingConsoleExporter(MSOptions.IOptions consoleLogFormatterOptions) + { + var formatterOptions = Throw.IfNullOrMemberNull(consoleLogFormatterOptions, consoleLogFormatterOptions?.Value); + + _consoleFormatter = new LogFormatter( + MSOptions.Options.Create( + new LogFormatterOptions + { + IncludeScopes = formatterOptions.IncludeScopes, + IncludeCategory = formatterOptions.IncludeCategory, + IncludeExceptionStacktrace = formatterOptions.IncludeExceptionStacktrace, + IncludeLogLevel = formatterOptions.IncludeLogLevel, + IncludeTimestamp = formatterOptions.IncludeTimestamp, + IncludeSpanId = formatterOptions.IncludeSpanId, + IncludeTraceId = formatterOptions.IncludeTraceId, + TimestampFormat = formatterOptions.TimestampFormat!, + UseUtcTimestamp = formatterOptions.UseUtcTimestamp + }), + MSOptions.Options.Create( + new LogFormatterTheme + { + ColorsEnabled = formatterOptions.ColorsEnabled, + Dimmed = new ColorSet(formatterOptions.DimmedColor, formatterOptions.DimmedBackgroundColor), + Exception = new ColorSet(formatterOptions.ExceptionColor, formatterOptions.ExceptionBackgroundColor), + ExceptionStackTrace = new ColorSet(formatterOptions.ExceptionStackTraceColor, formatterOptions.ExceptionStackTraceBackgroundColor) + })); + } +#endif + + public override ExportResult Export(in Batch batch) + { +#if NET5_0_OR_GREATER + string? formattedMessage; + + string FormatLog(LogEntryCompositeState compositeState, Exception? exception) => + string.IsNullOrEmpty(formattedMessage) + ? GetOriginalFormat(compositeState.State) + : formattedMessage; + + using var textWriter = new StringWriter(); + foreach (var logRecord in batch) + { + formattedMessage = logRecord.FormattedMessage; + var logEntry = new LogEntry( + logRecord.LogLevel, + logRecord.CategoryName!, + logRecord.EventId, + new LogEntryCompositeState(logRecord.StateValues, logRecord.TraceId, logRecord.SpanId), + logRecord.Exception, + FormatLog); + + var scopedProvider = new LoggerExternalScopeProvider(); + _consoleFormatter.Write(logEntry, scopedProvider, textWriter); + + WriteScopesLine(logRecord); + + var text = textWriter.ToString(); + if (!string.IsNullOrWhiteSpace(text)) + { + Write(text); + } + } + + return ExportResult.Success; +#else + WriteBatchOfLogRecords(batch); + return ExportResult.Success; +#endif + } + +#if NET5_0_OR_GREATER + protected override void Dispose(bool disposing) + { + if (disposing) + { + _consoleFormatter.Dispose(); + } + + base.Dispose(disposing); + } +#endif + + private static string GetOriginalFormat(IReadOnlyCollection>? state) + { + if (state is null) + { + return string.Empty; + } + + foreach (var item in state) + { + if (item.Key == OriginalFormat) + { + return item.Value?.ToString() ?? string.Empty; + } + } + + return string.Empty; + } + +#if !NET5_0_OR_GREATER + private void WriteBatchOfLogRecords(Batch batch) + { + foreach (var logRecord in batch) + { + WriteScopesLine(logRecord); + + var message = logRecord.FormattedMessage; + if (string.IsNullOrEmpty(message)) + { + message = GetOriginalFormat(logRecord.StateValues); + } + + WriteLine($"{logRecord.Timestamp:yyyy-MM-dd HH:mm:ss.fff} {logRecord.LogLevel} {logRecord.TraceId} {logRecord.SpanId} {message} {logRecord.CategoryName}/{logRecord.EventId}"); + + if (logRecord.Exception is not null) + { + var exceptionMessage = GetExceptionMessage(logRecord.Exception); + Write(exceptionMessage); + } + } + } + + private string GetExceptionMessage(Exception ex, string tabulation = "") + { + var result = PoolFactory.SharedStringBuilderPool.Get(); + try + { + var str = $"{tabulation}Exception: {ex.Message} {ex.GetType().FullName}.{Environment.NewLine}"; + _ = result.Append(str); + + if (!string.IsNullOrWhiteSpace(ex.StackTrace)) + { + _ = result.AppendLine(ex.StackTrace); + } + + if (ex is AggregateException ae) + { + for (var i = 0; i < ae.InnerExceptions.Count; i++) + { + _ = result.Append(GetExceptionMessage(ae.InnerExceptions[i], $"{tabulation}\t")); + } + + return result.ToString(); + } + + if (ex.InnerException is not null) + { + _ = result.Append(GetExceptionMessage(ex.InnerException, $"{tabulation}\t")); + } + + return result.ToString(); + } + finally + { + PoolFactory.SharedStringBuilderPool.Return(result); + } + } +#endif + + private void WriteScopesLine(LogRecord logRecord) + { + var isMultipleScopes = false; + + void WriteScope(KeyValuePair scope) + { + var text = string.IsNullOrEmpty(scope.Key) + ? $" {scope.Value}" + : $" {scope.Key}:{scope.Value}"; + + if (!isMultipleScopes) + { + isMultipleScopes = true; + Write($"Scope:{text}"); + } + else + { + Write(text); + } + } + + logRecord.ForEachScope((scope, _) => + { + foreach (var subScope in scope) + { + WriteScope(subScope); + } + }, this); + + if (logRecord.StateValues is not null) + { + foreach (var stateValue in logRecord.StateValues) + { + if (stateValue.Key != OriginalFormat) + { + WriteScope(stateValue); + } + } + } + + if (isMultipleScopes) + { + WriteLine(); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/LoggingConsoleExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/LoggingConsoleExtensions.cs new file mode 100644 index 0000000000..67ea0ac3d7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/LoggingConsoleExtensions.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +#endif +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; +using Microsoft.Shared.Diagnostics; +using OpenTelemetry; +using OpenTelemetry.Logs; + +namespace Microsoft.Extensions.Telemetry.Console; + +/// +/// Console exporter logging extensions for R9 logger. +/// +public static class LoggingConsoleExtensions +{ + /// + /// Adds console exporter as a configuration to the OpenTelemetry ILoggingBuilder. + /// + /// Logging builder where the exporter will be added. + /// The instance of to chain the calls. + public static ILoggingBuilder AddConsoleExporter(this ILoggingBuilder builder) + { + _ = Throw.IfNull(builder); + + builder.Services.TryAddSingleton, LoggingConsoleExporter>(); + +#if NET5_0_OR_GREATER + builder.Services.AddOptions(); +#endif + + return builder.AddProcessor(); + } + +#if NET5_0_OR_GREATER + /// + /// Adds console exporter as a configuration to the OpenTelemetry ILoggingBuilder. + /// + /// Logging builder where the exporter will be added. + /// An action to configure the for console output customization. + /// The instance of to chain the calls. + [Experimental] + public static ILoggingBuilder AddConsoleExporter(this ILoggingBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + builder.Services.TryAddSingleton, LoggingConsoleExporter>(); + _ = builder.Services.Configure(configure); + + return builder.AddProcessor(); + } + + /// + /// Adds console exporter as a configuration to the OpenTelemetry ILoggingBuilder. + /// + /// Logging builder where the exporter will be added. + /// The configuration section to bind for customization of the console output. + /// The instance of to chain the calls. + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(LoggingConsoleOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed by [DynamicDependency]")] + [Experimental] + public static ILoggingBuilder AddConsoleExporter(this ILoggingBuilder builder, IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + + builder.Services.TryAddSingleton, LoggingConsoleExporter>(); + _ = builder.Services + .AddOptions() + .Bind(section); + + return builder.AddProcessor(); + } +#endif +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/LoggingConsoleOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/LoggingConsoleOptions.cs new file mode 100644 index 0000000000..a8a02daf85 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Logging/LoggingConsoleOptions.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Telemetry.Console; + +/// +/// Options to configure console logging formatter. +/// +[Experimental] +public class LoggingConsoleOptions +{ + /// + /// Gets or sets a value indicating whether to display scopes. + /// + /// + /// Defaults to . + /// + public bool IncludeScopes { get; set; } = true; + + /// + /// Gets or sets format string used to format timestamp in logging messages. + /// + /// + /// Defaults to yyyy-MM-dd HH:mm:ss.fff. + /// + public string? TimestampFormat { get; set; } = "yyyy-MM-dd HH:mm:ss.fff"; + + /// + /// Gets or sets a value indicating whether or not UTC timezone should be used for timestamps in logging messages. + /// + /// + /// Defaults to . + /// + public bool UseUtcTimestamp { get; set; } + + /// + /// Gets or sets a value indicating whether to display timestamp. + /// + /// + /// Defaults to . + /// + public bool IncludeTimestamp { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to display log level. + /// + /// + /// Defaults to . + /// + public bool IncludeLogLevel { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to display category. + /// + /// + /// Defaults to . + /// + public bool IncludeCategory { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to display stack trace. + /// + /// + /// Defaults to . + /// + public bool IncludeExceptionStacktrace { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to display activity TraceId. + /// + /// + /// Default set to . + /// + public bool IncludeTraceId { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to display activity SpanId. + /// + /// + /// Default set to . + /// + public bool IncludeSpanId { get; set; } = true; + + /// + /// Gets or sets a value indicating whether colors are enabled or not. + /// + /// Defaults to . + public bool ColorsEnabled { get; set; } = true; + + /// + /// Gets or sets a value indicating what color to use for dimmed text. + /// + /// Defaults to . + public ConsoleColor DimmedColor { get; set; } = ConsoleColor.DarkGray; + + /// + /// Gets or sets a value indicating what color to use for dimmed text background. + /// + public ConsoleColor? DimmedBackgroundColor { get; set; } + + /// + /// Gets or sets a value indicating what color to use for exception text. + /// + /// Defaults to . + public ConsoleColor ExceptionColor { get; set; } = ConsoleColor.Red; + + /// + /// Gets or sets a value indicating what color to use for exception text background. + /// + public ConsoleColor? ExceptionBackgroundColor { get; set; } + + /// + /// Gets or sets a value indicating what color to use for exception stack trace text. + /// + /// Defaults to . + public ConsoleColor ExceptionStackTraceColor { get; set; } = ConsoleColor.DarkRed; + + /// + /// Gets or sets a value indicating what color to use for exception stack trace text background. + /// + public ConsoleColor? ExceptionStackTraceBackgroundColor { get; set; } +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Console/Microsoft.Extensions.Telemetry.Console.csproj b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Microsoft.Extensions.Telemetry.Console.csproj new file mode 100644 index 0000000000..0da3fa2836 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Console/Microsoft.Extensions.Telemetry.Console.csproj @@ -0,0 +1,32 @@ + + + Microsoft.Extensions.Telemetry.Console + Telemetry exporters targetting the console. + Telemetry + + + + true + true + true + true + true + + + + normal + 100 + 99 + 90 + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogCollector.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogCollector.cs new file mode 100644 index 0000000000..9abb6c773f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogCollector.cs @@ -0,0 +1,138 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable R9A052 + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Testing.Logging; + +/// +/// Collects log records sent to the fake logger. +/// +[DebuggerDisplay("Count = {Count}, LatestRecord = {LatestRecord}")] +[DebuggerTypeProxy(typeof(FakeLogCollectorDebugView))] +public class FakeLogCollector +{ + private readonly List _records = new(); + private readonly FakeLogCollectorOptions _options; + + /// + /// Initializes a new instance of the class. + /// + /// The options to control which log records to retain. + public FakeLogCollector(IOptions options) + { + _options = Throw.IfNullOrMemberNull(options, options?.Value); + } + + /// + /// Initializes a new instance of the class. + /// + public FakeLogCollector() + { + _options = new FakeLogCollectorOptions(); + } + + /// + /// Creates a new instance of the class. + /// + /// The options to control which log records to retain. + /// The collector. + public static FakeLogCollector Create(FakeLogCollectorOptions options) + { + return new FakeLogCollector(Options.Options.Create(Throw.IfNull(options))); + } + + /// + /// Removes all accumulated log records from the collector. + /// + public void Clear() + { + lock (_records) + { + _records.Clear(); + } + } + + /// + /// Gets the records that are held by the collector. + /// + /// + /// The list of records tracked to date by the collector. + /// + [SuppressMessage("Minor Code Smell", "S4049:Properties should be preferred", Justification = "Not suitable for a property since it allocates.")] + public IReadOnlyList GetSnapshot() + { + lock (_records) + { + return _records.ToArray(); + } + } + + /// + /// Gets the latest record that was created. + /// + /// + /// The latest log record created. + /// + /// When no records have been captured. + public FakeLogRecord LatestRecord + { + get + { + lock (_records) + { + if (_records.Count == 0) + { + Throw.InvalidOperationException("No records logged."); + } + + return _records[_records.Count - 1]; + } + } + } + + /// + /// Gets the number of log records captured by this collector. + /// + public int Count => _records.Count; + + internal void AddRecord(FakeLogRecord record) + { + if (_options.FilteredLevels.Count > 0 && !_options.FilteredLevels.Contains(record.Level)) + { + // level not being collected + return; + } + + if (_options.FilteredCategories.Count > 0) + { + if (record.Category == null || !_options.FilteredCategories.Contains(record.Category)) + { + // no category specified, or not in the list of allowed categories + return; + } + } + + if (!record.LevelEnabled && !_options.CollectRecordsForDisabledLogLevels) + { + // record is not enabled and we're not collecting disabled records + return; + } + + lock (_records) + { + _records.Add(record); + } + + _options.OutputSink?.Invoke(_options.OutputFormatter(record)); + } + + internal TimeProvider TimeProvider => _options.TimeProvider; +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogCollectorDebugView.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogCollectorDebugView.cs new file mode 100644 index 0000000000..a16da2c175 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogCollectorDebugView.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Testing.Logging; + +[ExcludeFromCodeCoverage/* (Justification = "Only used in debugger") */] +internal sealed class FakeLogCollectorDebugView +{ + private readonly FakeLogCollector _collector; + + public FakeLogCollectorDebugView(FakeLogCollector collector) + { + _collector = Throw.IfNull(collector); + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public IReadOnlyList Records => _collector.GetSnapshot(); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogCollectorOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogCollectorOptions.cs new file mode 100644 index 0000000000..25c01cb4b1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogCollectorOptions.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; + +#pragma warning disable CA2227 // Collection properties should be read only + +namespace Microsoft.Extensions.Telemetry.Testing.Logging; + +/// +/// Options for the fake logger. +/// +public class FakeLogCollectorOptions +{ + /// + /// Gets or sets the logger categories for whom records are collected. + /// + /// + /// Defaults to an empty set, which doesn't filter any records. + /// If not empty, only records coming from loggers in these categories will be collected by the fake logger. + /// + public ISet FilteredCategories { get; set; } = new HashSet(); + + /// + /// Gets or sets the logging levels for whom records are collected. + /// + /// + /// Defaults to an empty set, which doesn't filter any records. + /// If not empty, only records with the given level will be collected by the fake logger. + /// + public ISet FilteredLevels { get; set; } = new HashSet(); + + /// + /// Gets or sets a value indicating whether to collect records that are logged when the associated log level is currently disabled. + /// + /// + /// Defaults to . + /// + public bool CollectRecordsForDisabledLogLevels { get; set; } = true; + + /// + /// Gets or sets the time provider to use when time-stamping log records. + /// + /// + /// Defaults to . + /// + public TimeProvider TimeProvider { get; set; } = TimeProvider.System; + + /// + /// Gets or sets an output sink where every record harvested by the collector is sent. + /// + /// + /// By setting this property, you can have all log records harvested by the collector be copied somewhere convenient. + /// Defaults to . + /// + public Action? OutputSink { get; set; } + + /// + /// Gets or sets a delegate that is used to format log records in a specialized way before sending them to the registered output sink. + /// + public Func OutputFormatter { get; set; } = FakeLogRecord.Formatter; +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogRecord.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogRecord.cs new file mode 100644 index 0000000000..055fcef040 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogRecord.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Testing.Logging; + +/// +/// A single log record tracked by . +/// +public class FakeLogRecord +{ + /// + /// Initializes a new instance of the class. + /// + /// The level used when producing the log record. + /// The id representing the specific log statement. + /// The opaque state supplied by the caller when creating the log record. + /// An optional exception associated with the log record. + /// The formatted message text for the record. + /// List of active scopes active for this log record. + /// The optional category for this record, which corresponds to the T in . + /// Whether the log level was enabled or not when the method was called. + /// The time at which the log record was created. +#pragma warning disable S107 // Methods should not have too many parameters + public FakeLogRecord(LogLevel level, EventId id, object? state, Exception? exception, string message, IReadOnlyList scopes, string? category, bool enabled, DateTimeOffset timestamp) +#pragma warning restore S107 // Methods should not have too many parameters + { + Level = level; + Id = id; + State = state; + Exception = exception; + Message = Throw.IfNull(message); + Scopes = Throw.IfNull(scopes); + Category = category; + LevelEnabled = enabled; + Timestamp = timestamp; + } + + /// + /// Gets the level used when producing the log record. + /// + public LogLevel Level { get; } + + /// + /// Gets the id representing the specific log statement. + /// + public EventId Id { get; } + + /// + /// Gets the opaque state supplied by the caller when creating the log record. + /// + public object? State { get; } + + /// + /// Gets the opaque state supplied by the caller when creating the log record as a read-only list. + /// + /// + /// When logging using the R9 logging model, the arguments you supply to the logging method are packaged into + /// a single state object which is delivered to the + /// method. This state can be retrieved as a set of name/value pairs encoded in a read-only list. + /// + /// The object returned by this property is the same as what returns, except it has been cast to a + /// read-only list. + /// + /// If the state object was not generated by the R9 logging model and is not a read-only list. + public IReadOnlyList>? StructuredState => (IReadOnlyList>?)State; + + /// + /// Gets an optional exception associated with the log record. + /// + public Exception? Exception { get; } + + /// + /// Gets the formatted message text for the record. + /// + public string Message { get; } + + /// + /// Gets the logging scopes active when the log record was created. + /// + public IReadOnlyList Scopes { get; } + + /// + /// Gets the optional category of this record. + /// + /// + /// The category corresponds to the T value in . + /// + public string? Category { get; } + + /// + /// Gets a value indicating whether the log level was enabled or disabled when this record was collected. + /// + public bool LevelEnabled { get; } + + /// + /// Gets the time at which the log record was created. + /// + public DateTimeOffset Timestamp { get; } + + /// + /// Returns a string representing this object. + /// + /// A string that helps identity this object. + public override string ToString() => Formatter(this); + + internal static string Formatter(FakeLogRecord record) + { + // these strings are kept to the same length so the output lines up nicely + var level = record.Level switch + { + LogLevel.Debug => "debug", + LogLevel.Information => " info", + LogLevel.Warning => " warn", + LogLevel.Error => "error", + LogLevel.Critical => " crit", + LogLevel.Trace => "trace", + LogLevel.None => " none", + _ => "invld", + }; + + return $"[{record.Timestamp:mm:ss.fff}, {level}] {record.Message}"; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogger.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogger.cs new file mode 100644 index 0000000000..259f88d8d2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLogger.cs @@ -0,0 +1,165 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Testing.Logging; + +/// +/// A logger which captures everything logged to it and enables inspection. +/// +/// +/// This type is intended for use in unit tests. It captures all the log state to memory and lets you inspect it +/// to validate that your code is logging what it should. +/// +public class FakeLogger : ILogger +{ + private readonly ConcurrentDictionary _disabledLevels = new(); // used as a set, the value is ignored + + /// + /// Initializes a new instance of the class. + /// + /// Where to push all log state. + /// The logger's category, which indicates the origin of the logger and is captured in each record. + public FakeLogger(FakeLogCollector? collector = null, string? category = null) + { + Collector = collector ?? new FakeLogCollector(); + Category = string.IsNullOrEmpty(category) ? null : category; + } + + /// + /// Initializes a new instance of the class that copies all log records to the given output sink. + /// + /// Where to emit individual log records. + /// The logger's category, which indicates the origin of the logger and is captured in each record. + public FakeLogger(Action outputSink, string? category = null) + : this(FakeLogCollector.Create(new FakeLogCollectorOptions { OutputSink = outputSink }), category) + { + } + + /// + /// Begins a logical operation scope. + /// + /// The type of the state to begin scope for. + /// The identifier for the scope. + /// A disposable object that ends the logical operation scope on dispose. +#pragma warning disable CS8633 +#pragma warning disable CS8766 +#pragma warning disable R9A049 + public IDisposable? BeginScope(TState state) + where TState : notnull => ScopeProvider.Push(state); +#pragma warning restore CS8633 +#pragma warning restore CS8766 +#pragma warning restore R9A049 + + /// + /// Creates a new log record. + /// + /// The type of the object to be written. + /// Entry will be written on this level. + /// Id of the event. + /// The entry to be written. Can be also an object. + /// The exception related to this entry. + /// Function to create a string message of the state and exception. + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + _ = Throw.IfNull(formatter); + + var l = new List(); + ScopeProvider.ForEachScope((scopeState, list) => list.Add(scopeState), l); + + var record = new FakeLogRecord(logLevel, eventId, ConsumeTState(state), exception, formatter(state, exception), + l.ToArray(), Category, !_disabledLevels.ContainsKey(logLevel), Collector.TimeProvider.GetUtcNow()); + Collector.AddRecord(record); + } + + /// + /// Controls the enabled state of a log level. + /// + /// The log level to affect. + /// Whether the log level is enabled or not. + public void ControlLevel(LogLevel logLevel, bool enabled) => _ = enabled ? _disabledLevels.TryRemove(logLevel, out _) : _disabledLevels.TryAdd(logLevel, false); + + /// + /// Checks if the given log level is enabled. + /// + /// Level to be checked. + /// if enabled; otherwise. + public bool IsEnabled(LogLevel logLevel) => !_disabledLevels.ContainsKey(logLevel); + + /// + /// Gets the logger collector associated with this logger, as specified when the logger was created. + /// + public FakeLogCollector Collector { get; } + + /// + /// Gets the latest record logged to this logger. + /// + /// + /// This is a convenience property that merely returns the latest record from the underlying collector. + /// + /// When no records have been captured. + public FakeLogRecord LatestRecord => Collector.LatestRecord; + + /// + /// Gets this logger's category, as specified when the logger was created. + /// + public string? Category { get; } + + internal IExternalScopeProvider ScopeProvider { get; set; } = new LoggerExternalScopeProvider(); + + private static object? ConsumeTState(object? state) + { + if (state is IEnumerable> e) + { + var l = new List>(); + foreach (var pair in e) + { + if (pair.Value != null) + { + l.Add(new KeyValuePair(pair.Key, ConvertToString(pair))); + } + else + { + l.Add(new KeyValuePair(pair.Key, null)); + } + } + + return l; + } + + if (state == null) + { + return null; + } + + // the best we can do here is stringify the thing + return Convert.ToString(state, CultureInfo.InvariantCulture); + + static string? ConvertToString(KeyValuePair pair) + { + string? str; + if (pair.Value is string s) + { + str = s; + } + else if (pair.Value is IEnumerable ve) + { + str = LogMethodHelper.Stringify(ve); + } + else + { + str = Convert.ToString(pair.Value, CultureInfo.InvariantCulture); + } + + return str; + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLoggerExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLoggerExtensions.cs new file mode 100644 index 0000000000..1de6eb7ad5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLoggerExtensions.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Testing.Logging; + +/// +/// Extensions for configuring fake logging, used in unit tests. +/// +public static class FakeLoggerExtensions +{ + /// + /// Configure fake logging. + /// + /// Logging builder. + /// Configuration section that contains . + /// Logging . + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(FakeLogCollectorOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed by [DynamicDependency]")] + public static ILoggingBuilder AddFakeLogging(this ILoggingBuilder builder, IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + _ = builder.Services.Configure(section); + _ = builder.Services.AddSingleton(); + + return builder; + } + + /// + /// Configure fake logging. + /// + /// Logging builder. + /// Logging configuration options. + /// Logging . + public static ILoggingBuilder AddFakeLogging(this ILoggingBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + _ = builder.Services.Configure(configure); + _ = builder.Services.AddSingleton(); + + return builder; + } + + /// + /// Configure fake logging with default options. + /// + /// Logging builder. + /// Logging . + public static ILoggingBuilder AddFakeLogging(this ILoggingBuilder builder) => builder.AddFakeLogging(_ => { }); + + /// + /// Configure fake logging. + /// + /// Service collection. + /// Configuration section that contains . + /// Service collection for API chaining. + public static IServiceCollection AddFakeLogging(this IServiceCollection services, IConfigurationSection section) => services.AddLogging(x => x.AddFakeLogging(section)); + + /// + /// Configure fake logging. + /// + /// Service collection. + /// Logging configuration options. + /// Service collection for API chaining. + public static IServiceCollection AddFakeLogging(this IServiceCollection services, Action configure) => services.AddLogging(x => x.AddFakeLogging(configure)); + + /// + /// Configure fake logging with default options. + /// + /// Service collection. + /// Service collection for API chaining. + public static IServiceCollection AddFakeLogging(this IServiceCollection services) => services.AddLogging(builder => builder.AddFakeLogging()); + + /// + /// Gets the object that collects log records sent to the fake logger. + /// + /// The service provider containing the logger. + /// When no collector exists in the provider. + /// The collector which tracks records logged to fake loggers. + public static FakeLogCollector GetFakeLogCollector(this IServiceProvider services) + => services.GetService() ?? throw new InvalidOperationException("No fake log collector registered"); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLoggerProvider.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLoggerProvider.cs new file mode 100644 index 0000000000..116b1deed6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLoggerProvider.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Testing.Logging; + +// bug in .NET 7 preview analyzer +#pragma warning disable CA1063 + +/// +/// A provider of fake loggers. +/// +[ProviderAlias("Fake")] +public class FakeLoggerProvider : ILoggerProvider, ISupportExternalScope +{ + private readonly ConcurrentDictionary _loggers = new(); + private IExternalScopeProvider _scopeProvider = new LoggerExternalScopeProvider(); + + /// + /// Initializes a new instance of the class. + /// + /// The collector that will receive all log records emitted to fake loggers. + public FakeLoggerProvider(FakeLogCollector? collector = null) + { + Collector = collector ?? new FakeLogCollector(); + } + + /// + /// Finalizes an instance of the class. + /// + [ExcludeFromCodeCoverage] + ~FakeLoggerProvider() + { + Dispose(false); + } + + /// + /// Sets external scope information source for logger provider. + /// + /// The provider of scope data. + public void SetScopeProvider(IExternalScopeProvider scopeProvider) + { + _scopeProvider = Throw.IfNull(scopeProvider); + + foreach (var entry in _loggers) + { + entry.Value.ScopeProvider = _scopeProvider; + } + } + + /// + /// Creates a new instance. + /// + /// The category name for messages produced by the logger. + /// The instance of that was created. + ILogger ILoggerProvider.CreateLogger(string categoryName) => CreateLogger(categoryName); + + /// + /// Creates a new instance. + /// + /// The category name for messages produced by the logger. + /// The instance of that was created. + public FakeLogger CreateLogger(string? categoryName) + { + return _loggers.GetOrAdd(categoryName ?? string.Empty, (name) => new(Collector, name) + { + ScopeProvider = _scopeProvider, + }); + } + + /// + /// Clean up resources held by this object. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Gets the log record collector for all loggers created by this provider. + /// + public FakeLogCollector Collector { get; } + + /// + /// Clean up resources held by this object. + /// + /// when called from the method, when called from a finalizer. + protected virtual void Dispose(bool disposing) + { + } + + internal string FirstLoggerName => _loggers.First().Key; +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLoggerT.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLoggerT.cs new file mode 100644 index 0000000000..1a1be65d4a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Logging/FakeLoggerT.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Telemetry.Testing.Logging; + +#pragma warning disable SA1649 // File name should match first type name + +/// +/// A logger which captures everything logged to it and enables inspection. +/// +/// +/// This type is intended for use in unit tests. It captures all the log state to memory and lets you inspect it +/// to validate that your code is logging what it should. +/// +/// The type whose name to use as a logger category. +#pragma warning disable CS8633 +#pragma warning disable CS8766 +public sealed class FakeLogger : FakeLogger, ILogger +#pragma warning restore CS8633 +#pragma warning restore CS8766 +{ + /// + /// Initializes a new instance of the class. + /// + /// Where to push all log state. + public FakeLogger(FakeLogCollector? collector = null) + : base(collector, GetNiceNameOfT()) + { + } + + /// + /// Initializes a new instance of the class that copies all log records to the given output sink. + /// + /// Where to emit individual log records. + public FakeLogger(Action outputSink) + : this(FakeLogCollector.Create(new FakeLogCollectorOptions { OutputSink = outputSink })) + { + } + + private static string GetNiceNameOfT() + { + // we do all this stuff just to get the nice generated name for "T" that LoggerFactory takes care of. + using var provider = new FakeLoggerProvider(); + using var factory = new LoggerFactory(); + factory.AddProvider(provider); + _ = factory.CreateLogger(); + return provider.FirstLoggerName; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/Internal/AggregationType.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/Internal/AggregationType.cs new file mode 100644 index 0000000000..aa7e4fa783 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/Internal/AggregationType.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Telemetry.Testing.Metering.Internal; + +internal enum AggregationType +{ + Save, + Aggregate, + SaveOrUpdate +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricCollector.MeterListener.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricCollector.MeterListener.cs new file mode 100644 index 0000000000..ef8514cf8f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricCollector.MeterListener.cs @@ -0,0 +1,152 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Linq; +using Microsoft.Extensions.Telemetry.Testing.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metering.Internal; + +namespace Microsoft.Extensions.Telemetry.Testing.Metering; + +public partial class MetricCollector +{ + private void OnInstrumentPublished(Instrument instrument, MeterListener listener) + { + var matchedByMeter = _meter is not null && ReferenceEquals(instrument.Meter, _meter); + var matchedByMeterName = _meter is null && (_meterNames!.Length == 0 || _meterNames!.Any(x => x.StartsWith(instrument.Meter.Name, StringComparison.Ordinal))); + + if (matchedByMeter || matchedByMeterName) + { + RegisterInstrument(instrument); + + listener.EnableMeasurementEvents(instrument, this); + } + } + + private void CollectMeasurement(Instrument instrument, T value, ReadOnlySpan> tags, object? state) + where T : struct + { + if (state == this) + { + var metricValuesHolder = GetMetricValuesHolder(instrument); + metricValuesHolder.ReceiveValue(value, tags); + } + } + + private void RegisterInstrument(Instrument instrument) + { + Type genericDefinedType = instrument.GetType().GetGenericTypeDefinition(); + + AggregationType aggregationType = AggregationType.Save; + Dictionary allValuesDictionary = null!; + + if (genericDefinedType == typeof(Counter<>)) + { + aggregationType = AggregationType.Aggregate; + allValuesDictionary = _allCounters; + } + else if (genericDefinedType == typeof(Histogram<>)) + { + aggregationType = AggregationType.Save; + allValuesDictionary = _allHistograms; + } + else if (genericDefinedType == typeof(UpDownCounter<>)) + { + aggregationType = AggregationType.Aggregate; + allValuesDictionary = _allUpDownCounters; + } + else if (genericDefinedType == typeof(ObservableCounter<>)) + { + aggregationType = AggregationType.SaveOrUpdate; + allValuesDictionary = _allObservableCounters; + } + else if (genericDefinedType == typeof(ObservableGauge<>)) + { + aggregationType = AggregationType.Save; + allValuesDictionary = _allObservableGauges; + } + else if (genericDefinedType == typeof(ObservableUpDownCounter<>)) + { + aggregationType = AggregationType.SaveOrUpdate; + allValuesDictionary = _allObservableUpDownCounters; + } + + Type measurementValueType = instrument.GetType().GetGenericArguments()[0]; + + if (measurementValueType == typeof(int)) + { + var metricsValuesDictionary = (ConcurrentDictionary>)allValuesDictionary[typeof(int)]; + _ = metricsValuesDictionary.GetOrAdd(instrument.Name, new MetricValuesHolder(_timeProvider, aggregationType, instrument.Name)); + } + else if (measurementValueType == typeof(byte)) + { + var metricsValuesDictionary = (ConcurrentDictionary>)allValuesDictionary[typeof(byte)]; + _ = metricsValuesDictionary.GetOrAdd(instrument.Name, new MetricValuesHolder(_timeProvider, aggregationType, instrument.Name)); + } + else if (measurementValueType == typeof(short)) + { + var metricsValuesDictionary = (ConcurrentDictionary>)allValuesDictionary[typeof(short)]; + _ = metricsValuesDictionary.GetOrAdd(instrument.Name, new MetricValuesHolder(_timeProvider, aggregationType, instrument.Name)); + } + else if (measurementValueType == typeof(long)) + { + var metricsValuesDictionary = (ConcurrentDictionary>)allValuesDictionary[typeof(long)]; + _ = metricsValuesDictionary.GetOrAdd(instrument.Name, new MetricValuesHolder(_timeProvider, aggregationType, instrument.Name)); + } + else if (measurementValueType == typeof(double)) + { + var metricsValuesDictionary = (ConcurrentDictionary>)allValuesDictionary[typeof(double)]; + _ = metricsValuesDictionary.GetOrAdd(instrument.Name, new MetricValuesHolder(_timeProvider, aggregationType, instrument.Name)); + } + else if (measurementValueType == typeof(float)) + { + var metricsValuesDictionary = (ConcurrentDictionary>)allValuesDictionary[typeof(float)]; + _ = metricsValuesDictionary.GetOrAdd(instrument.Name, new MetricValuesHolder(_timeProvider, aggregationType, instrument.Name)); + } + else if (measurementValueType == typeof(decimal)) + { + var metricsValuesDictionary = (ConcurrentDictionary>)allValuesDictionary[typeof(decimal)]; + _ = metricsValuesDictionary.GetOrAdd(instrument.Name, new MetricValuesHolder(_timeProvider, aggregationType, instrument.Name)); + } + } + + private MetricValuesHolder GetMetricValuesHolder(Instrument instrument) + where T : struct + { + var instrumentType = instrument.GetType(); + + Dictionary allValuesDictionary = null!; + + if (instrumentType == typeof(Counter)) + { + allValuesDictionary = _allCounters; + } + else if (instrumentType == typeof(Histogram)) + { + allValuesDictionary = _allHistograms; + } + else if (instrumentType == typeof(UpDownCounter)) + { + allValuesDictionary = _allUpDownCounters; + } + else if (instrumentType == typeof(ObservableCounter)) + { + allValuesDictionary = _allObservableCounters; + } + else if (instrumentType == typeof(ObservableGauge)) + { + allValuesDictionary = _allObservableGauges; + } + else if (instrumentType == typeof(ObservableUpDownCounter)) + { + allValuesDictionary = _allObservableUpDownCounters; + } + + var metricsValuesDictionary = (ConcurrentDictionary>)allValuesDictionary[typeof(T)]; + + return metricsValuesDictionary[instrument.Name]; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricCollector.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricCollector.cs new file mode 100644 index 0000000000..3432d4d83e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricCollector.cs @@ -0,0 +1,395 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; +using System.Linq; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Testing.Metering; + +/// +/// The helper class to automatically capture metering data that has been recorded +/// by instruments created by . +/// +/// +/// This type has been designed to be used only for testing purposes. +/// +[Experimental] +public partial class MetricCollector : IDisposable +{ + private readonly MeterListener _listener; + private readonly string[]? _meterNames; +#pragma warning disable CA2213 // Disposable fields should be disposed + private readonly Meter? _meter; +#pragma warning restore CA2213 // Disposable fields should be disposed + private readonly TimeProvider _timeProvider; + + private readonly Dictionary _allCounters; + private readonly Dictionary _allHistograms; + private readonly Dictionary _allUpDownCounters; + private readonly Dictionary _allObservableCounters; + private readonly Dictionary _allObservableGauges; + private readonly Dictionary _allObservableUpDownCounters; + + /// + /// Initializes a new instance of the class. + /// + /// The names of .NET to capture measurements from. + /// The instance. + /// + /// This constructor is applicable for the scenario when metering data generated + /// by active instances which matches + /// the one from the list is to be captured. + /// + public MetricCollector(IEnumerable meterNames, TimeProvider? timeProvider = null) + : this(meterNames, null, timeProvider, false) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The instance. + /// + /// This constructor is applicable for the scenario when metering data + /// generated by all the active instances + /// in the application is to be captured. + /// + public MetricCollector(TimeProvider? timeProvider = null) + : this(Array.Empty(), null, timeProvider, false) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The instance to capture metering data. + /// The instance. + /// + /// This constructor is applicable for the scenario when metering data + /// generated by a specific instance is to be captured. + /// + public MetricCollector(Meter meter, TimeProvider? timeProvider = null) + : this(null, meter, timeProvider, true) + { + } + + private MetricCollector(IEnumerable? meterNames, Meter? meter, TimeProvider? timeProvider, bool applyMeterFiltering) + { + if (applyMeterFiltering) + { + _meter = Throw.IfNull(meter); + } + else + { + _meterNames = Throw.IfNull(meterNames).ToArray(); + } + + _timeProvider = timeProvider ?? TimeProvider.System; + + _listener = new MeterListener + { + InstrumentPublished = OnInstrumentPublished + }; + + _listener.SetMeasurementEventCallback(CollectMeasurement); + _listener.SetMeasurementEventCallback(CollectMeasurement); + _listener.SetMeasurementEventCallback(CollectMeasurement); + _listener.SetMeasurementEventCallback(CollectMeasurement); + _listener.SetMeasurementEventCallback(CollectMeasurement); + _listener.SetMeasurementEventCallback(CollectMeasurement); + _listener.SetMeasurementEventCallback(CollectMeasurement); + + _allCounters = CreateMetricsValuesDictionary(); + _allHistograms = CreateMetricsValuesDictionary(); + _allUpDownCounters = CreateMetricsValuesDictionary(); + _allObservableGauges = CreateMetricsValuesDictionary(); + _allObservableCounters = CreateMetricsValuesDictionary(); + _allObservableUpDownCounters = CreateMetricsValuesDictionary(); + + _listener.Start(); + } + + /// + /// Clears all the captured metering data. + /// + public void Clear() + { + ClearValues(_allCounters); + ClearValues(_allHistograms); + ClearValues(_allUpDownCounters); + ClearValues(_allObservableCounters); + ClearValues(_allObservableGauges); + ClearValues(_allObservableUpDownCounters); + } + + /// + /// Gets the object containing all the captured metering data that has been recorded by a counter instrument. + /// + /// The type of metric measurement value. + /// The metric name. + /// + /// The instance or if a counter with given was not found. + /// + public MetricValuesHolder? GetCounterValues(string counterName) + where T : struct => GetInstrumentCapturedData(_allCounters, counterName); + + /// + /// Gets the object containing all the captured metering data that has been recorded by a histogram instrument. + /// + /// The type of metric measurement value. + /// The metric name. + /// + /// The instance or if a histogram with given was not found. + /// + public MetricValuesHolder? GetHistogramValues(string histogramName) + where T : struct => GetInstrumentCapturedData(_allHistograms, histogramName); + + /// + /// Gets the object containing all the captured metering data that has been recorded by an updown counter instrument. + /// + /// The type of metric measurement value. + /// The metric name. + /// + /// The instance or if an updown counter with given was not found. + /// + public MetricValuesHolder? GetUpDownCounterValues(string updownCounterName) + where T : struct => GetInstrumentCapturedData(_allUpDownCounters, updownCounterName); + + /// + /// Gets the object containing all the captured metering data that has been recorded by an observable gauge instrument. + /// + /// The type of metric measurement value. + /// The metric name. + /// + /// The instance or if an observable gauge with given was not found. + /// + public MetricValuesHolder? GetObservableGaugeValues(string observableGaugeName) + where T : struct => GetInstrumentCapturedData(_allObservableGauges, observableGaugeName); + + /// + /// Gets the object containing all the captured metering data that has been recorded by an observable counter instrument. + /// + /// The type of metric measurement value. + /// The metric name. + /// + /// The instance or if an observable counter with given was not found. + /// + public MetricValuesHolder? GetObservableCounterValues(string observableCounterName) + where T : struct => GetInstrumentCapturedData(_allObservableCounters, observableCounterName); + + /// + /// Gets the object containing all the captured metering data that has been recorded by an observable updown counter instrument. + /// + /// The type of metric measurement value. + /// The metric name. + /// + /// The instance or if an observable updown counter with given was not found. + /// + public MetricValuesHolder? GetObservableUpDownCounterValues(string observableUpDownCounterName) + where T : struct => GetInstrumentCapturedData(_allObservableUpDownCounters, observableUpDownCounterName); + + /// + /// Gets a measurement value recorded by a counter instrument. + /// + /// The type of metric measurement value. + /// The metric name. + /// The dimensions collection describing the measurement value. + /// The measurement value or if the value was not recorded. + public T? GetCounterValue(string counterName, params KeyValuePair[] tags) + where T : struct => GetCounterValues(counterName)?.GetValue(tags); + + /// + /// Gets a measurement value recorded by a histogram instrument. + /// + /// The type of metric measurement value. + /// The metric name. + /// The dimensions collection describing the measurement value. + /// The measurement value or if the value was not recorded. + public T? GetHistogramValue(string histogramName, params KeyValuePair[] tags) + where T : struct => GetHistogramValues(histogramName)?.GetValue(tags); + + /// + /// Gets a measurement value recorded by an updown counter instrument. + /// + /// The type of metric measurement value. + /// The metric name. + /// The dimensions collection describing the measurement value. + /// The measurement value or if the value was not recorded. + public T? GetUpDownCounterValue(string updownCounterName, params KeyValuePair[] tags) + where T : struct => GetUpDownCounterValues(updownCounterName)?.GetValue(tags); + + /// + /// Gets a measurement value recorded by an observable counter instrument. + /// + /// The type of metric measurement value. + /// The metric name. + /// The dimensions collection describing the measurement value. + /// The measurement value or if the value was not recorded. + public T? GetObservableCounterValue(string observableCounterName, params KeyValuePair[] tags) + where T : struct => GetObservableCounterValues(observableCounterName)?.GetValue(tags); + + /// + /// Gets a measurement value recorded by an observable gauge instrument. + /// + /// The type of metric measurement value. + /// The metric name. + /// The dimensions collection describing the measurement value. + /// The measurement value or if the value was not recorded. + public T? GetObservableGaugeValue(string observableGaugeName, params KeyValuePair[] tags) + where T : struct => GetObservableGaugeValues(observableGaugeName)?.GetValue(tags); + + /// + /// Gets a measurement value recorded by an observable updown counter instrument. + /// + /// The type of metric measurement value. + /// The metric name. + /// The dimensions collection describing the measurement value. + /// The measurement value or if the value was not recorded. + public T? GetObservableUpDownCounterValue(string observableUpDownCounterName, params KeyValuePair[] tags) + where T : struct => GetObservableUpDownCounterValues(observableUpDownCounterName)?.GetValue(tags); + +#pragma warning disable S4049 // Properties should be preferred + /// + /// Gets a list of all counters registered with this metrics collector. + /// + /// The type of metric measurement value. + /// Read only dictionary of . + public IReadOnlyDictionary>? GetAllCounters() + where T : struct => GetAllInstruments(_allCounters); + + /// + /// Gets a list of all UpDown counters registered with this metrics collector. + /// + /// The type of metric measurement value. + /// Read only dictionary of . + public IReadOnlyDictionary>? GetAllUpDownCounters() + where T : struct => GetAllInstruments(_allUpDownCounters); + + /// + /// Gets a list of all histograms registered with this metrics collector. + /// + /// The type of metric measurement value. + /// Read only dictionary of . + public IReadOnlyDictionary>? GetAllHistograms() + where T : struct => GetAllInstruments(_allHistograms); + + /// + /// Gets a list of all observable counters registered with this metrics collector. + /// + /// The type of metric measurement value. + /// Read only dictionary of . + public IReadOnlyDictionary>? GetAllObservableCounters() + where T : struct => GetAllInstruments(_allObservableCounters); + + /// + /// Gets a list of all observable UpDown counters registered with this metrics collector. + /// + /// The type of metric measurement value. + /// Read only dictionary of . + public IReadOnlyDictionary>? GetAllObservableUpDownCounters() + where T : struct => GetAllInstruments(_allObservableUpDownCounters); + + /// + /// Gets a list of all observable Gauges registered with this metrics collector. + /// + /// The type of metric measurement value. + /// Read only dictionary of . + public IReadOnlyDictionary>? GetAllObservableGauges() + where T : struct => GetAllInstruments(_allObservableGauges); +#pragma warning restore S4049 // Properties should be preferred + + /// + /// Collects all observable instruments and records their measurements. + /// + public void CollectObservableInstruments() + { + _listener.RecordObservableInstruments(); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disponse the el. + /// + /// Disposing. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _listener.Dispose(); + } + } + + private static void ClearValues(Dictionary instrumentValues) + { + ClearValuesOf(instrumentValues); + ClearValuesOf(instrumentValues); + ClearValuesOf(instrumentValues); + ClearValuesOf(instrumentValues); + ClearValuesOf(instrumentValues); + ClearValuesOf(instrumentValues); + ClearValuesOf(instrumentValues); + } + + private static void ClearValuesOf(Dictionary instrumentValues) + where T : struct + { + var valuesDictionary = (ConcurrentDictionary>)instrumentValues[typeof(T)]; + + foreach (var kvp in valuesDictionary) + { + kvp.Value.Clear(); + } + } + + private static MetricValuesHolder? GetInstrumentCapturedData(Dictionary allInstrumentsValues, string instrumentName) + where T : struct + { + if (!allInstrumentsValues.TryGetValue(typeof(T), out object? value)) + { + throw new InvalidOperationException($"The type {typeof(T)} is not supported as a type for a metric measurement value"); + } + + var instrumentsDictionary = (IReadOnlyDictionary>)value!; + _ = instrumentsDictionary.TryGetValue(instrumentName, out var metricValuesHolder); + + return metricValuesHolder; + } + + private static IReadOnlyDictionary>? GetAllInstruments(Dictionary allInstruments) + where T : struct + { + if (!allInstruments.TryGetValue(typeof(T), out object? value)) + { + throw new InvalidOperationException($"The type {typeof(T)} is not supported as a type for a metric measurement value"); + } + + return (IReadOnlyDictionary>)value!; + } + + private static Dictionary CreateMetricsValuesDictionary() + { +#pragma warning disable CPR121 // Specify 'concurrencyLevel' and 'capacity' in the ConcurrentDictionary ctor. + return new Dictionary + { + [typeof(byte)] = new ConcurrentDictionary>(StringComparer.Ordinal), + [typeof(short)] = new ConcurrentDictionary>(StringComparer.Ordinal), + [typeof(int)] = new ConcurrentDictionary>(StringComparer.Ordinal), + [typeof(long)] = new ConcurrentDictionary>(StringComparer.Ordinal), + [typeof(float)] = new ConcurrentDictionary>(StringComparer.Ordinal), + [typeof(double)] = new ConcurrentDictionary>(StringComparer.Ordinal), + [typeof(decimal)] = new ConcurrentDictionary>(StringComparer.Ordinal), + }; +#pragma warning restore CPR121 // Specify 'concurrencyLevel' and 'capacity' in the ConcurrentDictionary ctor. + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricCollectorT.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricCollectorT.cs new file mode 100644 index 0000000000..ed7a6b5652 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricCollectorT.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; + +namespace Microsoft.Extensions.Telemetry.Testing.Metering; + +/// +/// The helper class to automatically capture metering information that has been recorded +/// by instruments created by . +/// +/// +/// This type has been designed to be used only for testing purposes. +/// +/// The type whose name is used as the instance name. +[Experimental] +#pragma warning disable SA1649 // File name should match first type name +public sealed class MetricCollector : MetricCollector +#pragma warning restore SA1649 // File name should match first type name +{ + /// + /// Initializes a new instance of the class. + /// + public MetricCollector() + : base(new[] { typeof(TMeterName).FullName! }) + { + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricValue.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricValue.cs new file mode 100644 index 0000000000..049775573e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricValue.cs @@ -0,0 +1,112 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; + +namespace Microsoft.Extensions.Telemetry.Testing.Metering; + +/// +/// Represents the whole information about a single metric measurement. +/// +/// The type of metric measurement value. +[Experimental] +public sealed class MetricValue + where T : struct +{ + private int _isLockTaken; + + internal MetricValue(T measurement, KeyValuePair[] tags, DateTimeOffset timestamp) + { + Tags = tags; + Timestamp = timestamp; + Value = measurement; + } + + /// + /// Gets a measurement's value. + /// + public T Value { get; internal set; } + + /// + /// Gets a timestamp indicating when a measurement was recorded. + /// + public DateTimeOffset Timestamp { get; } + + /// + /// Gets a collection of measurement's dimensions. + /// + public IReadOnlyCollection> Tags { get; } + + /// + /// Gets a dimension value by a dimension name. + /// + /// The dimension name. + /// The dimension value or if the dimension value was not recorded. + public object? GetDimension(string dimensionName) + { + foreach (var kvp in Tags) + { + if (kvp.Key == dimensionName) + { + return kvp.Value; + } + } + + return null; + } + + internal void Add(T value) + { + SafeUpdate( + () => + { + var valueObj = (object)Value; + var valueToAddObj = (object)value; + +#pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive). + object result = value switch + { + byte => (byte)((byte)valueObj + (byte)valueToAddObj), + short => (short)((short)valueObj + (short)valueToAddObj), + int => (int)valueObj + (int)valueToAddObj, + long => (long)valueObj + (long)valueToAddObj, + float => (float)valueObj + (float)valueToAddObj, + double => (double)valueObj + (double)valueToAddObj, + decimal => (decimal)valueObj + (decimal)valueToAddObj, + _ => throw new InvalidOperationException($"The type {typeof(T).FullName} is not supported as a metering measurement value type."), + }; +#pragma warning restore CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive). + + Value = (T)result; + }); + } + + internal void Update(T value) + { + SafeUpdate(() => Value = value); + } + + [ExcludeFromCodeCoverage] + private void SafeUpdate(Action action) + { + var sw = default(SpinWait); + + while (true) + { + if (Interlocked.Exchange(ref _isLockTaken, 1) == 0) + { + // Lock acquired + action(); + + // Release lock + _ = Interlocked.Exchange(ref _isLockTaken, 0); + break; + } + + sw.SpinOnce(); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricValuesHolder.cs b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricValuesHolder.cs new file mode 100644 index 0000000000..0a29f7f579 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Metering/MetricValuesHolder.cs @@ -0,0 +1,226 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using Microsoft.Extensions.Telemetry.Testing.Metering.Internal; + +namespace Microsoft.Extensions.Telemetry.Testing.Metering; + +/// +/// The metric measurements holder that contains information belonging to one named metric. +/// +/// The type of metric measurement value. +[Experimental] +public sealed class MetricValuesHolder + where T : struct +{ + private static readonly HashSet _supportedValueTypesAsDimensionValue = new() + { + typeof(int), + typeof(byte), + typeof(short), + typeof(long), + typeof(float), + typeof(double), + typeof(char), + }; + + private readonly TimeProvider _timeProvider; + private readonly AggregationType _aggregationType; + private readonly ConcurrentDictionary> _valuesTable; +#if NETCOREAPP3_1_OR_GREATER + private readonly ConcurrentBag> _values; +#else + private ConcurrentBag> _values; +#endif + private string? _latestWrittenKey; + + internal MetricValuesHolder(TimeProvider timeProvider, AggregationType aggregationType, string metricName) + { + _timeProvider = timeProvider; + _aggregationType = aggregationType; + MetricName = metricName; + _values = new(); + _valuesTable = new(); + } + + /// + /// Gets the metric name. + /// + public string MetricName { get; } + + /// + /// Gets all metric values recorded by the instrument. + /// + public IReadOnlyCollection> AllValues => _values; + + /// + /// Gets the latest recorded metric measurement value. + /// + public T? LatestWrittenValue => LatestWritten?.Value; + + /// + /// Gets the object containing whole information about the latest recorded metric measurement. + /// + public MetricValue? LatestWritten => _latestWrittenKey == null ? null : _valuesTable[_latestWrittenKey]; + + /// + /// Gets a recorded metric measurement value by given dimensions. + /// + /// The dimensions of a metric measurement. + /// The metric measurement value or if it does not exist. + public T? GetValue(params KeyValuePair[] tags) + { + var tagsCopy = tags.ToArray(); + Array.Sort(tagsCopy, (x, y) => StringComparer.Ordinal.Compare(x.Key, y.Key)); + + var key = CreateKey(tagsCopy); + + _ = _valuesTable.TryGetValue(key, out var value); + + return value?.Value; + } + + /// + /// Clears all metric measurements information. + /// + public void Clear() + { +#if NETCOREAPP3_1_OR_GREATER + _values.Clear(); +#else + _values = new(); +#endif + _valuesTable.Clear(); + _latestWrittenKey = null; + } + + internal void ReceiveValue(T value, ReadOnlySpan> tags) + { + var tagsArray = tags.ToArray(); + Array.Sort(tagsArray, (x, y) => StringComparer.Ordinal.Compare(x.Key, y.Key)); + var key = CreateKey(tagsArray); + + switch (_aggregationType) + { + case AggregationType.Save: + Save(value, tagsArray, key); + break; + + case AggregationType.Aggregate: + SaveAndAggregate(value, tagsArray, key); + break; + + case AggregationType.SaveOrUpdate: + SaveOrUpdate(value, tagsArray, key); + break; + + default: + throw new InvalidOperationException($"Aggregation type {_aggregationType} is not supported."); + } + } + + private static string CreateKey(params KeyValuePair[] tags) + { + if (tags.Length == 0) + { + return string.Empty; + } + + const char TagSeparator = ';'; + const char KeyValueSeparator = ':'; + const char ArrayMemberSeparator = ','; + + var keyBuilder = new StringBuilder(); + + foreach (var kvp in tags) + { + _ = keyBuilder + .Append(kvp.Key) + .Append(KeyValueSeparator); + + if (kvp.Value is null) + { + _ = keyBuilder.Append(string.Empty); + } + else + { + var valueType = kvp.Value.GetType(); + + if (valueType == typeof(string) || (!valueType.IsArray && _supportedValueTypesAsDimensionValue.Contains(valueType))) + { + _ = keyBuilder.Append(kvp.Value); + } + else if (valueType.IsArray && _supportedValueTypesAsDimensionValue.Contains(valueType.GetElementType()!)) + { + var array = (Array)kvp.Value; + + _ = keyBuilder.Append('['); + + foreach (var item in array) + { + _ = keyBuilder + .Append(item) + .Append(ArrayMemberSeparator); + } + + _ = keyBuilder.Append(']'); + } + else + { + throw new InvalidOperationException($"The type {valueType.FullName} is not supported as a dimension value type."); + } + } + + _ = keyBuilder.Append(TagSeparator); + } + + return keyBuilder.ToString(); + } + + private void Save(T value, KeyValuePair[] tagsArray, string key) + { + var metricValue = new MetricValue(value, tagsArray, _timeProvider.GetUtcNow()); + + _latestWrittenKey = key; + + SaveMetricValue(key, metricValue); + } + + private void SaveAndAggregate(T value, KeyValuePair[] tagsArray, string key) + { + _latestWrittenKey = key; + + GetOrAdd(key, tagsArray).Add(value); + } + + private void SaveOrUpdate(T value, KeyValuePair[] tagsArray, string key) + { + _latestWrittenKey = key; + + GetOrAdd(key, tagsArray).Update(value); + } + + private MetricValue GetOrAdd(string key, KeyValuePair[] tagsArray) + { + return _valuesTable.GetOrAdd(key, + (_) => + { + var metricValue = new MetricValue(default, tagsArray, _timeProvider.GetUtcNow()); + _values.Add(metricValue); + + return metricValue; + }); + } + + private void SaveMetricValue(string key, MetricValue metricValue) + { + _values.Add(metricValue); + _ = _valuesTable.AddOrUpdate(key, metricValue, (_, _) => metricValue); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Microsoft.Extensions.Telemetry.Testing.csproj b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Microsoft.Extensions.Telemetry.Testing.csproj new file mode 100644 index 0000000000..2bd3b88e40 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry.Testing/Microsoft.Extensions.Telemetry.Testing.csproj @@ -0,0 +1,34 @@ + + + Microsoft.Extensions.Telemetry.Testing + Hand-crafted fakes to make telemetry-related testing easier. + Telemetry + $(PackageTags);Testing + + + + true + true + + + + normal + 100 + 95 + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessEnricherDimensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessEnricherDimensions.cs new file mode 100644 index 0000000000..1d1b3b2382 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessEnricherDimensions.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Telemetry.Enrichment; + +/// +/// Constants used for enrichment dimensions. +/// +public static class ProcessEnricherDimensions +{ + /// + /// Process ID. + /// + public const string ProcessId = "pid"; + + /// + /// Thread ID. + /// + public const string ThreadId = "tid"; + + /// + /// Gets a list of all dimension names. + /// + /// A read-only of all dimension names. + public static IReadOnlyList DimensionNames { get; } = + Array.AsReadOnly(new[] + { + ProcessId, + ThreadId + }); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessEnricherExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessEnricherExtensions.cs new file mode 100644 index 0000000000..1e83b5e695 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessEnricherExtensions.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Enrichment; + +/// +/// Extension methods for setting up Process enrichers in an . +/// +public static class ProcessEnricherExtensions +{ + /// + /// Adds an instance of the process enricher to the . + /// + /// The to add the process enricher to. + /// The so that additional calls can be chained. + /// The is . + public static IServiceCollection AddProcessLogEnricher(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + return services + .AddProcessLogEnricher(_ => { }); + } + + /// + /// Adds an instance of the process enricher to the . + /// + /// The to add the process enricher to. + /// The configuration delegate. + /// The so that additional calls can be chained. + /// One of the arguments is . + public static IServiceCollection AddProcessLogEnricher(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + return services + .AddLogEnricher() + .AddLogEnricherOptions(configure); + } + + /// + /// Adds an instance of the host enricher to the . + /// + /// The to add the process enricher to. + /// The to use for configuring in the process enricher. + /// The so that additional calls can be chained. + /// One of the arguments is . + public static IServiceCollection AddProcessLogEnricher(this IServiceCollection services, IConfigurationSection section) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(section); + + return services + .AddLogEnricher() + .AddLogEnricherOptions(_ => { }, section); + } + + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(ProcessLogEnricherOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed by [DynamicDependency]")] + private static IServiceCollection AddLogEnricherOptions( + this IServiceCollection services, + Action configure, + IConfigurationSection? section = null) + { + _ = services.Configure(configure); + + if (section is not null) + { + _ = services.Configure(section); + } + + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessLogEnricher.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessLogEnricher.cs new file mode 100644 index 0000000000..5828e5d866 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessLogEnricher.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Text; + +namespace Microsoft.Extensions.Telemetry.Enrichment; + +/// +/// Enriches logs with process information. +/// +internal sealed class ProcessLogEnricher : ILogEnricher +{ + [ThreadStatic] + private static string? _threadId; + private readonly bool _threadIdEnabled; + + private readonly string? _processId; + + public ProcessLogEnricher(IOptions options) + { + var enricherOptions = Throw.IfMemberNull(options, options.Value); + + _threadIdEnabled = enricherOptions.ThreadId; + + if (enricherOptions.ProcessId) + { +#if NET5_0_OR_GREATER + var pid = Environment.ProcessId; +#else + var pid = System.Diagnostics.Process.GetCurrentProcess().Id; +#endif + + _processId = pid.ToInvariantString(); + } + } + + public void Enrich(IEnrichmentPropertyBag enrichmentBag) + { + if (_processId != null) + { + enrichmentBag.Add(ProcessEnricherDimensions.ProcessId, _processId); + } + + if (_threadIdEnabled) + { +#pragma warning disable S2696 // Instance members should not write to "static" fields + _threadId ??= Environment.CurrentManagedThreadId.ToInvariantString(); +#pragma warning restore S2696 // Instance members should not write to "static" fields + + enrichmentBag.Add(ProcessEnricherDimensions.ThreadId, _threadId); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessLogEnricherOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessLogEnricherOptions.cs new file mode 100644 index 0000000000..2cdf3ea9f6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Process/ProcessLogEnricherOptions.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Telemetry.Enrichment; + +/// +/// Options for the process enricher. +/// +public class ProcessLogEnricherOptions +{ + /// + /// Gets or sets a value indicating whether current process ID is used for log enrichment. + /// + /// + /// Default set to . + /// + public bool ProcessId { get; set; } = true; + + /// + /// Gets or sets a value indicating whether current thread ID is used for log enrichment. + /// + /// + /// Default set to . + /// + public bool ThreadId { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceEnricherDimensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceEnricherDimensions.cs new file mode 100644 index 0000000000..ee8f91e864 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceEnricherDimensions.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Telemetry.Enrichment; + +/// +/// Constants used for enrichment dimensions. +/// +public static class ServiceEnricherDimensions +{ + /// + /// Application name. + /// + public const string ApplicationName = "env_app_name"; + + /// + /// Environment name. + /// + public const string EnvironmentName = "env_cloud_env"; + + /// + /// Deployment ring. + /// + public const string DeploymentRing = "env_cloud_deploymentRing"; + + /// + /// Build version. + /// + public const string BuildVersion = "env_cloud_roleVer"; + + /// + /// Gets a list of all dimension names. + /// + /// A read-only of all dimension names. + public static IReadOnlyList DimensionNames { get; } = + Array.AsReadOnly(new[] + { + ApplicationName, + EnvironmentName, + BuildVersion, + DeploymentRing + }); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceEnricherExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceEnricherExtensions.cs new file mode 100644 index 0000000000..630ca1da9f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceEnricherExtensions.cs @@ -0,0 +1,226 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Shared.Diagnostics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Telemetry.Enrichment; + +/// +/// Extension methods for setting up the service enrichers in an . +/// +public static class ServiceEnricherExtensions +{ + /// + /// Adds an instance of the service enricher to the . + /// + /// The to add the service enricher to. + /// The so that additional calls can be chained. + /// is . + public static IServiceCollection AddServiceLogEnricher(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + return services + .AddServiceLogEnricher(_ => { }); + } + + /// + /// Adds an instance of the service enricher to the . + /// + /// The to add the service enricher to. + /// The configuration delegate. + /// The so that additional calls can be chained. + /// One of the arguments is . + public static IServiceCollection AddServiceLogEnricher(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + return services + .AddLogEnricher() + .AddLogEnricherOptions(configure); + } + + /// + /// Adds an instance of the service enricher to the . + /// + /// The to add the service enricher to. + /// The to use for configuring in the service enricher. + /// The so that additional calls can be chained. + /// One of the arguments is . + public static IServiceCollection AddServiceLogEnricher(this IServiceCollection services, IConfigurationSection section) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(section); + + return services + .AddLogEnricher() + .AddLogEnricherOptions(_ => { }, section); + } + + /// + /// Adds an instance of the service enricher to the . + /// + /// The to add the service enricher to. + /// The so that additional calls can be chained. + /// is . + public static IServiceCollection AddServiceMetricEnricher(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + return services + .AddServiceMetricEnricher(_ => { }); + } + + /// + /// Adds an instance of the service enricher to the . + /// + /// The to add the service enricher to. + /// The configuration delegate. + /// The so that additional calls can be chained. + /// One of the arguments is . + public static IServiceCollection AddServiceMetricEnricher(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + return services + .AddMetricEnricherOptions(configure) + .AddMetricEnricher(); + } + + /// + /// Adds an instance of the service enricher to the . + /// + /// The to add the service enricher to. + /// The to use for configuring in the service enricher. + /// The so that additional calls can be chained. + /// One of the arguments is . + public static IServiceCollection AddServiceMetricEnricher(this IServiceCollection services, IConfigurationSection section) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(section); + + return services + .AddMetricEnricherOptions(_ => { }, section) + .AddMetricEnricher(); + } + + /// + /// Adds an instance of service trace enricher to the . + /// + /// The to add the service trace enricher to. + /// The so that additional calls can be chained. + /// is . + public static TracerProviderBuilder AddServiceTraceEnricher(this TracerProviderBuilder builder) + { + _ = Throw.IfNull(builder); + + _ = builder.AddTraceEnricher(); + _ = builder.ConfigureServices(services => services.AddTraceEnricherOptions(_ => { })); + + return builder; + } + + /// + /// Adds an instance of Service trace enricher to the . + /// + /// The to add the service trace enricher to. + /// The configuration delegate. + /// The so that additional calls can be chained. + /// One of the arguments is . + public static TracerProviderBuilder AddServiceTraceEnricher(this TracerProviderBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + _ = builder.AddTraceEnricher(); + _ = builder.ConfigureServices(services => services.AddTraceEnricherOptions(configure)); + + return builder; + } + + /// + /// Adds an instance of Service trace enricher to the . + /// + /// The to add the Service trace enricher to. + /// The to use for configuring in the Service trace enricher. + /// The so that additional calls can be chained. + /// One of the arguments is . + public static TracerProviderBuilder AddServiceTraceEnricher(this TracerProviderBuilder builder, IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + + _ = builder.AddTraceEnricher(); + _ = builder.ConfigureServices(services => services.AddTraceEnricherOptions(_ => { }, section)); + + return builder; + } + + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(ServiceMetricEnricherOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed by [DynamicDependency]")] + private static IServiceCollection AddMetricEnricherOptions( + this IServiceCollection services, + Action configure, + IConfigurationSection? section = null) + { + _ = services.Configure(configure); + + if (section is not null) + { + _ = services.Configure(section); + } + + return services; + } + + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(ServiceLogEnricherOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed by [DynamicDependency]")] + private static IServiceCollection AddLogEnricherOptions( + this IServiceCollection services, + Action configure, + IConfigurationSection? section = null) + { + _ = services.Configure(configure); + + if (section is not null) + { + _ = services.Configure(section); + } + + return services; + } + + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(ServiceTraceEnricherOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed with [DynamicDependency]")] + private static IServiceCollection AddTraceEnricherOptions( + this IServiceCollection services, + Action configure, + IConfigurationSection? section = null) + { + _ = services.Configure(configure); + + if (section is not null) + { + _ = services.Configure(section); + } + + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceLogEnricher.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceLogEnricher.cs new file mode 100644 index 0000000000..30e3a362e8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceLogEnricher.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.AmbientMetadata; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Enrichment; + +internal sealed class ServiceLogEnricher : ILogEnricher +{ + private readonly KeyValuePair[] _props; + + public ServiceLogEnricher( + IOptions options, + IOptions metadata) + { + var enricherOptions = Throw.IfMemberNull(options, options.Value); + var applicationMetadata = Throw.IfMemberNull(metadata, metadata.Value); + + _props = Initialize(enricherOptions, applicationMetadata); + } + + public void Enrich(IEnrichmentPropertyBag enrichmentPropertyBag) => enrichmentPropertyBag.Add(_props); + + private static KeyValuePair[] Initialize(ServiceLogEnricherOptions enricherOptions, ApplicationMetadata applicationMetadata) + { + var l = new List>(); + + if (enricherOptions.ApplicationName) + { + l.Add(new(ServiceEnricherDimensions.ApplicationName, applicationMetadata.ApplicationName)); + } + + if (enricherOptions.EnvironmentName) + { + l.Add(new(ServiceEnricherDimensions.EnvironmentName, applicationMetadata.EnvironmentName)); + } + + if (enricherOptions.DeploymentRing && applicationMetadata.DeploymentRing is not null) + { + l.Add(new(ServiceEnricherDimensions.DeploymentRing, applicationMetadata.DeploymentRing)); + } + + if (enricherOptions.BuildVersion && applicationMetadata.BuildVersion is not null) + { + l.Add(new(ServiceEnricherDimensions.BuildVersion, applicationMetadata.BuildVersion)); + } + + return l.ToArray(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceLogEnricherOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceLogEnricherOptions.cs new file mode 100644 index 0000000000..593f8be2d6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceLogEnricherOptions.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.AmbientMetadata; + +namespace Microsoft.Extensions.Telemetry.Enrichment; + +/// +/// Options for the service log enricher. +/// +public class ServiceLogEnricherOptions +{ + /// + /// Gets or sets a value indicating whether is used for logs enrichment. + /// + /// + /// Default set to . + /// + public bool EnvironmentName { get; set; } = true; + + /// + /// Gets or sets a value indicating whether is used for logs enrichment. + /// + /// + /// Default set to . + /// + public bool ApplicationName { get; set; } = true; + + /// + /// Gets or sets a value indicating whether is used for logs enrichment. + /// + /// + /// Default set to . + /// + public bool DeploymentRing { get; set; } + + /// + /// Gets or sets a value indicating whether is used for logs enrichment. + /// + /// + /// Default set to . + /// + public bool BuildVersion { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceMetricEnricher.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceMetricEnricher.cs new file mode 100644 index 0000000000..017345daed --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceMetricEnricher.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.AmbientMetadata; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Enrichment; + +internal sealed class ServiceMetricEnricher : IMetricEnricher +{ + private readonly KeyValuePair[] _props; + + public ServiceMetricEnricher( + IOptions options, + IOptions metadata) + { + var enricherOptions = Throw.IfMemberNull(options, options.Value); + var applicationMetadata = Throw.IfMemberNull(metadata, metadata.Value); + + _props = Initialize(enricherOptions, applicationMetadata); + } + + public void Enrich(IEnrichmentPropertyBag enrichmentPropertyBag) => enrichmentPropertyBag.Add(_props); + + private static KeyValuePair[] Initialize(ServiceMetricEnricherOptions enricherOptions, ApplicationMetadata applicationMetadata) + { + var l = new List>(); + + if (enricherOptions.ApplicationName) + { + l.Add(new(ServiceEnricherDimensions.ApplicationName, applicationMetadata.ApplicationName)); + } + + if (enricherOptions.EnvironmentName) + { + l.Add(new(ServiceEnricherDimensions.EnvironmentName, applicationMetadata.EnvironmentName)); + } + + if (enricherOptions.DeploymentRing && applicationMetadata.DeploymentRing is not null) + { + l.Add(new(ServiceEnricherDimensions.DeploymentRing, applicationMetadata.DeploymentRing)); + } + + if (enricherOptions.BuildVersion && applicationMetadata.BuildVersion is not null) + { + l.Add(new(ServiceEnricherDimensions.BuildVersion, applicationMetadata.BuildVersion)); + } + + return l.ToArray(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceMetricEnricherOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceMetricEnricherOptions.cs new file mode 100644 index 0000000000..d7c972b249 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceMetricEnricherOptions.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.AmbientMetadata; + +namespace Microsoft.Extensions.Telemetry.Enrichment; + +/// +/// Options for the service metric enricher. +/// +public class ServiceMetricEnricherOptions +{ + /// + /// Gets or sets a value indicating whether is used for metric enrichment. + /// + /// + /// Default set to . + /// + public bool EnvironmentName { get; set; } = true; + + /// + /// Gets or sets a value indicating whether is used for metric enrichment. + /// + /// + /// Default set to . + /// + public bool ApplicationName { get; set; } = true; + + /// + /// Gets or sets a value indicating whether is used for metric enrichment. + /// + /// + /// Default set to . + /// + public bool DeploymentRing { get; set; } + + /// + /// Gets or sets a value indicating whether is used for metric enrichment. + /// + /// + /// Default set to . + /// + public bool BuildVersion { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceTraceEnricher.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceTraceEnricher.cs new file mode 100644 index 0000000000..3d3311af53 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceTraceEnricher.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Microsoft.Extensions.AmbientMetadata; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Telemetry.Enrichment; + +internal sealed class ServiceTraceEnricher : ITraceEnricher +{ + private readonly string? _envName; + private readonly string? _appName; + private readonly string? _buildVersion; + private readonly string? _deploymentRing; + + public ServiceTraceEnricher( + IOptions options, + IOptions metadata) + { + var enricherOptions = options.Value; + var applicationMetadata = metadata.Value; + + if (enricherOptions.ApplicationName) + { + _appName = applicationMetadata.ApplicationName; + } + + if (enricherOptions.EnvironmentName) + { + _envName = applicationMetadata.EnvironmentName; + } + + if (enricherOptions.DeploymentRing) + { + _deploymentRing = applicationMetadata.DeploymentRing; + } + + if (enricherOptions.BuildVersion) + { + _buildVersion = applicationMetadata.BuildVersion; + } + } + + public void Enrich(Activity activity) + { + if (_appName is not null) + { + _ = activity.AddTag(ServiceEnricherDimensions.ApplicationName, _appName); + } + + if (_envName is not null) + { + _ = activity.AddTag(ServiceEnricherDimensions.EnvironmentName, _envName); + } + + if (_buildVersion is not null) + { + _ = activity.AddTag(ServiceEnricherDimensions.BuildVersion, _buildVersion); + } + + if (_deploymentRing is not null) + { + _ = activity.AddTag(ServiceEnricherDimensions.DeploymentRing, _deploymentRing); + } + } + + public void EnrichOnActivityStart(Activity activity) + { + // nothing + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceTraceEnricherOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceTraceEnricherOptions.cs new file mode 100644 index 0000000000..0d241ccddc --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Enrichment.Service/ServiceTraceEnricherOptions.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.AmbientMetadata; + +namespace Microsoft.Extensions.Telemetry.Enrichment; + +/// +/// Options for the service trace enricher. +/// +public class ServiceTraceEnricherOptions +{ + /// + /// Gets or sets a value indicating whether is used for trace enrichment. + /// + /// + /// Default set to . + /// + public bool EnvironmentName { get; set; } = true; + + /// + /// Gets or sets a value indicating whether is used for trace enrichment. + /// + /// + /// Default set to . + /// + public bool ApplicationName { get; set; } = true; + + /// + /// Gets or sets a value indicating whether is used for trace enrichment. + /// + /// + /// Default set to . + /// + public bool DeploymentRing { get; set; } + + /// + /// Gets or sets a value indicating whether is used for trace enrichment. + /// + /// + /// Default set to . + /// + public bool BuildVersion { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/CheckpointTracker.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/CheckpointTracker.cs new file mode 100644 index 0000000000..350cd9e330 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/CheckpointTracker.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Telemetry.Latency; + +namespace Microsoft.Extensions.Telemetry.Latency.Internal; + +/// +/// Class that tracks checkpoints. +/// +internal sealed class CheckpointTracker : IResettable +{ + internal TimeProvider TimeProvider; + private readonly Registry _checkpointNames; + private readonly int[] _checkpointAdded; + private readonly Checkpoint[] _checkpoints; + + private long _timestamp; + + private int _numCheckpoints; + + public long Elapsed => TimeProvider.GetTimestamp() - _timestamp; + + public long Frequency => TimeProvider.TimestampFrequency; + + /// + /// Initializes a new instance of the class. + /// + /// Registry of checkpoint names. + public CheckpointTracker(Registry registry) + { + _checkpointNames = registry; + var keyCount = _checkpointNames.KeyCount; + _checkpointAdded = new int[keyCount]; + _checkpoints = new Checkpoint[keyCount]; + TimeProvider = TimeProvider.System; + _timestamp = TimeProvider.GetTimestamp(); + } + + /// + /// Resets the CheckpointTracker. + /// + public bool TryReset() + { + _timestamp = TimeProvider.GetTimestamp(); + _numCheckpoints = 0; + Array.Clear(_checkpointAdded, 0, _checkpointAdded.Length); + return true; + } + + public CheckpointToken GetToken(string name) + { + int pos = _checkpointNames.GetRegisteredKeyIndex(name); + return new CheckpointToken(name, pos); + } + + /// + /// Add checkpoint for token. + /// + /// Token for checkpoint. + /// If same checkpoint is added more than once, first write wins. + public void Add(CheckpointToken token) + { + if (token.Position > -1 && Interlocked.CompareExchange(ref _checkpointAdded[token.Position], 1, 0) == 0) + { + var p = Interlocked.Increment(ref _numCheckpoints); + _checkpoints[p - 1] = new Checkpoint(token.Name, Elapsed, Frequency); + } + } + + /// + /// Gets list of checkpoints added. + /// + public ArraySegment Checkpoints => new(_checkpoints, 0, _numCheckpoints); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContext.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContext.cs new file mode 100644 index 0000000000..23684a2dec --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContext.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Telemetry.Latency; + +namespace Microsoft.Extensions.Telemetry.Latency.Internal; + +/// +/// Implementation of . +/// +internal sealed class LatencyContext : ILatencyContext, IResettable +{ + internal bool IsDisposed; + + internal bool IsRunning; + + private readonly ObjectPool _poolToReturnTo; + + private readonly CheckpointTracker _checkpointTracker; + + private readonly TagCollection _tagCollection; + + private readonly MeasureTracker _measureTracker; + + private long _duration; + + public LatencyContext(LatencyContextPool latencyContextPool) + { + var latencyInstrumentProvider = latencyContextPool.LatencyInstrumentProvider; + _checkpointTracker = latencyInstrumentProvider.CreateCheckpointTracker(); + _measureTracker = latencyInstrumentProvider.CreateMeasureTracker(); + _tagCollection = latencyInstrumentProvider.CreateTagCollection(); + _poolToReturnTo = latencyContextPool.Pool; + IsRunning = true; + } + + public LatencyData LatencyData => IsDisposed ? default : new(_tagCollection.Tags, _checkpointTracker.Checkpoints, _measureTracker.Measures, Duration, _checkpointTracker.Frequency); + + private long Duration => IsRunning ? _checkpointTracker.Elapsed : _duration; + + #region Checkpoints + public void AddCheckpoint(CheckpointToken token) + { + if (IsRunning) + { + _checkpointTracker.Add(token); + } + } + #endregion + + #region Tags + public void SetTag(TagToken token, string value) + { + if (IsRunning) + { + _tagCollection.Set(token, value); + } + } + #endregion + + #region Measure + public void AddMeasure(MeasureToken token, long value) + { + if (IsRunning) + { + _measureTracker.AddLong(token, value); + } + } + + public void RecordMeasure(MeasureToken token, long value) + { + if (IsRunning) + { + _measureTracker.SetLong(token, value); + } + } + #endregion + + #region State + public void Freeze() + { + if (IsRunning) + { + IsRunning = false; + _duration = _checkpointTracker.Elapsed; + } + } + + public void Dispose() + { + if (IsDisposed) + { + return; + } + + if (IsRunning) + { + Freeze(); + } + + _poolToReturnTo.Return(this); + IsDisposed = true; + } + + public bool TryReset() + { + _ = _checkpointTracker.TryReset(); + _ = _measureTracker.TryReset(); + _ = _tagCollection.TryReset(); + IsRunning = true; + IsDisposed = false; + return true; + } + #endregion +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContextPool.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContextPool.cs new file mode 100644 index 0000000000..f0315563fd --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContextPool.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.Extensions.Telemetry.Latency.Internal; + +/// +/// Object pools for instruments used for latency measurement. +/// +internal sealed class LatencyContextPool +{ + internal ObjectPool Pool { get; set; } + + internal readonly LatencyInstrumentProvider LatencyInstrumentProvider; + + /// + /// Initializes a new instance of the class. + /// + public LatencyContextPool(LatencyInstrumentProvider latencyInstrumentProvider) + { + LatencyInstrumentProvider = latencyInstrumentProvider; + var lcp = new LatencyContextPolicy(this); + Pool = new ResetOnGetObjectPool(lcp); + } + + /// + /// Object pool policy for . + /// + internal sealed class LatencyContextPolicy : PooledObjectPolicy + { + private readonly LatencyContextPool _latencyContextPool; + + /// + /// Initializes a new instance of the class. + /// + public LatencyContextPolicy(LatencyContextPool latencyContextPool) + { + _latencyContextPool = latencyContextPool; + } + + /// + /// Creates the object. + /// + /// Created object. + public override LatencyContext Create() + { + return new LatencyContext(_latencyContextPool); + } + + /// + /// Return object to the pool. + /// + /// Object to be returned. + /// True, indicating object is to be returned. + public override bool Return(LatencyContext obj) => true; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContextProvider.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContextProvider.cs new file mode 100644 index 0000000000..33887e518b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContextProvider.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Telemetry.Latency; + +namespace Microsoft.Extensions.Telemetry.Latency.Internal; + +/// +/// Implementation of . +/// +internal sealed class LatencyContextProvider : ILatencyContextProvider +{ + private readonly LatencyContextPool _latencyInstrumentPool; + + /// + /// Initializes a new instance of the class. + /// + /// Latency instrument provider. + public LatencyContextProvider(LatencyInstrumentProvider latencyInstrumentProvider) + { + _latencyInstrumentPool = new LatencyContextPool(latencyInstrumentProvider); + } + + public ILatencyContext CreateContext() => _latencyInstrumentPool.Pool.Get(); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContextRegistrySet.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContextRegistrySet.cs new file mode 100644 index 0000000000..2cdf914e70 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContextRegistrySet.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Latency.Internal; + +/// +/// Class that holds registry of names used with APIs. +/// +internal sealed class LatencyContextRegistrySet +{ + /// + /// Gets the registry of checkpoint names. + /// + public Registry CheckpointNameRegistry { get; } + + /// + /// Gets the registry of tag names. + /// + public Registry TagNameRegistry { get; } + + /// + /// Gets the registry of counter names. + /// + public Registry MeasureNameRegistry { get; } + + public LatencyContextRegistrySet(IOptions latencyContextOptions, + IOptions? registrationOptions = null) + { + var latencyContextRegistrationOptions = registrationOptions != null ? registrationOptions.Value : new LatencyContextRegistrationOptions(); + var throwOnUnregisteredNames = latencyContextOptions.Value.ThrowOnUnregisteredNames; + + CheckpointNameRegistry = CreateRegistry(latencyContextRegistrationOptions.CheckpointNames, throwOnUnregisteredNames); + TagNameRegistry = CreateRegistry(latencyContextRegistrationOptions.TagNames, throwOnUnregisteredNames); + MeasureNameRegistry = CreateRegistry(latencyContextRegistrationOptions.MeasureNames, throwOnUnregisteredNames); + } + + private static Registry CreateRegistry(IEnumerable names, bool throwOnUnregisteredNames) + { + var n = GetRegistryKeys(names); + return new Registry(n, throwOnUnregisteredNames); + } + + private static string[] GetRegistryKeys(IEnumerable names) + { + _ = Throw.IfNull(names); + + foreach (var name in names) + { + if (string.IsNullOrWhiteSpace(name)) + { + Throw.ArgumentException(nameof(names), "Found null or whitespace name in supplied set"); + } + } + + HashSet keys = new HashSet(names); + return keys.ToArray(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContextTokenIssuer.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContextTokenIssuer.cs new file mode 100644 index 0000000000..db9e267c0b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyContextTokenIssuer.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Latency.Internal; + +internal sealed class LatencyContextTokenIssuer : ILatencyContextTokenIssuer +{ + private readonly CheckpointTracker _checkpointTracker; + + private readonly MeasureTracker _measureTracker; + + private readonly TagCollection _tagCollection; + + public LatencyContextTokenIssuer(LatencyInstrumentProvider latencyInstrumentProvider) + { + _checkpointTracker = latencyInstrumentProvider.CreateCheckpointTracker(); + _measureTracker = latencyInstrumentProvider.CreateMeasureTracker(); + _tagCollection = latencyInstrumentProvider.CreateTagCollection(); + } + + public CheckpointToken GetCheckpointToken(string name) + { + _ = Throw.IfNull(name); + return _checkpointTracker.GetToken(name); + } + + public TagToken GetTagToken(string name) + { + _ = Throw.IfNull(name); + return _tagCollection.GetToken(name); + } + + public MeasureToken GetMeasureToken(string name) + { + _ = Throw.IfNull(name); + return _measureTracker.GetToken(name); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyInstrumentProvider.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyInstrumentProvider.cs new file mode 100644 index 0000000000..16649216dc --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/LatencyInstrumentProvider.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Telemetry.Latency.Internal; + +internal sealed class LatencyInstrumentProvider +{ + private readonly LatencyContextRegistrySet _latencyContextRegistrySet; + + public LatencyInstrumentProvider(LatencyContextRegistrySet latencyContextRegistrySet) + { + _latencyContextRegistrySet = latencyContextRegistrySet; + } + + public CheckpointTracker CreateCheckpointTracker() + { + return new CheckpointTracker(_latencyContextRegistrySet.CheckpointNameRegistry); + } + + public MeasureTracker CreateMeasureTracker() + { + return new MeasureTracker(_latencyContextRegistrySet.MeasureNameRegistry); + } + + public TagCollection CreateTagCollection() + { + return new TagCollection(_latencyContextRegistrySet.TagNameRegistry); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/MeasureTracker.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/MeasureTracker.cs new file mode 100644 index 0000000000..e769a9a69c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/MeasureTracker.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Telemetry.Latency; + +namespace Microsoft.Extensions.Telemetry.Latency.Internal; + +internal sealed class MeasureTracker : IResettable +{ + private readonly Registry _registeredMeasureNames; + + private readonly string[] _measureNames; + + private readonly long[] _measureValues; + + private readonly Measure[] _measures; + + private readonly int[] _measurePosition; + + private readonly object _measurePositionLock = new(); + + private int _numMeasures; + + /// + /// Initializes a new instance of the class. + /// + /// Registry of measure names. + public MeasureTracker(Registry registry) + { + _registeredMeasureNames = registry; + int arraySize = _registeredMeasureNames.KeyCount + 1; + _measurePosition = new int[arraySize]; + _measures = new Measure[arraySize]; + _measureNames = new string[arraySize]; + _measureValues = new long[arraySize]; + } + + /// + /// Resets the MeasureTracker. + /// + public bool TryReset() + { +#if NET6_0_OR_GREATER + Array.Clear(_measurePosition); +#else + Array.Clear(_measurePosition, 0, _measurePosition.Length); +#endif + _numMeasures = 0; + return true; + } + + public MeasureToken GetToken(string name) + { + int pos = _registeredMeasureNames.GetRegisteredKeyIndex(name); + return new MeasureToken(name, pos); + } + + /// + /// Add value to measure. + /// + /// Token for measure. + /// Value of measure. + public void AddLong(MeasureToken token, long value) + { + if (token.Position > -1) + { + int pos = GetPositionOfMeasure(token); + _ = Interlocked.Add(ref _measureValues[pos], value); + } + } + + /// + /// Set value of measure. + /// + /// Token for measure. + /// Value of measure. + public void SetLong(MeasureToken token, long value) + { + if (token.Position > -1) + { + int pos = GetPositionOfMeasure(token); + _measureValues[pos] = value; + } + } + + /// + /// Gets the position at which a measure has been added. + /// + /// Token for the measure. + /// Position of the measure. + /// This function uses _measurePosition as a dictionary. The key is + /// the order of the name in the registry. The value is the position in the tracking arrays, + /// _measureNames and _measureValues. + private int GetPositionOfMeasure(MeasureToken measureToken) + { + int pos = measureToken.Position; + + // If measure with the name has already been added, return position. + // If being used for the first time, assign a position to it and initialize the tracking + // arrays. + if (_measurePosition[pos] == 0) + { + lock (_measurePositionLock) + { + if (_measurePosition[pos] == 0) + { + _numMeasures++; + _measureNames[_numMeasures] = measureToken.Name; + _measureValues[_numMeasures] = 0; + _measurePosition[pos] = _numMeasures; + } + } + } + + return _measurePosition[pos]; + } + + /// + /// Gets the list of measures added. + /// + public ArraySegment Measures + { + get + { + for (int i = 1; i <= _numMeasures; i++) + { + _measures[i] = new Measure(_measureNames[i], _measureValues[i]); + } + + return new(_measures, 1, _numMeasures); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/Registry.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/Registry.cs new file mode 100644 index 0000000000..a176fe6e3b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/Registry.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Latency.Internal; + +/// +/// Provides registry functionality. +/// +internal sealed class Registry +{ + private readonly FrozenDictionary _keyOrder; + + private readonly bool _throwOnUnregisteredKeyLookup; + + /// + /// Initializes a new instance of the class. + /// + /// Set of keys to be registered. + /// Throws when getting order for unregistered keys if true. + public Registry(string[] keys, bool throwOnUnregisteredKeyLookup) + { + // Order the keys + Array.Sort(keys); + OrderedKeys = keys; + + // Create lookup for key order + int c = OrderedKeys.Length; + var keyOrderBuilder = new Dictionary(c); + for (int i = 0; i < c; i++) + { + if (OrderedKeys[i] == null) + { + Throw.ArgumentException(nameof(keys), "Supplied set contains null values"); + } + + keyOrderBuilder.Add(OrderedKeys[i], i); + } + + _keyOrder = keyOrderBuilder.ToFrozenDictionary(StringComparer.Ordinal, optimizeForReading: true); + _throwOnUnregisteredKeyLookup = throwOnUnregisteredKeyLookup; + } + + /// + /// Gets the list of registered keys, ordered using default comparator. + /// + /// List of ordered keys. + public string[] OrderedKeys { get; } + + /// + /// Gets the number of registered keys. + /// + public int KeyCount => OrderedKeys.Length; + + /// + /// Gets the zero-based order of registered key. + /// + /// The key to get order for. + /// Index of key. Returns -1 if not registered. + public int GetRegisteredKeyIndex(string key) + { + _ = Throw.IfNull(key); + + if (_keyOrder.TryGetValue(key, out var order)) + { + return order; + } + else if (_throwOnUnregisteredKeyLookup) + { + Throw.ArgumentException(nameof(key), $"Name {key} has not been registered."); + } + + return -1; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/ResetOnGetObjectPool.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/ResetOnGetObjectPool.cs new file mode 100644 index 0000000000..f2f0e7d8ab --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/ResetOnGetObjectPool.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Telemetry.Latency.Internal; + +internal sealed class ResetOnGetObjectPool : ObjectPool + where T : class, IResettable +{ + private readonly ObjectPool _objectPool; + + /// + /// Initializes a new instance of the class. + /// + public ResetOnGetObjectPool(PooledObjectPolicy policy) + { + _objectPool = PoolFactory.CreatePool(policy); + } + + public override T Get() + { + var o = _objectPool.Get(); + _ = o.TryReset(); + return o; + } + + public override void Return(T obj) + { + _objectPool.Return(obj); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/TagCollection.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/TagCollection.cs new file mode 100644 index 0000000000..02956f19ed --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/Internal/TagCollection.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Telemetry.Latency; + +namespace Microsoft.Extensions.Telemetry.Latency.Internal; + +/// +/// Class used to add tags. +/// +internal sealed class TagCollection : IResettable +{ + private readonly Registry _tagNames; + + private readonly int _numTags; + + private readonly Tag[] _tags; + + /// + /// Initializes a new instance of the class. + /// + /// Registry of tag names. + public TagCollection(Registry registry) + { + _tagNames = registry; + var keyCount = registry.KeyCount; + _numTags = keyCount; + _tags = new Tag[_numTags]; + _ = TryReset(); + } + + /// + /// Resets . + /// + public bool TryReset() + { + var names = _tagNames.OrderedKeys; + for (int i = 0; i < _numTags; i++) + { + _tags[i] = new Tag(names[i], string.Empty); + } + + return true; + } + + public TagToken GetToken(string name) + { + int pos = _tagNames.GetRegisteredKeyIndex(name); + return new TagToken(name, pos); + } + + /// + /// Set value of the tag. + /// + /// Token for the tag. + /// Value of the tag. + public void Set(TagToken token, string value) + { + int pos = token.Position; + if (pos > -1) + { + _tags[pos] = new Tag(token.Name, value); + } + } + + /// + /// Gets the list of tags that have been added. + /// + public ArraySegment Tags => new(_tags, 0, _numTags); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Latency/LatencyContextExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/LatencyContextExtensions.cs new file mode 100644 index 0000000000..09a5acd455 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/LatencyContextExtensions.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Extensions.Telemetry.Latency.Internal; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Latency; + +/// +/// Extensions to add latency context. +/// +public static class LatencyContextExtensions +{ + /// + /// Add latency context. + /// + /// Dependency injection container. + /// Provided service collection with added. + public static IServiceCollection AddLatencyContext(this IServiceCollection services) + { + _ = Throw.IfNull(services); + _ = services.AddOptions(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + return services; + } + + /// + /// Add latency context. + /// + /// Dependency injection container. + /// configuration delegate. + /// Provided service collection with added. + public static IServiceCollection AddLatencyContext(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + _ = services.Configure(configure); + + return AddLatencyContext(services); + } + + /// + /// Add latency context. + /// + /// Dependency injection container. + /// Configuration of . + /// Provided service collection with added. + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(LatencyContextOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed by [DynamicDependency]")] + public static IServiceCollection AddLatencyContext(this IServiceCollection services, IConfigurationSection section) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(section); + + _ = services.Configure(section); + + return AddLatencyContext(services); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Latency/LatencyContextOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/LatencyContextOptions.cs new file mode 100644 index 0000000000..1e3966649c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Latency/LatencyContextOptions.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Telemetry.Latency; + +/// +/// Options for LatencyContext. +/// +public class LatencyContextOptions +{ + /// + /// Gets or sets a value indicating whether exception is thrown when using unregistered names. + /// + /// The ILatencyContext APIs throws when using unregistred names if true. + /// Becomes no-op otherwise. Defaults to false. + public bool ThrowOnUnregisteredNames { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/Logger.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/Logger.cs new file mode 100644 index 0000000000..aaa4a1b88a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/Logger.cs @@ -0,0 +1,244 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; +using Microsoft.Shared.Pools; +using OpenTelemetry.Logs; + +namespace Microsoft.Extensions.Telemetry.Logging; + +internal sealed class Logger : ILogger +{ + internal static readonly Func>?, LogRecord> CreateLogRecord = GetLogCreator(); + + private const string ExceptionStackTrace = "stackTrace"; + private readonly string _categoryName; + private readonly LoggerProvider _provider; + + /// + /// Call OpenTelemetry's LogRecord constructor. + /// + /// + /// Reflection is used because the constructor has 'internal' modifier and cannot be called directly. + /// This will be replaced with a direct call in one of the two conditions below. + /// - LogRecord will make its internalsVisible to R9 library. + /// - LogRecord constructor will become public. + /// + private static Func>?, LogRecord> GetLogCreator() + { + var logRecordConstructor = typeof(LogRecord).GetConstructor( +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + BindingFlags.Instance | BindingFlags.NonPublic, +#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + null, + + new[] + { + typeof(IExternalScopeProvider), + typeof(DateTime), + typeof(string), + typeof(LogLevel), + typeof(EventId), + typeof(string), + typeof(object), + typeof(Exception), + typeof(IReadOnlyList>) + }, + null)!; + + var val = new[] + { + Expression.Parameter(typeof(IExternalScopeProvider)), + Expression.Parameter(typeof(DateTime)), + Expression.Parameter(typeof(string)), + Expression.Parameter(typeof(LogLevel)), + Expression.Parameter(typeof(EventId)), + Expression.Parameter(typeof(string)), + Expression.Parameter(typeof(object)), + Expression.Parameter(typeof(Exception)), + Expression.Parameter(typeof(IReadOnlyList>)) + }; + + var lambdaLogRecord = Expression.Lambda>?, + LogRecord>>(Expression.New(logRecordConstructor, val), val); + + return lambdaLogRecord.Compile(); + } + + internal static TimeProvider TimeProvider => TimeProvider.System; + + internal Logger(string categoryName, LoggerProvider provider) + { + _categoryName = categoryName; + _provider = provider; + } + + internal IExternalScopeProvider? ScopeProvider { get; set; } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + LogMethodHelper propertyBag; + LogMethodHelper? rentedHelper = null; + + try + { + if (state is LogMethodHelper helper && _provider.CanUsePropertyBagPool) + { + propertyBag = helper; + } + else + { + rentedHelper = GetHelper(); + propertyBag = rentedHelper; + + switch (state) + { + case IReadOnlyList> stateList: + rentedHelper.AddRange(stateList); + break; + + case IEnumerable> stateList: + rentedHelper.AddRange(stateList); + break; + + case null: + break; + + default: + rentedHelper.Add("{OriginalFormat}", state); + break; + } + } + + foreach (var enricher in _provider.Enrichers) + { + enricher.Enrich(propertyBag); + } + + if (exception != null && _provider.IncludeStackTrace) + { + propertyBag.Add(ExceptionStackTrace, GetExceptionStackTrace(exception, _provider.MaxStackTraceLength)); + } + + var record = CreateLogRecord( + _provider.IncludeScopes ? ScopeProvider : null, + TimeProvider.GetUtcNow().UtcDateTime, + _categoryName, + logLevel, + eventId, + _provider.UseFormattedMessage ? formatter(state, exception) : null, + + // This parameter needs to be null for OpenTelemetry.Exporter.Geneva to pick up LogRecord.StateValues (the last parameter). + // This is equivalent to using OpenTelemetryLogger with ParseStateValues option set to true. + null, + exception, + propertyBag); + + _provider.Processor?.OnEnd(record); + } + catch (Exception ex) + { + LoggingEventSource.Log.LogException(ex); + throw; + } + finally + { + ReturnHelper(rentedHelper); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; + +#pragma warning disable CS8633 +#pragma warning disable CS8766 + public IDisposable? BeginScope(TState state) + where TState : notnull +#pragma warning restore CS8633 +#pragma warning restore CS8766 + { + ScopeProvider ??= new LoggerExternalScopeProvider(); + + return ScopeProvider.Push(state); + } + + private static string GetExceptionStackTrace(Exception exception, int maxStackTraceLength) + { + if (exception.StackTrace == null && exception.InnerException == null) + { + return string.Empty; + } + + var stackTrace = string.Empty; + var stringBuilder = PoolFactory.SharedStringBuilderPool.Get(); + _ = stringBuilder.AppendLine(exception.StackTrace); + + try + { + if (exception.InnerException != null) + { + GetInnerExceptionTrace(exception, stringBuilder, maxStackTraceLength); + } + + if (stringBuilder.Length > maxStackTraceLength) + { + stackTrace = stringBuilder.ToString(0, maxStackTraceLength); + } + else + { + stackTrace = stringBuilder.ToString(); + } + } + finally + { + PoolFactory.SharedStringBuilderPool.Return(stringBuilder); + } + + return stackTrace; + } + + private static void GetInnerExceptionTrace(Exception exception, StringBuilder stringBuilder, int maxStackTraceLength) + { + var innerException = exception.InnerException; + if (innerException != null && stringBuilder.Length < maxStackTraceLength) + { + _ = stringBuilder.Append("InnerException type:"); + _ = stringBuilder.Append(innerException.GetType()); + _ = stringBuilder.Append(" message:"); + _ = stringBuilder.Append(innerException.Message); + _ = stringBuilder.Append(" stack:"); + _ = stringBuilder.Append(innerException.StackTrace); + + GetInnerExceptionTrace(innerException, stringBuilder, maxStackTraceLength); + } + } + + private LogMethodHelper GetHelper() + { + return _provider.CanUsePropertyBagPool + ? LogMethodHelper.GetHelper() + : new LogMethodHelper(); + } + + private void ReturnHelper(LogMethodHelper? helper) + { + if (_provider.CanUsePropertyBagPool && helper != null) + { + LogMethodHelper.ReturnHelper(helper); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerProvider.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerProvider.cs new file mode 100644 index 0000000000..7a15f0e56d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggerProvider.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Shared.Diagnostics; +using OpenTelemetry; +using OpenTelemetry.Logs; + +namespace Microsoft.Extensions.Telemetry.Logging; + +/// +/// OpenTelemetry Logger provider class. +/// +[ProviderAlias("R9")] +internal sealed class LoggerProvider : BaseProvider, ILoggerProvider, ISupportExternalScope +{ + private const int ProcessorShutdownGracePeriodInMs = 5000; + private readonly ConcurrentDictionary _loggers = new(); + private bool _disposed; + private IExternalScopeProvider? _scopeProvider; + + /// + /// Initializes a new instance of the class. + /// + /// Logger options. + /// Collection of enrichers. + /// Collection of processors. + public LoggerProvider( + IOptions loggingOptions, + IEnumerable enrichers, + IEnumerable> processors) + { + var options = Throw.IfMemberNull(loggingOptions, loggingOptions.Value); + + // Accessing Sdk class https://github.com/open-telemetry/opentelemetry-dotnet/blob/7fd37833711e27a02e169de09f3816d1d9557be4/src/OpenTelemetry/Sdk.cs + // is just to activate OpenTelemetry .NET SDK defaults along with its Self-Diagnostics. + _ = Sdk.SuppressInstrumentation; + + SelfDiagnostics.EnsureInitialized(); + + var allProcessors = processors.ToList(); + + Processor = allProcessors.Count switch + { + 0 => null, + 1 => allProcessors[0], + _ => new CompositeProcessor(allProcessors) + }; + + Enrichers = enrichers.ToArray(); + UseFormattedMessage = options.UseFormattedMessage; + IncludeScopes = options.IncludeScopes; + IncludeStackTrace = options.IncludeStackTrace; + MaxStackTraceLength = options.MaxStackTraceLength; + + if (!allProcessors.Exists(p => p is BatchExportProcessor)) + { + CanUsePropertyBagPool = true; + } + } + + internal bool CanUsePropertyBagPool { get; } + internal bool UseFormattedMessage { get; } + internal bool IncludeScopes { get; } + internal BaseProcessor? Processor { get; } + internal ILogEnricher[] Enrichers { get; } + internal bool IncludeStackTrace { get; } + internal int MaxStackTraceLength { get; } + + /// + /// Sets external scope information source for logger provider. + /// + /// scope provider object. + public void SetScopeProvider(IExternalScopeProvider scopeProvider) + { + _scopeProvider = scopeProvider; + + foreach (KeyValuePair entry in _loggers) + { + if (entry.Value is Logger logger) + { + logger.ScopeProvider = _scopeProvider; + } + } + } + + /// + /// Creates a new Microsoft.Extensions.Logging.ILogger instance. + /// + /// The category name for message produced by the logger. + /// ILogger object. + public ILogger CreateLogger(string categoryName) + { + return _loggers.GetOrAdd(categoryName, static (name, t) => new Logger(name, t) + { + ScopeProvider = t._scopeProvider, + }, this); + } + + /// + /// Performs tasks related to freeing up resources. + /// + /// Parameter indicating whether resources need disposing. + protected override void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + _ = Processor?.Shutdown(ProcessorShutdownGracePeriodInMs); + Processor?.Dispose(); + } + + _disposed = true; + + base.Dispose(disposing); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingEventSource.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingEventSource.cs new file mode 100644 index 0000000000..a50597d9db --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingEventSource.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.Tracing; + +namespace Microsoft.Extensions.Telemetry.Logging; + +/// +/// EventSource implementation for R9 logging implementation. +/// +[EventSource(Name = "R9-Logging-Instrumentation")] +internal sealed class LoggingEventSource : EventSource +{ + public static readonly LoggingEventSource Log = new(); + + [NonEvent] + internal void LogException(Exception ex) + { + LogException(ex.ToString()); + } + + [Event(1, Message = "Exception occurred during logging. {exception}.", Level = EventLevel.Error)] + private void LogException(string exception) + { + WriteEvent(1, exception); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingExtensions.cs new file mode 100644 index 0000000000..bed92d406c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingExtensions.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Shared.Diagnostics; +using OpenTelemetry; +using OpenTelemetry.Logs; + +namespace Microsoft.Extensions.Telemetry.Logging; + +/// +/// Extensions for configuring logging. +/// +public static class LoggingExtensions +{ + /// + /// Configure logging. + /// + /// Logging builder. + /// Configuration section that contains . + /// Logging . + public static ILoggingBuilder AddOpenTelemetryLogging(this ILoggingBuilder builder, IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + _ = builder.Services.AddValidatedOptions().Bind(section); + + return builder; + } + + /// + /// Configure logging. + /// + /// Logging builder. + /// Logging configuration options. + /// Logging . + public static ILoggingBuilder AddOpenTelemetryLogging(this ILoggingBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + _ = builder.Services.AddValidatedOptions().Configure(configure); + + return builder; + } + + /// + /// Configure logging with default options. + /// + /// Logging builder. + /// Logging . + public static ILoggingBuilder AddOpenTelemetryLogging(this ILoggingBuilder builder) => builder.AddOpenTelemetryLogging(_ => { }); + + /// + /// Adds a logging processor to the builder. + /// + /// The builder to add the processor to. + /// Log processor to add. + /// Returns for chaining. + public static ILoggingBuilder AddProcessor(this ILoggingBuilder builder, BaseProcessor processor) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(processor); + + _ = builder.Services.AddSingleton(processor); + + return builder; + } + + /// + /// Adds a logging processor to the builder. + /// + /// Type of processor to add. + /// The builder to add the processor to. + /// Returns for chaining. + public static ILoggingBuilder AddProcessor(this ILoggingBuilder builder) + where T : BaseProcessor + { + _ = Throw.IfNull(builder); + + _ = builder.Services.AddSingleton, T>(); + + return builder; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingOptions.cs new file mode 100644 index 0000000000..c301fef100 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingOptions.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Telemetry.Logging; + +/// +/// Options for logger. +/// +public class LoggingOptions +{ + private const int MaxDefinedStackTraceLength = 32768; + private const int MinDefinedStackTraceLength = 2048; + private const int DefaultStackTraceLength = 4096; + + /// + /// Gets or sets a value indicating whether to include log scopes in + /// captured log state. + /// + /// + /// Default set to . + /// + public bool IncludeScopes { get; set; } + + /// + /// Gets or sets a value indicating whether to format the message included in captured log state. + /// + /// + /// When set to the placeholders in the message will be replaced by the actual values + /// otherwise the message template will be included as-is without replacements. + /// + /// Default set to . + /// + public bool UseFormattedMessage { get; set; } + + /// + /// Gets or sets a value indicating whether to include stack trace when exception is logged. + /// + /// + /// When set to and exceptions are logged, the logger will add exception stack trace + /// with inner exception as a separate key-value pair with key 'stackTrace'. The max length of the column + /// defaults to 4096 characters and can be modified by setting the property. + /// Stack trace beyond the current limit will be truncated. + /// + /// Default set to . + /// + public bool IncludeStackTrace { get; set; } + + /// + /// Gets or sets a value indicating maximum stack trace length configured by the user. + /// + /// + /// When set to a value less than 2kb or greater than 32kb, an exception will be thrown. + /// + /// Default set to 4096. + /// + [Experimental] + [Range(MinDefinedStackTraceLength, MaxDefinedStackTraceLength, ErrorMessage = "Maximum stack trace length should be between 2kb and 32kb")] + public int MaxStackTraceLength { get; set; } = DefaultStackTraceLength; +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingOptionsValidator.cs new file mode 100644 index 0000000000..9d9489c9a6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Logging/LoggingOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Telemetry.Logging; + +[OptionsValidator] +internal sealed partial class LoggingOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/EventCountersCollectorOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/EventCountersCollectorOptions.cs new file mode 100644 index 0000000000..fe6a1a33f2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/EventCountersCollectorOptions.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Microsoft.Shared.Data.Validation; + +namespace Microsoft.Extensions.Telemetry.Metering; + +/// +/// Configuration options for . +/// +public class EventCountersCollectorOptions +{ + private static readonly TimeSpan _defaultSamplingInterval = TimeSpan.FromSeconds(1); + +#if NET5_0_OR_GREATER + /// + /// This is a work-around for this issue. + /// The field is intended to be used on .NET 5 only, we ship it for newer TFMs to resolve package compositional issues. + /// See discussion in for additional context. + /// + private static readonly TimeSpan _defaultEventListenerRecyclingInterval = TimeSpan.FromHours(1); +#endif + + /// + /// Gets or sets a list of EventSources and CounterNames to listen for. + /// + /// + /// It is a dictionary of EventSource to the set of counters that needs to be collected from the event source. + /// Please visit + /// for well known event counters and their availability. + /// Default set to an empty dictionary. + /// + [Required] + [Microsoft.Shared.Data.Validation.Length(1)] +#pragma warning disable CA2227 // Collection properties should be read only +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + public IDictionary> Counters { get; set; } +#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +#pragma warning restore CA2227 // Collection properties should be read only + = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets or sets a sampling interval for counters. + /// + /// + /// Default set to 1 second. + /// + [TimeSpan("00:00:01", "00:10:00")] + public TimeSpan SamplingInterval { get; set; } = _defaultSamplingInterval; + +#if NET5_0_OR_GREATER + /// + /// Gets or sets the interval at which to recycle the . + /// + /// + /// This is a work-around for this issue. + /// Default set to 1 hour. + /// This only has an effect on .NET 5, it is ignored for .NET 6 and above. + /// + // The property is intended to be used on .NET 5 only, we ship it for newer TFMs to resolve package compositional issues. Refer to discussion for details: + // https://domoreexp.visualstudio.com/R9/_git/SDK/pullrequest/552703?_a=files&path=/src/Extensions/Metering.Collectors.EventCounters/EventCountersCollectorOptions.cs&discussionId=9983912 + [TimeSpan("00:10:00", "06:00:00")] + public TimeSpan EventListenerRecyclingInterval { get; set; } = _defaultEventListenerRecyclingInterval; +#endif +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/EventCountersExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/EventCountersExtensions.cs new file mode 100644 index 0000000000..1878b736ce --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/EventCountersExtensions.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Telemetry.Metering.Internal; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Metering; + +/// +/// Extensions for registering . +/// +public static class EventCountersExtensions +{ + /// + /// Adds to the specified . + /// + /// The to add the service to. + /// An to configure the provided . + /// The so that additional calls can be chained. + /// Either or is . + public static IServiceCollection AddEventCounterCollector(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + _ = AddEventCounterCollectorInternal(services) + .Configure(configure); + + return services; + } + + /// + /// Adds to the specified . + /// + /// The to add the service to. + /// An to configure the provided . + /// The so that additional calls can be chained. + /// Either or is . + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(EventCountersCollectorOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed by [DynamicDependency]")] + public static IServiceCollection AddEventCounterCollector(this IServiceCollection services, IConfigurationSection section) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(section); + + var optionsBuilder = AddEventCounterCollectorInternal(services); + +#if NET7_0_OR_GREATER + _ = optionsBuilder.Bind(section); +#else + // Regular call: + // optionsBuilder.Bind(section) + + // Translates to: + // optionsBuilder.Services.Configure(optionsBuilder.Name, section) + + // Above call to Configure() contains following: + // services.AddSingleton>( + // new ConfigurationChangeTokenSource(optionsBuilder.Name, section)) + + // services.AddSingleton>( + // new NamedConfigureFromConfigurationOptions(optionsBuilder.Name, section)) + + // Since NamedConfigureFromConfigurationOptions calls ConfigurationBinder.Bind(), + // we need to use our custom version that calls custom binder with added BindToSet() method: + _ = services.AddSingleton>( + new ConfigurationChangeTokenSource(optionsBuilder.Name, section)); + + _ = services.AddSingleton>( + new CustomConfigureNamedOptions(optionsBuilder.Name, section)); +#endif + + return services; + } + + private static OptionsBuilder AddEventCounterCollectorInternal(IServiceCollection services) + { + var optionsBuilder = services + .AddValidatedOptions(); + + services.TryAddEnumerable(ServiceDescriptor.Singleton, EventCountersValidator>()); + _ = services.RegisterMetering(); + _ = services.AddActivatedSingleton(); + + return optionsBuilder; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/EventCountersListener.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/EventCountersListener.cs new file mode 100644 index 0000000000..5e0eefb954 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/EventCountersListener.cs @@ -0,0 +1,243 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Diagnostics.Tracing; +using System.Globalization; +using System.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Metering.Internal; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Metering; + +/// +/// An that allows to send data written by +/// instances of an to the metering pipeline. +/// +internal sealed class EventCountersListener : EventListener +{ + private readonly bool _isInitialized; + private readonly Dictionary _eventSourceSettings; + private readonly Meter _meter; + private readonly FrozenDictionary> _counters; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The options. + /// The meter provider. + /// Logger instance. + public EventCountersListener( + IOptions options, + Meter meter, + ILogger? logger = null) + { + var value = Throw.IfMemberNull(options, options.Value); + _counters = CreateCountersDictionary(value.Counters); + _meter = meter; + _logger = logger ?? NullLoggerFactory.Instance.CreateLogger(); + _eventSourceSettings = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["EventCounterIntervalSec"] = value.SamplingInterval.TotalSeconds.ToString(CultureInfo.InvariantCulture) + }; + + _logger.ConfiguredEventCountersOptions(value); + + _isInitialized = true; + foreach (var eventSource in EventSource.GetSources()) + { + EnableIfNeeded(eventSource); + } + } + + /// + /// Called for all existing event sources when the event listener is created and + /// when a new event source is attached to the listener. + /// + /// The event source. + /// + /// This method is called whenever a new eventSource is 'attached' to the dispatcher. + /// This can happen for all existing EventSources when the EventListener is created + /// as well as for any EventSources that come into existence after the EventListener has been created. + /// These 'catch up' events are called during the construction of the EventListener. + /// Subclasses need to be prepared for that. In a multi-threaded environment, + /// it is possible that 'OnEventWritten' callbacks for a particular eventSource to occur + /// BEFORE the OnEventSourceCreated is issued. + /// + protected override void OnEventSourceCreated(EventSource eventSource) + { + if (!_isInitialized || eventSource == null) + { + return; + } + + EnableIfNeeded(eventSource); + } + + /// + /// Called whenever an event has been written by an event source for which the event listener has enabled events. + /// + /// The event arguments that describe the event. + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + if (!_isInitialized) + { + return; + } + + if (eventData.EventName == null) + { + _logger.EventNameIsNull(eventData.EventSource.Name); + return; + } + + if (eventData.Payload == null) + { + _logger.PayloadIsNull(eventData.EventSource.Name, eventData.EventName); + return; + } + + if (!eventData.EventName.Equals("EventCounters", StringComparison.OrdinalIgnoreCase)) + { + _logger.EventNameIsNotEventCounters(eventData.EventName); + return; + } + + if (_counters.TryGetValue(eventData.EventSource.Name, out var counters)) + { + for (var i = 0; i < eventData.Payload.Count; ++i) + { + if (eventData.Payload[i] is not IDictionary eventPayload) + { + continue; + } + + if (!eventPayload.TryGetValue("CounterType", out var counterType)) + { + _logger.EventPayloadDoesNotContainCounterType(eventData.EventSource.Name, eventData.EventName); + continue; + } + + if (!eventPayload.TryGetValue("Name", out var counterName)) + { + _logger.EventPayloadDoesNotContainName(eventData.EventSource.Name, eventData.EventName); + continue; + } + + var name = counterName.ToString(); + + if (string.IsNullOrEmpty(name)) + { + _logger.CounterNameIsEmpty(eventData.EventSource.Name, eventData.EventName); + continue; + } + + if (!counters.Contains(name)) + { + _logger.CounterNotEnabled(name, eventData.EventSource.Name, eventData.EventName); + continue; + } + + var type = counterType.ToString(); + + _logger.ReceivedEventOfType(eventData.EventSource.Name, counterName, type); + if ("Sum".Equals(type, StringComparison.OrdinalIgnoreCase)) + { + var fullName = $"{eventData.EventSource.Name}|{counterName}"; + RecordSumEvent(eventPayload, fullName); + } + else if ("Mean".Equals(type, StringComparison.OrdinalIgnoreCase)) + { + var fullName = $"{eventData.EventSource.Name}|{counterName}"; + RecordMeanEvent(eventPayload, fullName); + } + } + } + } + + private static FrozenDictionary> CreateCountersDictionary(IDictionary> counters) + { + var d = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + foreach (var kvp in counters) + { + if (kvp.Value == null || kvp.Value.Count == 0) + { + continue; + } + + d.Add(kvp.Key, new HashSet(kvp.Value, StringComparer.OrdinalIgnoreCase)); + } + + return d.Select(x => new KeyValuePair>(x.Key, + x.Value.ToFrozenSet(StringComparer.Ordinal, optimizeForReading: true))).ToFrozenDictionary(StringComparer.OrdinalIgnoreCase, optimizeForReading: true); + } + + private bool TryConvertToLong(object? value, out long convertedValue) + { + try + { + convertedValue = Convert.ToInt64(value, CultureInfo.InvariantCulture); + return true; + } + catch (OverflowException) + { + // The number is invalid, ignore processing it and don't bubble up exception. + _logger.OverflowExceptionWhileConversion(value); + } + catch (FormatException) + { + // The number is invalid, ignore processing it and don't bubble up exception. + _logger.FormatExceptionWhileConversion(value); + } + + convertedValue = 0; + return false; + } + + private void EnableIfNeeded(EventSource eventSource) + { + if (!_counters.ContainsKey(eventSource.Name)) + { + return; + } + + _logger.EnablingEventSource(eventSource.Name); + EnableEvents(eventSource, EventLevel.LogAlways, EventKeywords.All, _eventSourceSettings); + } + + private void RecordSumEvent(IDictionary eventPayload, string counterName) + { + if (!eventPayload.TryGetValue("Increment", out var payloadValue)) + { + return; + } + + if (TryConvertToLong(payloadValue, out long metricValue)) + { + var metric = _meter.CreateCounter(counterName); + metric.Add(metricValue); + } + } + + private void RecordMeanEvent(IDictionary eventPayload, string counterName) + { + if (!eventPayload.TryGetValue("Mean", out var payloadValue)) + { + return; + } + + if (TryConvertToLong(payloadValue, out long metricValue)) + { + var metric = _meter.CreateHistogram(counterName); + metric.Record(metricValue); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/CustomConfigurationBinder.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/CustomConfigurationBinder.cs new file mode 100644 index 0000000000..5be81e5762 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/CustomConfigurationBinder.cs @@ -0,0 +1,510 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !NET7_0_OR_GREATER + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Metering.Internal; + +// Contains modifications to bind ISet and IReadOnlySet within options classes properly +// Originally taken from: https://source.dot.net/#Microsoft.Extensions.Configuration.Binder/ConfigurationBinder.cs +// This class can be removed once https://github.com/dotnet/runtime/issues/66141 is resolved +// Tracked under https://domoreexp.visualstudio.com/R9/_workitems/edit/2285691/ +[SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Original impl")] +[ExcludeFromCodeCoverage] +internal static class CustomConfigurationBinder +{ + private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; + private const string TrimmingWarningMessage = "In case the type is non-primitive, the trimmer cannot statically analyze the object's type so its members may be trimmed."; + private const string InstanceGetTypeTrimmingWarningMessage = "Cannot statically analyze the type of instance so its members may be trimmed"; + private const string PropertyTrimmingWarningMessage = "Cannot statically analyze property.PropertyType so its members may be trimmed."; + + /// + /// Attempts to bind the given object instance to configuration values by matching property names against configuration keys recursively. + /// + /// The configuration instance to bind. + /// The object to bind. + [RequiresUnreferencedCode(InstanceGetTypeTrimmingWarningMessage)] + internal static void Bind(IConfiguration configuration, object? instance) + { + if (instance != null) + { + _ = BindInstance(instance.GetType(), instance, configuration); + } + } + + [RequiresUnreferencedCode(PropertyTrimmingWarningMessage)] + private static void BindNonScalar(this IConfiguration configuration, object? instance) + { + if (instance != null) + { + List modelProperties = GetAllProperties(instance.GetType()); + + foreach (PropertyInfo property in modelProperties) + { + BindProperty(property, instance, configuration); + } + } + } + + [RequiresUnreferencedCode(PropertyTrimmingWarningMessage)] + private static void BindProperty(PropertyInfo property, object instance, IConfiguration config) + { + // We don't support set only, non public, or indexer properties + if (property.GetMethod == null || + (!property.GetMethod.IsPublic) || + property.GetMethod.GetParameters().Length > 0) + { + return; + } + + object? propertyValue = property.GetValue(instance); + bool hasSetter = property.SetMethod != null && property.SetMethod.IsPublic; + + if (propertyValue == null && !hasSetter) + { + // Property doesn't have a value and we cannot set it so there is no + // point in going further down the graph + return; + } + + propertyValue = GetPropertyValue(property, instance, config); + + if (propertyValue != null && hasSetter) + { + property.SetValue(instance, propertyValue); + } + } + + [RequiresUnreferencedCode("Cannot statically analyze what the element type is of the object collection in type so its members may be trimmed.")] + private static object? BindToCollection(Type type, IConfiguration config) + { + Type genericType = typeof(List<>).MakeGenericType(type.GenericTypeArguments[0]); + object? instance = Activator.CreateInstance(genericType); + BindCollection(instance, genericType, config); + return instance; + } + + [RequiresUnreferencedCode("Cannot statically analyze what the element type is of the object collection in type so its members may be trimmed.")] + private static object? BindToSet(Type type, IConfiguration config) + { + Type genericType = typeof(HashSet<>).MakeGenericType(type.GenericTypeArguments[0]); + object? instance = Activator.CreateInstance(genericType); + BindCollection(instance, genericType, config); + return instance; + } + + // Try to create an array/dictionary instance to back various collection interfaces + [RequiresUnreferencedCode("In case type is a Dictionary, cannot statically analyze what the element type is of the value objects in the dictionary so its members may be trimmed.")] + private static object? AttemptBindToCollectionInterfaces( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] + Type type, + IConfiguration config) + { + if (!type.IsInterface) + { + return null; + } + + Type? collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyList<>), type); + if (collectionInterface != null) + { + // IEnumerable is guaranteed to have exactly one parameter + return BindToCollection(type, config); + } + + collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyDictionary<,>), type); + if (collectionInterface != null) + { + Type dictionaryType = typeof(Dictionary<,>).MakeGenericType(type.GenericTypeArguments[0], type.GenericTypeArguments[1]); + object? instance = Activator.CreateInstance(dictionaryType); + BindDictionary(instance, dictionaryType, config); + return instance; + } + + collectionInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type); + if (collectionInterface != null) + { + object? instance = Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(type.GenericTypeArguments[0], type.GenericTypeArguments[1])); + BindDictionary(instance, collectionInterface, config); + return instance; + } + +#if NET5_0_OR_GREATER + collectionInterface = FindOpenGenericInterface(typeof(IReadOnlySet<>), type); + if (collectionInterface != null) + { + // IReadOnlySet is guaranteed to have exactly one parameter + return BindToSet(type, config); + } +#endif + + collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyCollection<>), type); + if (collectionInterface != null) + { + // IReadOnlyCollection is guaranteed to have exactly one parameter + return BindToCollection(type, config); + } + + collectionInterface = FindOpenGenericInterface(typeof(ISet<>), type); + if (collectionInterface != null) + { + // ISet is guaranteed to have exactly one parameter + return BindToSet(type, config); + } + + collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type); + if (collectionInterface != null) + { + // ICollection is guaranteed to have exactly one parameter + return BindToCollection(type, config); + } + + collectionInterface = FindOpenGenericInterface(typeof(IEnumerable<>), type); + if (collectionInterface != null) + { + // IEnumerable is guaranteed to have exactly one parameter + return BindToCollection(type, config); + } + + return null; + } + + [RequiresUnreferencedCode(TrimmingWarningMessage)] + private static object? BindInstance( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] + Type type, + object? instance, IConfiguration config) + { + // if binding IConfigurationSection, break early + if (type == typeof(IConfigurationSection)) + { + return config; + } + + var section = config as IConfigurationSection; + string? configValue = section?.Value; + if (configValue != null && TryConvertValue(type, configValue, section?.Path, out object? convertedValue, out Exception? error)) + { + if (error != null) + { + throw error; + } + + // Leaf nodes are always reinitialized + return convertedValue; + } + + if (config != null && config.GetChildren().Any()) + { + // If we don't have an instance, try to create one + if (instance == null) + { + // We are already done if binding to a new collection instance worked + instance = AttemptBindToCollectionInterfaces(type, config); + if (instance != null) + { + return instance; + } + + instance = CreateInstance(type); + } + + // See if its a Dictionary + Type? collectionInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type); + if (collectionInterface != null) + { + BindDictionary(instance, collectionInterface, config); + } + else if (type.IsArray) + { + instance = BindArray((Array)instance!, config); + } + else + { + // See if its an ICollection + collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type); + if (collectionInterface != null) + { + BindCollection(instance, collectionInterface, config); + } + else + { + BindNonScalar(config, instance); + } + } + } + + return instance; + } + + private static object? CreateInstance([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type) + { + if (type.IsInterface || type.IsAbstract) + { + Throw.InvalidOperationException($"Cannot create instance of type '{type}' because it is either abstract or an interface."); + } + + if (type.IsArray) + { + if (type.GetArrayRank() > 1) + { + Throw.InvalidOperationException( + $"Cannot create instance of type '{type}' because multidimensional arrays are not supported."); + } + + return Array.CreateInstance(type.GetElementType()!, 0); + } + + if (!type.IsValueType) + { + bool hasDefaultConstructor = type.GetConstructors(DeclaredOnlyLookup) + .Any(ctor => ctor.IsPublic && ctor.GetParameters().Length == 0); + + if (!hasDefaultConstructor) + { + Throw.InvalidOperationException( + $"Cannot create instance of type '{type}' because it is missing a public parameterless constructor."); + } + } + + try + { + return Activator.CreateInstance(type); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to create instance of type '{type}'.", ex); + } + } + + [RequiresUnreferencedCode("Cannot statically analyze what the element type is of the value objects in the dictionary so its members may be trimmed.")] + private static void BindDictionary( + object? dictionary, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] + Type dictionaryType, + IConfiguration config) + { + // IDictionary is guaranteed to have exactly two parameters + Type keyType = dictionaryType.GenericTypeArguments[0]; + Type valueType = dictionaryType.GenericTypeArguments[1]; + bool keyTypeIsEnum = keyType.IsEnum; + + if (keyType != typeof(string) && !keyTypeIsEnum) + { + // We only support string and enum keys + return; + } + + MethodInfo tryGetValue = dictionaryType.GetMethod("TryGetValue")!; + PropertyInfo setter = dictionaryType.GetProperty("Item", DeclaredOnlyLookup)!; + foreach (IConfigurationSection child in config.GetChildren()) + { + try + { + object key = keyTypeIsEnum ? Enum.Parse(keyType, child.Key) : child.Key; + var args = new object?[] { key, null }; + _ = tryGetValue.Invoke(dictionary, args); + object? item = BindInstance( + type: valueType, + instance: args[1], + config: child); + + if (item != null) + { + setter.SetValue(dictionary, item, new[] { key }); + } + } + catch + { + // ignored + } + } + } + + [RequiresUnreferencedCode("Cannot statically analyze what the element type is of the object collection so its members may be trimmed.")] + private static void BindCollection( + object? collection, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] + Type collectionType, + IConfiguration config) + { + // ICollection is guaranteed to have exactly one parameter + Type itemType = collectionType.GenericTypeArguments[0]; + MethodInfo? addMethod = collectionType.GetMethod("Add", DeclaredOnlyLookup); + + foreach (IConfigurationSection section in config.GetChildren()) + { + try + { + object? item = BindInstance( + type: itemType, + instance: null, + config: section); + + if (item != null) + { + _ = addMethod?.Invoke(collection, new[] { item }); + } + } + catch + { + // ignored + } + } + } + + [RequiresUnreferencedCode("Cannot statically analyze what the element type is of the Array so its members may be trimmed.")] + private static Array BindArray(Array source, IConfiguration config) + { + IConfigurationSection[] children = config.GetChildren().ToArray(); + int arrayLength = source.Length; + Type elementType = source.GetType().GetElementType()!; + var newArray = Array.CreateInstance(elementType, arrayLength + children.Length); + + // binding to array has to preserve already initialized arrays with values + if (arrayLength > 0) + { + Array.Copy(source, newArray, arrayLength); + } + + for (int i = 0; i < children.Length; i++) + { + try + { + object? item = BindInstance( + type: elementType, + instance: null, + config: children[i]); + + if (item != null) + { + newArray.SetValue(item, arrayLength + i); + } + } + catch + { + // ignored + } + } + + return newArray; + } + + [RequiresUnreferencedCode(TrimmingWarningMessage)] + private static bool TryConvertValue( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] + Type type, + string value, string? path, out object? result, out Exception? error) + { + error = null; + result = null; + if (type == typeof(object)) + { + result = value; + return true; + } + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + if (string.IsNullOrEmpty(value)) + { + return true; + } + + return TryConvertValue(Nullable.GetUnderlyingType(type)!, value, path, out result, out error); + } + + TypeConverter converter = TypeDescriptor.GetConverter(type); + if (converter.CanConvertFrom(typeof(string))) + { + try + { + result = converter.ConvertFromInvariantString(value); + } + catch (Exception ex) + { + error = new InvalidOperationException( + $"Failed to convert configuration value at '{path}' to type '{type}'.", ex); + } + + return true; + } + + if (type == typeof(byte[])) + { + try + { + result = Convert.FromBase64String(value); + } + catch (FormatException ex) + { + error = new InvalidOperationException($"Failed to convert configuration value at '{path}' to type '{type}'.", ex); + } + + return true; + } + + return false; + } + + private static Type? FindOpenGenericInterface( + Type expected, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] + Type actual) + { + if (actual.IsGenericType && + actual.GetGenericTypeDefinition() == expected) + { + return actual; + } + + Type[] interfaces = actual.GetInterfaces(); + foreach (Type interfaceType in interfaces) + { + if (interfaceType.IsGenericType && + interfaceType.GetGenericTypeDefinition() == expected) + { + return interfaceType; + } + } + + return null; + } + + private static List GetAllProperties( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] + Type type) + { + var allProperties = new List(); + + Type? baseType = type; + do + { + allProperties.AddRange(baseType!.GetProperties(DeclaredOnlyLookup)); + baseType = baseType.BaseType; + } + while (baseType != typeof(object)); + + return allProperties; + } + + [RequiresUnreferencedCode(PropertyTrimmingWarningMessage)] + private static object? GetPropertyValue(PropertyInfo property, object instance, IConfiguration config) + { + return BindInstance( + property.PropertyType, + property.GetValue(instance), + config.GetSection(property.Name)); + } +} + +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/CustomConfigureNamedOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/CustomConfigureNamedOptions.cs new file mode 100644 index 0000000000..5b9b4d8a50 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/CustomConfigureNamedOptions.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !NET7_0_OR_GREATER + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Telemetry.Metering.Internal; + +// Uses custom configuration binder - to bind ISet and IReadOnlySet within options classes properly +// Inspired by: https://source.dot.net/#Microsoft.Extensions.Options.ConfigurationExtensions/NamedConfigureFromConfigurationOptions.cs +// This class can be removed once https://github.com/dotnet/runtime/issues/66141 is resolved +internal sealed class CustomConfigureNamedOptions : ConfigureNamedOptions +{ + public CustomConfigureNamedOptions(string name, IConfigurationSection section) + : base(name, options => BindFromOptions(options, section)) + { + } + + [UnconditionalSuppressMessage("Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Original impl")] + private static void BindFromOptions(EventCountersCollectorOptions options, IConfiguration section) + => CustomConfigurationBinder.Bind(section, options); +} +#endif diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/EventCountersCollectorOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/EventCountersCollectorOptionsValidator.cs new file mode 100644 index 0000000000..8eb3e85bc8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/EventCountersCollectorOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Telemetry.Metering.Internal; + +[OptionsValidator] +internal sealed partial class EventCountersCollectorOptionsValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/EventCountersValidator.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/EventCountersValidator.cs new file mode 100644 index 0000000000..a71247e858 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/EventCountersValidator.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Data.Validation; +using Validation = Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Telemetry.Metering.Internal; + +internal sealed class EventCountersValidator : IValidateOptions +{ + private const string MemberName = nameof(EventCountersCollectorOptions.Counters); + + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(EventCountersCollectorOptions))] + [UnconditionalSuppressMessage("Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed by [DynamicDependency]")] + public ValidateOptionsResult Validate(string? name, EventCountersCollectorOptions options) + { + if (options.Counters is null) + { + // Nullness is covered in source-generated validator + return ValidateOptionsResult.Skip; + } + + var baseName = string.IsNullOrEmpty(name) + ? nameof(EventCountersCollectorOptions) + : name; + + var context = new ValidationContext(options); + var builder = new ValidateOptionsResultBuilder(); + var requiredAttribute = new RequiredAttribute(); + var lengthAttribute = new Microsoft.Shared.Data.Validation.LengthAttribute(1); + foreach (var pair in options.Counters) + { + if (pair.Value is null) + { + context.MemberName = MemberName + "[\"" + pair.Key + "\"]"; + context.DisplayName = baseName + "." + context.MemberName; + builder.AddResult(requiredAttribute.GetValidationResult(pair.Value, context)); + } + else if (pair.Value.Count == 0) + { + context.MemberName = MemberName + "[\"" + pair.Key + "\"]"; + context.DisplayName = baseName + "." + context.MemberName; + builder.AddResult(lengthAttribute.GetValidationResult(pair.Value, context)); + } + } + + return builder.Build(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/Log.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/Log.cs new file mode 100644 index 0000000000..38fbabd4a1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Metering.Collectors.EventCounters/Internal/Log.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.Extensions.Telemetry.Metering.Internal; + +#pragma warning disable S109 // Magic numbers should not be used +internal static partial class Log +{ + /// + /// Logs `Configured EventCounters options: {value}` at `Information` level. + /// + [LogMethod(0, LogLevel.Information, "Configured EventCounters options: {value}")] + internal static partial void ConfiguredEventCountersOptions(this ILogger logger, EventCountersCollectorOptions value); + + /// + /// Logs `Enabling event source: {name}` at `Information` level. + /// + [LogMethod(1, LogLevel.Information, "Enabling event source: {name}")] + internal static partial void EnablingEventSource(this ILogger logger, string name); + + /// + /// Logs `Invalid value {value} resulting in overflow exception during conversion` at `Warning` level. + /// + [LogMethod(2, LogLevel.Warning, "Invalid value {value} resulting in overflow exception during conversion")] + internal static partial void OverflowExceptionWhileConversion(this ILogger logger, object? value); + + /// + /// Logs `Invalid value {value} resulting in Format exception during conversion` at `Warning` level. + /// + [LogMethod(3, LogLevel.Warning, "Invalid value {value} resulting in Format exception during conversion")] + internal static partial void FormatExceptionWhileConversion(this ILogger logger, object? value); + + /// + /// Logs `Event name is null, eventSource {name}` at `Debug` level. + /// + [LogMethod(4, LogLevel.Debug, "Event name is null, eventSource {name}")] + internal static partial void EventNameIsNull(this ILogger logger, string name); + + /// + /// Logs `Payload is null for event {name}:{eventName}` at `Debug` level. + /// + [LogMethod(5, LogLevel.Debug, "Payload is null for event {eventSourceName}:{eventName}")] + internal static partial void PayloadIsNull(this ILogger logger, string eventSourceName, string eventName); + + /// + /// Logs `EventName: `{eventName}` is not `EventCounters`` at `Debug` level. + /// + [LogMethod(6, LogLevel.Debug, "EventName: `{eventName}` is not `EventCounters`")] + internal static partial void EventNameIsNotEventCounters(this ILogger logger, string eventName); + + /// + /// Logs `No counters registered for eventSource: {eventSourceName}` at `Debug` level. + /// + [LogMethod(7, LogLevel.Debug, "No counters registered for eventSource: {eventSourceName}")] + internal static partial void NoCountersRegisteredForEventSource(this ILogger logger, string eventSourceName); + + /// + /// Logs `Event payload for event {eventSourceName}:{eventName} does not contain `CounterType`` at `Trace` level. + /// + [LogMethod(8, LogLevel.Trace, "Event payload for event {eventSourceName}:{eventName} does not contain `CounterType`")] + internal static partial void EventPayloadDoesNotContainCounterType(this ILogger logger, string eventSourceName, string eventName); + + /// + /// Logs `Event payload for event {eventSourceName}:{eventName} does not contain `Name`` at `Trace` level. + /// + [LogMethod(9, LogLevel.Trace, "Event payload for event {eventSourceName}:{eventName} does not contain `Name`")] + internal static partial void EventPayloadDoesNotContainName(this ILogger logger, string eventSourceName, string eventName); + + /// + /// Logs `Counter name is empty for event {eventSourceName}:{eventName}` at `Trace` level. + /// + [LogMethod(10, LogLevel.Trace, "Counter name is empty for event {eventSourceName}:{eventName}")] + internal static partial void CounterNameIsEmpty(this ILogger logger, string eventSourceName, string eventName); + + /// + /// Logs `Counter `{counterName}` for eventSource {eventSourceName}:{eventName} not enabled` at `Trace` level. + /// + [LogMethod(11, LogLevel.Trace, "Counter `{counterName}` for eventSource {eventSourceName}:{eventName} not enabled")] + internal static partial void CounterNotEnabled(this ILogger logger, string counterName, string eventSourceName, string eventName); + + /// + /// Logs `Received event {eventSourceName}:{counterName} of type {type}` at `Trace` level. + /// + [LogMethod(12, LogLevel.Trace, "Received event {eventSourceName}:{counterName} of type {type}")] + internal static partial void ReceivedEventOfType(this ILogger logger, string eventSourceName, object counterName, string? type); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Metering/MeteringOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Metering/MeteringOptions.cs new file mode 100644 index 0000000000..c78b3f76ff --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Metering/MeteringOptions.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Telemetry.Metering; + +/// +/// Options for configuring metering. +/// +[Experimental] +public class MeteringOptions +{ + /// + /// Gets or sets maximum number of Metric streams supported. + /// + /// + /// Default value is set to 1000. + /// + public int MaxMetricStreams { get; set; } = 1000; + + /// + /// Gets or sets the maximum number of metric points allowed per metric stream. + /// + /// + /// Default value is set to 2000. + /// + public int MaxMetricPointsPerStream { get; set; } = 2000; + + /// + /// Gets or sets default meter state to be used for emitting metrics. + /// + /// + /// Default set to . + /// + public MeteringState MeterState { get; set; } = MeteringState.Enabled; + + /// + /// Gets or sets metering state override to control metering state for specific categories. + /// + /// + /// + /// { + /// "MeterState": { + /// "Microsoft.Extensions.Cache.Meter": "Disabled" + /// } + /// } + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", + Justification = "Set for options validation")] + public IDictionary MeterStateOverrides { get; set; } = + new Dictionary(); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Metering/MeteringState.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Metering/MeteringState.cs new file mode 100644 index 0000000000..922acf5264 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Metering/MeteringState.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Telemetry.Metering; + +/// +/// Enum for supported metering states. +/// +public enum MeteringState +{ + /// + /// Metering is disabled. + /// + Disabled, + + /// + /// Metering is enabled. + /// + Enabled +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Metering/OTelMeteringExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Metering/OTelMeteringExtensions.cs new file mode 100644 index 0000000000..5c1073a0e4 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Metering/OTelMeteringExtensions.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; +using OpenTelemetry.Metrics; + +namespace Microsoft.Extensions.Telemetry.Metering; + +/// +/// Metering extensions for OpenTelemetry based metrics. +/// +[Experimental] +public static class OTelMeteringExtensions +{ + /// + /// Extension to configure metering. + /// + /// instance. + /// Configuration section that contains . + /// Returns for chaining. + /// When the extension is called without hosting package. + [Experimental] + public static MeterProviderBuilder AddMetering( + this MeterProviderBuilder builder, + IConfigurationSection configurationSection) + { + _ = Throw.IfNull(builder); + + _ = builder.ConfigureServices(services => services.Configure(configurationSection)); + return builder.AddMetering(); + } + + /// + /// Extension to configure metering. + /// + /// instance. + /// The configuration delegate. + /// Returns for chaining. + /// When the extension is called without hosting package. + [Experimental] + public static MeterProviderBuilder AddMetering( + this MeterProviderBuilder builder, + Action? configure = null) + { + _ = Throw.IfNull(builder); + + return builder.ConfigureServices(services => + services.ConfigureOpenTelemetryMeterProvider((sp, meterProviderBuilder) => + { + _ = meterProviderBuilder.AddMetering(sp.GetOptions(), configure); + })); + } + + private static MeterProviderBuilder AddMetering( + this MeterProviderBuilder builder, + MeteringOptions options, + Action? configure = null) + { + configure?.Invoke(options); + + const string Wildcard = "*"; + if (options.MeterState == MeteringState.Enabled) + { + _ = builder.AddMeter(Wildcard); + } + else if (options.MeterStateOverrides.Count > 0) + { + foreach (var meterStateOverride in options.MeterStateOverrides) + { + if (meterStateOverride.Value == MeteringState.Enabled) + { + _ = builder.AddMeter($"{meterStateOverride.Key}{Wildcard}"); + } + } + } + + return builder + .SetMaxMetricStreams(options.MaxMetricStreams) + .SetMaxMetricPointsPerMetricStream(options.MaxMetricPointsPerStream) + .AddView((instrument) => + { + if (GetMeterState(options, instrument.Meter.Name) == MeteringState.Disabled) + { + return MetricStreamConfiguration.Drop; + } + + return null; + }); + } + + private static T GetOptions(this IServiceProvider serviceProvider) + where T : class, new() + { + IOptions? options = (IOptions?)serviceProvider.GetService(typeof(IOptions)); + + // Note: options could be null if user never invoked services.AddOptions(). + return options?.Value ?? new T(); + } + + private static bool IsBetterCategoryMatch(string newCategory, string currentCategory, string typeName) + { + if (string.IsNullOrEmpty(newCategory)) + { + return false; + } + + if (!string.IsNullOrEmpty(currentCategory) && currentCategory.Length > newCategory.Length) + { + return false; + } + + if (!typeName.StartsWith(newCategory, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return true; + } + + private static MeteringState GetMeterState(MeteringOptions meteringOptions, string typeName) + { + MeteringState meterState = meteringOptions.MeterState; + string currentCategory = string.Empty; + + foreach (var meterStateOverride in meteringOptions.MeterStateOverrides) + { + if (IsBetterCategoryMatch(meterStateOverride.Key, currentCategory, typeName)) + { + currentCategory = meterStateOverride.Key; + meterState = meterStateOverride.Value; + } + } + + return meterState; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj new file mode 100644 index 0000000000..81b104e972 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Microsoft.Extensions.Telemetry.csproj @@ -0,0 +1,52 @@ + + + Microsoft.Extensions.Telemetry + Provides canonical implementations of telemetry abstractions + Telemetry + + + + true + true + true + true + true + true + true + true + true + + + + normal + 79 + 100 + 90 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/HttpHeadersRedactor.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/HttpHeadersRedactor.cs new file mode 100644 index 0000000000..f3d7da19d0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/HttpHeadersRedactor.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Telemetry.Internal; + +internal sealed class HttpHeadersRedactor : IHttpHeadersRedactor +{ + private const char SeparatorChar = ','; + + private readonly IRedactorProvider _redactorProvider; + + public HttpHeadersRedactor(IRedactorProvider redactorProvider) + { + _redactorProvider = redactorProvider; + } + + public string Redact(IEnumerable headerValues, DataClassification classification) => + headerValues switch + { + IReadOnlyList headerValueList => RedactList(headerValueList, classification), + { } => RedactIEnumerable(headerValues, classification), + _ => TelemetryConstants.Unknown + }; + + private string RedactIEnumerable(IEnumerable input, DataClassification classification) + { + var redactor = _redactorProvider.GetRedactor(classification); + + using var enumerator = input.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return string.Empty; + } + + var firstItem = enumerator.Current.AsSpan(); + + ReadOnlySpan currentItem; + var redactedSize = 0; + var counter = 1; + while (enumerator.MoveNext()) + { + counter++; + redactedSize++; // for a separator char + + currentItem = enumerator.Current.AsSpan(); + if (!currentItem.IsEmpty) + { + redactedSize += redactor.GetRedactedLength(currentItem); + } + } + + if (counter == 1) + { + return redactor.Redact(firstItem); + } + + if (!firstItem.IsEmpty) + { + redactedSize += redactor.GetRedactedLength(firstItem); + } + + using var rental = new RentedSpan(redactedSize); + var destinationMany = rental.Rented ? rental.Span : stackalloc char[redactedSize]; + + enumerator.Reset(); + + // don't insert SeparatorChar before the first item. + _ = enumerator.MoveNext(); + currentItem = enumerator.Current.AsSpan(); + var index = 0; + if (!currentItem.IsEmpty) + { + index += redactor.Redact(currentItem, destinationMany.Slice(index)); + } + + while (enumerator.MoveNext()) + { + // insert SeparatorChar before every item, starting from the second. + destinationMany[index++] = SeparatorChar; + currentItem = enumerator.Current.AsSpan(); + if (!currentItem.IsEmpty) + { + index += redactor.Redact(currentItem, destinationMany.Slice(index)); + } + } + + return destinationMany.ToString(); + } + + private string RedactList(IReadOnlyList input, DataClassification classification) + { + if (input.Count == 0) + { + return string.Empty; + } + + var redactor = _redactorProvider.GetRedactor(classification); + var firstItem = input[0].AsSpan(); + if (input.Count == 1) + { + return redactor.Redact(firstItem); + } + + var redactedSize = 0; + if (!firstItem.IsEmpty) + { + redactedSize += redactor.GetRedactedLength(firstItem); + } + + ReadOnlySpan currentItem; + for (int i = 1; i < input.Count; i++) + { + redactedSize++; // for a separator char + currentItem = input[i].AsSpan(); + if (!currentItem.IsEmpty) + { + redactedSize += redactor.GetRedactedLength(currentItem); + } + } + + using var rental = new RentedSpan(redactedSize); + + // Stryker disable once all + Span destinationMany = rental.Rented ? rental.Span : stackalloc char[redactedSize]; + + var index = 0; + for (int i = 0; i < input.Count; i++) + { + currentItem = input[i].AsSpan(); + if (!currentItem.IsEmpty) + { + index += redactor.Redact(currentItem, destinationMany.Slice(index)); + } + + if (i < input.Count - 1) + { + destinationMany[index++] = SeparatorChar; + } + } + + return destinationMany.ToString(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/HttpRouteFormatter.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/HttpRouteFormatter.cs new file mode 100644 index 0000000000..b5c67fdedc --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/HttpRouteFormatter.cs @@ -0,0 +1,258 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Telemetry.Internal; + +internal sealed class HttpRouteFormatter : IHttpRouteFormatter +{ + private const char ForwardSlashSymbol = '/'; + +#if NETCOREAPP3_1_OR_GREATER + private const char ForwardSlash = ForwardSlashSymbol; +#else +#pragma warning disable IDE1006 // Naming Styles + private static readonly char[] ForwardSlash = new[] { ForwardSlashSymbol }; +#pragma warning restore IDE1006 // Naming Styles +#endif + + private readonly IHttpRouteParser _httpRouteParser; + private readonly IRedactorProvider _redactorProvider; + + public HttpRouteFormatter(IHttpRouteParser httpRouteParser, IRedactorProvider redactorProvider) + { + _httpRouteParser = httpRouteParser; + _redactorProvider = redactorProvider; + } + + public string Format(string httpRoute, string httpPath, HttpRouteParameterRedactionMode redactionMode, IReadOnlyDictionary parametersToRedact) + { + var routeSegments = _httpRouteParser.ParseRoute(httpRoute); + return Format(routeSegments, httpPath, redactionMode, parametersToRedact); + } + + public string Format( + in ParsedRouteSegments routeSegments, + string httpPath, + HttpRouteParameterRedactionMode redactionMode, + IReadOnlyDictionary parametersToRedact) + { + if (routeSegments.ParameterCount == 0 || + !IsRedactionRequired(routeSegments, redactionMode, parametersToRedact)) + { + return httpPath.Trim(ForwardSlash); + } + + var httpPathAsSpan = httpPath.AsSpan().TrimStart(ForwardSlash); + var pathStringBuilder = PoolFactory.SharedStringBuilderPool.Get(); + + try + { + int offset = 0; + + for (var i = 0; i < routeSegments.Segments.Length; i++) + { + var segment = routeSegments.Segments[i]; + + if (segment.IsParam) + { + var parameterContent = segment.Content; + var parameterTemplateLength = parameterContent.Length + 2; + + var startIndex = segment.Start + offset; + + // If we exceed a length of the http path it means that the appropriate http route + // has optional parameters or parameters with default values, and these parameters + // are omitted in the http path. In this case we stop processing and return resulting + // http path. + if (startIndex >= httpPathAsSpan.Length) + { + break; + } + + int length; + + if (i < routeSegments.Segments.Length - 1) + { + length = httpPathAsSpan.Slice(startIndex).IndexOf(routeSegments.Segments[i + 1].Content[0]); + } + else + { + length = httpPathAsSpan.Slice(startIndex).IndexOf(ForwardSlash); + } + + if (length == -1) + { + length = httpPathAsSpan.Slice(startIndex).Length; + } + + offset += length - parameterTemplateLength; + + FormatParameter(httpPathAsSpan, segment, startIndex, length, redactionMode, parametersToRedact, pathStringBuilder); + } + else + { + _ = pathStringBuilder.Append(segment.Content); + } + } + + RemoveTrailingForwardSlash(pathStringBuilder); + + return pathStringBuilder.ToString(); + } + finally + { + PoolFactory.SharedStringBuilderPool.Return(pathStringBuilder); + } + } + + private static bool IsRedactionRequired( + in ParsedRouteSegments routeSegments, HttpRouteParameterRedactionMode redactionMode, IReadOnlyDictionary parametersToRedact) + { + if (redactionMode == HttpRouteParameterRedactionMode.None) + { + return false; + } + + foreach (var segment in routeSegments.Segments) + { + if (!segment.IsParam) + { + continue; + } + + if (redactionMode == HttpRouteParameterRedactionMode.Strict) + { + // If no data class exists for a parameter, and the parameter is not a well known parameter, then we redact it. + // If data class exists and it's anything other than DataClassification.None, then also we redact it. + if ((!parametersToRedact.TryGetValue(segment.ParamName, out DataClassification classification) && + !Segment.IsKnownUnredactableParameter(segment.ParamName)) || + classification != DataClassification.None) + { + return true; + } + } + else if (redactionMode == HttpRouteParameterRedactionMode.Loose) + { + // If data class exists for a parameter and it's anything other than DataClassification.None, then we redact it. + if (parametersToRedact.TryGetValue(segment.ParamName, out DataClassification classification) && classification != DataClassification.None) + { + return true; + } + } + else + { + throw new InvalidOperationException(TelemetryCommonExtensions.UnsupportedEnumValueExceptionMessage); + } + } + + return false; + } + + private static void RemoveTrailingForwardSlash(StringBuilder formattedHttpPath) + { + if (formattedHttpPath.Length > 1) + { + int index = formattedHttpPath.Length - 1; + + if (formattedHttpPath[index] == ForwardSlashSymbol) + { + _ = formattedHttpPath.Remove(index, 1); + } + } + } + + private void FormatParameter( + ReadOnlySpan httpPath, + in Segment segment, + int startIndex, + int length, + HttpRouteParameterRedactionMode redactionMode, + IReadOnlyDictionary parametersToRedact, + StringBuilder outputBuffer) + { + if (redactionMode == HttpRouteParameterRedactionMode.Strict) + { + FormatParameterInStrictMode(httpPath, segment, startIndex, length, parametersToRedact, outputBuffer); + return; + } + + if (redactionMode == HttpRouteParameterRedactionMode.Loose) + { + FormatParameterInLooseMode(httpPath, segment, startIndex, length, parametersToRedact, outputBuffer); + } + } + + private void FormatParameterInStrictMode( + ReadOnlySpan httpPath, + Segment httpRouteSegment, + int startIndex, + int length, + IReadOnlyDictionary parametersToRedact, + StringBuilder outputBuffer) + { + if (parametersToRedact.TryGetValue(httpRouteSegment.ParamName, out var classification)) + { + if (classification != DataClassification.None) + { + var redactor = _redactorProvider.GetRedactor(classification); + _ = outputBuffer.AppendRedacted(redactor, httpPath.Slice(startIndex, length)); + } + else + { +#if NETCOREAPP3_1_OR_GREATER + _ = outputBuffer.Append(httpPath.Slice(startIndex, length)); +#else + _ = outputBuffer.Append(httpPath.Slice(startIndex, length).ToString()); +#endif + } + } + else if (Segment.IsKnownUnredactableParameter(httpRouteSegment.ParamName)) + { +#if NETCOREAPP3_1_OR_GREATER + _ = outputBuffer.Append(httpPath.Slice(startIndex, length)); +#else + _ = outputBuffer.Append(httpPath.Slice(startIndex, length).ToString()); +#endif + } + else + { +#if NETCOREAPP3_1_OR_GREATER + _ = outputBuffer.Append(TelemetryConstants.Redacted.AsSpan()); +#else + _ = outputBuffer.Append(TelemetryConstants.Redacted); +#endif + } + } + + private void FormatParameterInLooseMode( + ReadOnlySpan httpPath, + Segment httpRouteSegment, + int startIndex, + int length, + IReadOnlyDictionary parametersToRedact, + StringBuilder outputBuffer) + { + if (parametersToRedact.TryGetValue(httpRouteSegment.ParamName, out DataClassification classification) + && classification != DataClassification.None) + { + var redactor = _redactorProvider.GetRedactor(classification); + _ = outputBuffer.AppendRedacted(redactor, httpPath.Slice(startIndex, length)); + } + else + { +#if NETCOREAPP3_1_OR_GREATER + _ = outputBuffer.Append(httpPath.Slice(startIndex, length)); +#else + _ = outputBuffer.Append(httpPath.Slice(startIndex, length).ToString()); +#endif + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/HttpRouteParameter.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/HttpRouteParameter.cs new file mode 100644 index 0000000000..0cd06785d8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/HttpRouteParameter.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Telemetry.Internal; + +#pragma warning disable CA1815 // Override equals and operator equals on value types +/// +/// Struct to hold metadata about a route parameter. +/// +internal readonly struct HttpRouteParameter +#pragma warning restore CA1815 // Override equals and operator equals on value types +{ + /// + /// Initializes a new instance of the struct. + /// + /// parameter name. + /// parameter value. + /// conveys if the parameter value is redacted. + public HttpRouteParameter(string name, string value, bool isRedacted) + { + Name = name; + Value = value; + IsRedacted = isRedacted; + } + + /// + /// Gets parameter name. + /// + public string Name { get; } + + /// + /// Gets parameter value. + /// + public string Value { get; } + + /// + /// Gets a value indicating whether the parameter value is redacted. + /// + public bool IsRedacted { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/HttpRouteParser.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/HttpRouteParser.cs new file mode 100644 index 0000000000..f8bda5facf --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/HttpRouteParser.cs @@ -0,0 +1,284 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Http.Telemetry; + +namespace Microsoft.Extensions.Telemetry.Internal; + +internal sealed class HttpRouteParser : IHttpRouteParser +{ +#if NETCOREAPP3_1_OR_GREATER + private const char ForwardSlash = '/'; +#else +#pragma warning disable IDE1006 // Naming Styles + private static readonly char[] ForwardSlash = new[] { '/' }; +#pragma warning restore IDE1006 // Naming Styles +#endif + + private readonly IRedactorProvider _redactorProvider; + private readonly ConcurrentDictionary _routeTemplateSegmentsCache = new(); + + public HttpRouteParser(IRedactorProvider redactorProvider) + { + _redactorProvider = redactorProvider; + } + + public bool TryExtractParameters( + string httpPath, + in ParsedRouteSegments routeSegments, + HttpRouteParameterRedactionMode redactionMode, + IReadOnlyDictionary parametersToRedact, + ref HttpRouteParameter[] httpRouteParameters) + { + int paramCount = routeSegments.ParameterCount; + + if (httpRouteParameters == null || httpRouteParameters.Length < paramCount) + { + return false; + } + + var httpPathAsSpan = httpPath.AsSpan(); + httpPathAsSpan = httpPathAsSpan.TrimStart(ForwardSlash); + + if (paramCount > 0) + { + int offset = 0; + int index = 0; + + foreach (Segment segment in routeSegments.Segments) + { + if (segment.IsParam) + { + var startIndex = segment.Start + offset; + + string parameterValue; + bool isRedacted = false; + + if (startIndex < httpPathAsSpan.Length) + { + var parameterContent = segment.Content; + var parameterTemplateLength = parameterContent.Length + 2; + var length = httpPathAsSpan.Slice(startIndex).IndexOf(ForwardSlash); + + if (length == -1) + { + length = httpPathAsSpan.Slice(startIndex).Length; + } + + offset += length - parameterTemplateLength; + + parameterValue = GetRedactedParameterValue(httpPathAsSpan, segment, startIndex, length, redactionMode, parametersToRedact, ref isRedacted); + } + + // If we exceed a length of the http path it means that the appropriate http route + // has optional parameters or parameters with default values, and these parameters + // are omitted in the http path. In this case we return a default value of the + // omitted parameter. + else + { + parameterValue = segment.DefaultValue; + } + + httpRouteParameters[index++] = new HttpRouteParameter(segment.ParamName, parameterValue, isRedacted); + } + } + } + + return true; + } + + public ParsedRouteSegments ParseRoute(string httpRoute) + { + return _routeTemplateSegmentsCache.GetOrAdd(httpRoute, httpRoute => + { + httpRoute = httpRoute.TrimStart(ForwardSlash); + + var pos = 0; + var len = httpRoute.Length; + var start = 0; + char ch; + + var segments = new List(); + + while (pos < len) + { + ch = httpRoute[pos]; + + // Start of a parameter segment. + if (ch == '{') + { + // End of the current text segment. + if (pos > start) + { + segments.Add(new Segment( + start: start, + end: pos, + content: GetSegmentContent(httpRoute, start, pos), + isParam: false)); + } + + segments.Add(GetParameterSegment(httpRoute, ref pos)); + start = pos + 1; + } + + // Start of the query parameters sections. + else if (ch == '?') + { + // Remove the query parameters from the template. + httpRoute = httpRoute.Substring(0, pos); + break; + } + + pos++; + } + + // End the last text segment if any. + if (start < pos) + { + segments.Add(new Segment( + start: start, + end: pos, + content: GetSegmentContent(httpRoute, start, pos), + isParam: false)); + } + + return new ParsedRouteSegments(httpRoute, segments.ToArray()); + }); + } + + private static Segment GetParameterSegment(string httpRoute, ref int pos) + { + const int PositionNotFound = -1; + + int start = pos++; + int paramNameEnd = PositionNotFound; + int defaultValueStart = PositionNotFound; + + char ch; + + while ((ch = httpRoute[pos]) != '}') + { + // The segment has a default value '='. The character indicates + // that we've met the end of the segment's parameter name and + // the start of the default value. + if (ch == '=') + { + if (paramNameEnd == PositionNotFound) + { + paramNameEnd = pos; + } + + defaultValueStart = pos + 1; + } + + // The segment is optional '?' or has a constraint ':'. + // When we meet one of the above characters it indicates + // that we've met the end of the segment's parameter name. + else if (ch == '?' || ch == ':') + { + if (paramNameEnd == PositionNotFound) + { + paramNameEnd = pos; + } + } + + pos++; + } + + string content = GetSegmentContent(httpRoute, start + 1, pos); + string paramName = paramNameEnd == PositionNotFound + ? content + : GetSegmentContent(httpRoute, start + 1, paramNameEnd); + string defaultValue = defaultValueStart == PositionNotFound + ? string.Empty + : GetSegmentContent(httpRoute, defaultValueStart, pos); + + // Remove the opening and closing curly braces when getting content. + return new Segment( + start: start, + end: pos + 1, + content: content, + isParam: true, + paramName: paramName, + defaultValue: defaultValue); + } + + private static string GetSegmentContent(string httpRoute, int start, int end) + { + return httpRoute.Substring(start, end - start); + } + + private string GetRedactedParameterValue( + ReadOnlySpan httpPath, + in Segment segment, + int startIndex, + int length, + HttpRouteParameterRedactionMode redactionMode, + IReadOnlyDictionary parametersToRedact, + ref bool isRedacted) + { + return redactionMode switch + { + HttpRouteParameterRedactionMode.None => httpPath.Slice(startIndex, length).ToString(), + HttpRouteParameterRedactionMode.Strict => GetRedactedParameterInStrictMode(httpPath, segment, startIndex, length, parametersToRedact, ref isRedacted), + HttpRouteParameterRedactionMode.Loose => GetRedactedParameterInLooseMode(httpPath, segment, startIndex, length, parametersToRedact, ref isRedacted), + _ => throw new InvalidOperationException(TelemetryCommonExtensions.UnsupportedEnumValueExceptionMessage) + }; + } + + private string GetRedactedParameterInStrictMode( + ReadOnlySpan httpPathAsSpan, + Segment segment, + int startIndex, + int length, + IReadOnlyDictionary parametersToRedact, + ref bool isRedacted) + { + if (parametersToRedact.TryGetValue(segment.ParamName, out DataClassification classification)) + { + if (classification != DataClassification.None) + { + var redactor = _redactorProvider.GetRedactor(classification); + isRedacted = true; + + return redactor.Redact(httpPathAsSpan.Slice(startIndex, length)); + } + + return httpPathAsSpan.Slice(startIndex, length).ToString(); + } + + if (Segment.IsKnownUnredactableParameter(segment.ParamName)) + { + return httpPathAsSpan.Slice(startIndex, length).ToString(); + } + + isRedacted = true; + + return TelemetryConstants.Redacted; + } + + private string GetRedactedParameterInLooseMode( + ReadOnlySpan httpPathAsSpan, + Segment segment, + int startIndex, + int length, + IReadOnlyDictionary parametersToRedact, + ref bool isRedacted) + { + if (parametersToRedact.TryGetValue(segment.ParamName, out DataClassification classification) + && classification != DataClassification.None) + { + var redactor = _redactorProvider.GetRedactor(classification); + isRedacted = true; + + return redactor.Redact(httpPathAsSpan.Slice(startIndex, length)); + } + + return httpPathAsSpan.Slice(startIndex, length).ToString(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/IDownstreamDependencyMetadataManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/IDownstreamDependencyMetadataManager.cs new file mode 100644 index 0000000000..9553bb0577 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/IDownstreamDependencyMetadataManager.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Net.Http; +using Microsoft.Extensions.Http.Telemetry; + +namespace Microsoft.Extensions.Telemetry.Internal; + +/// +/// Interface to manage dependency metadata. +/// +internal interface IDownstreamDependencyMetadataManager +{ + /// + /// Get metadata for the given request. + /// + /// Request object. + /// object. + RequestMetadata? GetRequestMetadata(HttpRequestMessage requestMessage); + + /// + /// Get metadata for the given request. + /// + /// Request object. + /// object. + RequestMetadata? GetRequestMetadata(HttpWebRequest requestMessage); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/IHttpHeadersRedactor.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/IHttpHeadersRedactor.cs new file mode 100644 index 0000000000..3ced3da01b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/IHttpHeadersRedactor.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; + +namespace Microsoft.Extensions.Telemetry.Internal; + +/// +/// HTTP headers redactor. +/// +internal interface IHttpHeadersRedactor +{ + /// + /// Redacts HTTP header values and joins the results into a . + /// + /// HTTP header values. + /// Data classification which is used to get an appropriate redactor to redact headers. + /// Returns text and parameter segments of route. + string Redact(IEnumerable headerValues, DataClassification classification); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/IHttpRouteFormatter.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/IHttpRouteFormatter.cs new file mode 100644 index 0000000000..f109271773 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/IHttpRouteFormatter.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Http.Telemetry; + +namespace Microsoft.Extensions.Telemetry.Internal; + +/// +/// Http request route formatter. +/// +internal interface IHttpRouteFormatter +{ + /// + /// Format the http path using the route template with sensitive parameters redacted. + /// + /// Http request route template. + /// Http request's absolute path. + /// Strategy to decide how parameters are redacted. + /// Dictionary of parameters with their data classification that needs to be redacted. + /// Returns formatted path with sensitive parameter values redacted. + [Experimental] + string Format(string httpRoute, string httpPath, HttpRouteParameterRedactionMode redactionMode, IReadOnlyDictionary parametersToRedact); + + /// + /// Format the http path using the route template with sensitive parameters redacted. + /// + /// Http request's route segments. + /// Http request's absolute path. + /// Strategy to decide how parameters are redacted. + /// Dictionary of parameters with their data classification that needs to be redacted. + /// Returns formatted path with sensitive parameter values redacted. + [Experimental] + string Format(in ParsedRouteSegments routeSegments, string httpPath, HttpRouteParameterRedactionMode redactionMode, IReadOnlyDictionary parametersToRedact); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/IHttpRouteParser.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/IHttpRouteParser.cs new file mode 100644 index 0000000000..6e692641cb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/IHttpRouteParser.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Http.Telemetry; + +namespace Microsoft.Extensions.Telemetry.Internal; + +/// +/// Http request route parser. +/// +internal interface IHttpRouteParser +{ + /// + /// Parses http route and breaks it into text and parameter segments. + /// + /// Http request's route template. + /// Returns text and parameter segments of route. + ParsedRouteSegments ParseRoute(string httpRoute); + + /// + /// Extract parameters values from the http request path. + /// + /// Http request's absolute path. + /// Route segments containing text and parameter segments of the route. + /// Strategy to decide how parameters are redacted. + /// Dictionary of parameters with their data classification that needs to be redacted. + /// Output array where parameters will be stored. Caller must provide the array with enough capacity to hold all parameters in route segment. + /// Returns true if parameters were extracted successfully, return false otherwise. +#pragma warning disable CA1045 // Do not pass types by reference + bool TryExtractParameters( + string httpPath, + in ParsedRouteSegments routeSegments, + HttpRouteParameterRedactionMode redactionMode, + IReadOnlyDictionary parametersToRedact, + ref HttpRouteParameter[] httpRouteParameters); +#pragma warning restore CA1045 // Do not pass types by reference +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/MetricEnrichmentPropertyBag.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/MetricEnrichmentPropertyBag.cs new file mode 100644 index 0000000000..a7b0f32d73 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/MetricEnrichmentPropertyBag.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Internal; + +/// +/// Canonical implementation of a metric enrichment property bag. +/// +internal sealed class MetricEnrichmentPropertyBag : List>, IEnrichmentPropertyBag, IResettable +{ + /// + public void Add(string key, object value) + { + _ = Throw.IfNullOrEmpty(key); + _ = Throw.IfNull(value); + + Add(new KeyValuePair(key, value.ToString() ?? string.Empty)); + } + + /// + public void Add(string key, string value) + { + _ = Throw.IfNullOrEmpty(key); + _ = Throw.IfNull(value); + + Add(new KeyValuePair(key, value)); + } + + /// + public void Add(ReadOnlySpan> properties) + { + foreach (var p in properties) + { + Add(p); + } + } + + /// + public void Add(ReadOnlySpan> properties) + { + foreach (var p in properties) + { + Add(new KeyValuePair(p.Key, p.Value.ToString() ?? string.Empty)); + } + } + + /// + public bool TryReset() + { + Clear(); + return true; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/OutgoingRequestContext.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/OutgoingRequestContext.cs new file mode 100644 index 0000000000..2e4e603f63 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/OutgoingRequestContext.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Microsoft.Extensions.Http.Telemetry; + +namespace Microsoft.Extensions.Telemetry.Internal; + +internal sealed class OutgoingRequestContext : IOutgoingRequestContext +{ + private static readonly AsyncLocal _asyncLocal = new(); + + public RequestMetadata? RequestMetadata + { + get => _asyncLocal.Value; + set => _asyncLocal.Value = value; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/ParsedRouteSegments.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/ParsedRouteSegments.cs new file mode 100644 index 0000000000..1393b56965 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/ParsedRouteSegments.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Internal; + +/// +/// Struct to hold the route segments created after parsing the route. +/// +#pragma warning disable CA1815 // Override equals and operator equals on value types +internal readonly struct ParsedRouteSegments +#pragma warning restore CA1815 // Override equals and operator equals on value types +{ + /// + /// Initializes a new instance of the struct. + /// + /// Route's template. + /// Array of segments. + public ParsedRouteSegments(string routeTemplate, Segment[] segments) + { + _ = Throw.IfNull(segments); + + Segments = segments; + + var paramCount = 0; + foreach (var segment in segments) + { + if (segment.IsParam) + { + paramCount++; + } + } + + ParameterCount = paramCount; + RouteTemplate = routeTemplate; + } + + /// + /// Gets the route template. + /// + public string RouteTemplate { get; } + + /// + /// Gets all segments of the route. + /// +#pragma warning disable CA1819 // Properties should not return arrays + public Segment[] Segments { get; } +#pragma warning restore CA1819 // Properties should not return arrays + + /// + /// Gets the count of parameters in the route. + /// + public int ParameterCount { get; } +} + diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/Segment.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/Segment.cs new file mode 100644 index 0000000000..994ace28da --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/Segment.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Telemetry.Internal; + +/// +/// Struct to hold the metadata about a route's segment. +/// +#pragma warning disable CA1815 // Override equals and operator equals on value types +internal readonly struct Segment +#pragma warning restore CA1815 // Override equals and operator equals on value types +{ + private const string ControllerParameter = "controller"; + private const string ActionParameter = "action"; + + /// + /// Initializes a new instance of the struct. + /// + /// Start index of the segment. + /// End index of the segment. + /// Actual content of the segment. + /// If the segment is a param. + /// Name of the parameter. + /// Default value of the parameter. + public Segment( + int start, int end, string content, bool isParam, + string paramName = "", string defaultValue = "") + { + Start = start; + End = end; + Content = content; + IsParam = isParam; + ParamName = paramName; + DefaultValue = defaultValue; + } + + /// + /// Gets start index of the segment. + /// + public int Start { get; } + + /// + /// Gets end index of the segment. + /// + public int End { get; } + + /// + /// Gets content of the segment. + /// + public string Content { get; } = string.Empty; + + /// + /// Gets a value indicating whether the segment is a parameter. + /// + public bool IsParam { get; } + + /// + /// Gets a name of the parameter. + /// + public string ParamName { get; } = string.Empty; + + /// + /// Gets a default value of the parameter. + /// + public string DefaultValue { get; } = string.Empty; + + internal static bool IsKnownUnredactableParameter(string parameter) => + parameter.Equals(ControllerParameter, StringComparison.OrdinalIgnoreCase) || + parameter.Equals(ActionParameter, StringComparison.OrdinalIgnoreCase); +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnostics.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnostics.cs new file mode 100644 index 0000000000..8ceaa748f6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnostics.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Telemetry.Internal; + +/// +/// Self diagnostics class captures the EventSource events sent by OpenTelemetry +/// modules and writes them to local file for internal troubleshooting. +/// +/// +/// This is copied from the OpenTelemetry-dotnet repo +/// https://github.com/open-telemetry/opentelemetry-dotnet/blob/952c3b17fc2eaa0622f5f3efd336d4cf103c2813/src/OpenTelemetry/Internal/SelfDiagnostics.cs +/// as the class is internal and not visible to this project. This will be removed from R9 library +/// in one of the two conditions below. +/// - OpenTelemetry-dotnet will make it internalVisible to R9 library. +/// - This class will be added to OpenTelemetry-dotnet project as public. +/// +internal sealed class SelfDiagnostics : IDisposable +{ + internal static readonly TimeProvider TimeProvider = TimeProvider.System; + + /// + /// Long-living object that holds relevant resources. + /// + private static readonly SelfDiagnostics _instance = new(); + private readonly SelfDiagnosticsConfigRefresher _configRefresher; + + static SelfDiagnostics() + { + AppDomain.CurrentDomain.ProcessExit += OnProcessExit; + } + + internal SelfDiagnostics() + { + _configRefresher = new(TimeProvider); + } + + /// + /// No member of SelfDiagnostics class is explicitly called when an EventSource class, say + /// OpenTelemetryApiEventSource, is invoked to send an event. + /// To trigger CLR to initialize static fields and static constructors of SelfDiagnostics, + /// call EnsureInitialized method before any EventSource event is sent. + /// + public static void EnsureInitialized() + { + // see the XML comment above. + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + } + + internal static void OnProcessExit(object? sender, EventArgs e) => _instance.Dispose(); + + private void Dispose(bool disposing) + { + if (disposing) + { + _configRefresher.Dispose(); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsConfigParser.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsConfigParser.cs new file mode 100644 index 0000000000..072a02c684 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsConfigParser.cs @@ -0,0 +1,157 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.Tracing; +using System.Globalization; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; + +namespace Microsoft.Extensions.Telemetry.Internal; + +/// +/// This is copied from the OpenTelemetry-dotnet repo +/// https://github.com/open-telemetry/opentelemetry-dotnet/blob/952c3b17fc2eaa0622f5f3efd336d4cf103c2813/src/OpenTelemetry/Internal/SelfDiagnosticsConfigParser.cs +/// as the class is internal and not visible to this project. This will be removed from R9 library +/// in one of the two conditions below. +/// - OpenTelemetry-dotnet will make it internalVisible to R9 library. +/// - This class will be added to OpenTelemetry-dotnet project as public. +/// +#pragma warning disable R9A013 // Class inherited in tests. +internal class SelfDiagnosticsConfigParser +#pragma warning restore R9A013 // Class inherited in tests. +{ + public const string ConfigFileName = "OTEL_DIAGNOSTICS.json"; + + internal const int FileSizeLowerLimit = 1024; // Lower limit for log file size in KB: 1MB + internal const int FileSizeUpperLimit = 128 * 1024; // Upper limit for log file size in KB: 128MB + + // This class is called in SelfDiagnosticsConfigRefresher.UpdateMemoryMappedFileFromConfiguration + // in both main thread and the worker thread. + // In theory the variable won't be access at the same time because worker thread first Task.Delay for a few seconds. + internal byte[]? ConfigBuffer; + + /// + /// ConfigBufferSize is the maximum bytes of config file that will be read. + /// + private const int ConfigBufferSize = 4 * 1024; + + private static readonly Regex _logDirectoryRegex = SelfDiagnosticsConfigParserRegex.MakeLogDirectoryRegex(); + private static readonly Regex _fileSizeRegex = SelfDiagnosticsConfigParserRegex.MakeFileSizeRegex(); + private static readonly Regex _logLevelRegex = SelfDiagnosticsConfigParserRegex.MakeLogLevelRegex(); + + public bool TryGetConfiguration(out string logDirectory, out int fileSizeInKb, out EventLevel logLevel) + { + logDirectory = string.Empty; + fileSizeInKb = 0; + logLevel = EventLevel.LogAlways; + + if (!TryReadConfigFile(ConfigFileName, out var configJson)) + { + return false; + } + + if (!TryParseLogDirectory(configJson, out logDirectory)) + { + return false; + } + + if (!TryParseFileSize(configJson, out fileSizeInKb)) + { + return false; + } + + fileSizeInKb = SetFileSizeWithinLimit(fileSizeInKb); + + if (!TryParseLogLevel(configJson, out var logLevelString)) + { + return false; + } + + logLevel = (EventLevel)Enum.Parse(typeof(EventLevel), logLevelString); + return true; + + } + + internal static bool TryParseLogDirectory(string configJson, out string logDirectory) + { + var logDirectoryResult = _logDirectoryRegex.Match(configJson); + logDirectory = logDirectoryResult.Groups["LogDirectory"].Value; + return logDirectoryResult.Success && !string.IsNullOrWhiteSpace(logDirectory); + } + + internal static bool TryParseFileSize(string configJson, out int fileSizeInKb) + { + fileSizeInKb = 0; + var fileSizeResult = _fileSizeRegex.Match(configJson); + return fileSizeResult.Success && int.TryParse(fileSizeResult.Groups["FileSize"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out fileSizeInKb); + } + + internal static bool TryParseLogLevel(string configJson, out string logLevel) + { + var logLevelResult = _logLevelRegex.Match(configJson); + logLevel = logLevelResult.Groups["LogLevel"].Value; + return logLevelResult.Success && !string.IsNullOrWhiteSpace(logLevel); + } + + internal virtual int SetFileSizeWithinLimit(int fileSizeInKb) + { + if (fileSizeInKb < FileSizeLowerLimit) + { + fileSizeInKb = FileSizeLowerLimit; + } + + if (fileSizeInKb > FileSizeUpperLimit) + { + fileSizeInKb = FileSizeUpperLimit; + } + + return fileSizeInKb; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", + "R9A017:Use asynchronous operations instead of legacy thread blocking code", + Justification = "Imported from OpenTelemetry-dotnet repo.")] + internal virtual bool TryReadConfigFile(string configFilePath, out string configJson) + { + configJson = string.Empty; + + try + { + // First check using current working directory + if (!File.Exists(configFilePath)) + { + configFilePath = Path.Combine(AppContext.BaseDirectory, configFilePath); + + // Second check using application base directory + if (!File.Exists(configFilePath)) + { + return false; + } + } + + using var file = File.Open(configFilePath, FileMode.Open, FileAccess.Read, + FileShare.ReadWrite | FileShare.Delete); + var buffer = ConfigBuffer; + if (buffer == null) + { + buffer = new byte[ConfigBufferSize]; // Fail silently if OOM + ConfigBuffer = buffer; + } + + _ = file.Read(buffer, 0, buffer.Length); + + configJson = Encoding.UTF8.GetString(buffer); + return true; + } +#pragma warning disable CA1031 // Do not catch general exception types - this tools is nice-to-have and good if it just works, it should not never throw if anything happens. + catch (Exception) + { + // do nothing on failure to open/read/parse config file + } +#pragma warning restore CA1031 // Do not catch general exception types + + return false; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsConfigParserRegex.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsConfigParserRegex.cs new file mode 100644 index 0000000000..7cc9130e28 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsConfigParserRegex.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.RegularExpressions; + +namespace Microsoft.Extensions.Telemetry.Internal; + +#if NET7_0_OR_GREATER +internal static partial class SelfDiagnosticsConfigParserRegex +#else +internal static class SelfDiagnosticsConfigParserRegex +#endif +{ + private const string LogDirectoryRegexString = @"""LogDirectory""\s*:\s*""(?.*?)"""; + private const string FileSizeRegexString = @"""FileSize""\s*:\s*(?\d+)"; + private const string LogLevelRegexString = @"""LogLevel""\s*:\s*""(?.*?)"""; + +#if NET7_0_OR_GREATER + + [GeneratedRegex(LogDirectoryRegexString, RegexOptions.IgnoreCase)] + public static partial Regex MakeLogDirectoryRegex(); + + [GeneratedRegex(FileSizeRegexString, RegexOptions.IgnoreCase)] + public static partial Regex MakeFileSizeRegex(); + + [GeneratedRegex(LogLevelRegexString, RegexOptions.IgnoreCase)] + public static partial Regex MakeLogLevelRegex(); + +#else + + public static Regex MakeLogDirectoryRegex() => new(LogDirectoryRegexString, RegexOptions.IgnoreCase | RegexOptions.Compiled); + public static Regex MakeFileSizeRegex() => new(FileSizeRegexString, RegexOptions.IgnoreCase | RegexOptions.Compiled); + public static Regex MakeLogLevelRegex() => new(LogLevelRegexString, RegexOptions.IgnoreCase | RegexOptions.Compiled); + +#endif +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsConfigRefresher.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsConfigRefresher.cs new file mode 100644 index 0000000000..63f2487551 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsConfigRefresher.cs @@ -0,0 +1,331 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; +using System.IO; +using System.IO.MemoryMappedFiles; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Telemetry.Internal; + +/// +/// SelfDiagnosticsConfigRefresher class checks a location for a configuration file +/// and open a MemoryMappedFile of a configured size at the configured file path. +/// The class provides a stream object with proper write position if the configuration +/// file is present and valid. Otherwise, the stream object would be unavailable, +/// nothing will be logged to any file. +/// +/// +/// This is copied from the OpenTelemetry-dotnet repo +/// https://github.com/open-telemetry/opentelemetry-dotnet/blob/952c3b17fc2eaa0622f5f3efd336d4cf103c2813/src/OpenTelemetry/Internal/SelfDiagnosticsConfigRefresher.cs +/// repo as the class is internal and not visible to this project. This will be removed from R9 library +/// in one of the two conditions below. +/// - OpenTelemetry-dotnet will make it internalVisible to R9 library. +/// - This class will be added to OpenTelemetry-dotnet project as public. +/// +#pragma warning disable R9A013 // Class inherited in tests. +internal class SelfDiagnosticsConfigRefresher : IDisposable +#pragma warning restore R9A013 // Class inherited in tests. +{ + public static readonly byte[] MessageOnNewFile = Encoding.UTF8.GetBytes("Successfully opened file.\n"); + + /// + /// memoryMappedFileCache is a handle kept in thread-local storage as a cache to indicate whether the cached + /// viewStream is created from the current m_memoryMappedFile. + /// + internal readonly ThreadLocal ViewStream = new(true); + + internal readonly ThreadLocal MemoryMappedFileCache = new(true); + + private const int ConfigurationUpdatePeriodMilliSeconds = 10000; + + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly Task _worker; + private readonly SelfDiagnosticsConfigParser _configParser; + + private readonly TimeProvider _timeProvider; + private readonly string _logFileName; + + private bool _disposedValue; + + // Once the configuration file is valid, an eventListener object will be created. + private SelfDiagnosticsEventListener? _eventListener; + private volatile FileStream? _underlyingFileStreamForMemoryMappedFile; + private volatile MemoryMappedFile? _memoryMappedFile; + private string? _logDirectory; // Log directory for log files + private int _logFileSize; // Log file size in bytes + private long _logFilePosition; // The logger will write into the byte at this position + private EventLevel _logEventLevel = (EventLevel)(-1); + + public SelfDiagnosticsConfigRefresher( + TimeProvider timeProvider, + SelfDiagnosticsConfigParser? parser = null, + string logFileName = "", + CancellationToken? workerTaskToken = null) + { + _timeProvider = timeProvider; + + if (string.IsNullOrEmpty(logFileName)) + { + _logFileName = GenerateLogFileName(); + } + else + { + _logFileName = logFileName; + } + + _configParser = parser ?? new SelfDiagnosticsConfigParser(); + UpdateMemoryMappedFileFromConfiguration(); + _cancellationTokenSource = new CancellationTokenSource(); + _worker = Task.Run(() => WorkerAsync(_cancellationTokenSource.Token), workerTaskToken ?? _cancellationTokenSource.Token); + } + + /// + public void Dispose() + { + _underlyingFileStreamForMemoryMappedFile?.Dispose(); + _memoryMappedFile?.Dispose(); + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Try to get the log stream which is seeked to the position where the next line of log should be written. + /// + /// The number of bytes that need to be written. + /// When this method returns, contains the Stream object where `byteCount` of bytes can be written. + /// The number of bytes that is remaining until the end of the stream. + /// Whether the logger should log in the stream. + public virtual bool TryGetLogStream(int byteCount, out Stream stream, out int availableByteCount) + { + if (_memoryMappedFile == null) + { + stream = Stream.Null; + availableByteCount = 0; + return false; + } + + try + { + var cachedViewStream = ViewStream.Value; + + // Each thread has its own MemoryMappedViewStream created from the only one MemoryMappedFile. + // Once worker thread updates the MemoryMappedFile, all the cached ViewStream objects become + // obsolete. + // Each thread creates a new MemoryMappedViewStream the next time it tries to retrieve it. + // Whether the MemoryMappedViewStream is obsolete is determined by comparing the current + // MemoryMappedFile object with the MemoryMappedFile object cached at the creation time of the + // MemoryMappedViewStream. + if (cachedViewStream == null || MemoryMappedFileCache.Value != _memoryMappedFile) + { + // Race condition: The code might reach here right after the worker thread sets memoryMappedFile + // to null in CloseLogFile(). + // In this case, let the NullReferenceException be caught and fail silently. + // By design, all events captured will be dropped during a configuration file refresh if + // the file changed, regardless whether the file is deleted or updated. + cachedViewStream = _memoryMappedFile.CreateViewStream(); + ViewStream.Value = cachedViewStream; + MemoryMappedFileCache.Value = _memoryMappedFile; + } + + long beginPosition; + long endPosition; + do + { + beginPosition = _logFilePosition; + endPosition = beginPosition + byteCount; + if (endPosition >= _logFileSize) + { + endPosition %= _logFileSize; + } + } + while (beginPosition != Interlocked.CompareExchange(ref _logFilePosition, endPosition, beginPosition)); + availableByteCount = (int)(_logFileSize - beginPosition); + _ = cachedViewStream.Seek(beginPosition, SeekOrigin.Begin); + stream = cachedViewStream; + return true; + } +#pragma warning disable CA1031 // Do not catch general exception types - this tools is nice-to-have and good if it just works, it should not never throw if anything happens. + catch (Exception) + { + stream = Stream.Null; + availableByteCount = 0; + return false; + } +#pragma warning restore CA1031 // Do not catch general exception types + } + + internal async Task WorkerAsync(CancellationToken cancellationToken) + { + do + { + await _timeProvider.Delay(TimeSpan.FromMilliseconds(ConfigurationUpdatePeriodMilliSeconds), cancellationToken).ConfigureAwait(false); + UpdateMemoryMappedFileFromConfiguration(); + } + while (!cancellationToken.IsCancellationRequested); + } + + [ExcludeFromCodeCoverage] + private static string GenerateLogFileName() + { +#if NET5_0_OR_GREATER + return Path.GetFileName(Process.GetCurrentProcess().MainModule?.FileName) + + ".R9." + + Environment.ProcessId + + ".log"; +#else + return Path.GetFileName(Process.GetCurrentProcess().MainModule?.FileName) + + ".R9." + + Process.GetCurrentProcess().Id + + ".log"; +#endif + } + + private void UpdateMemoryMappedFileFromConfiguration() + { + if (_configParser.TryGetConfiguration(out string? newLogDirectory, out int fileSizeInKb, out EventLevel newEventLevel)) + { + int newFileSize = fileSizeInKb * 1024; + if (!newLogDirectory.Equals(_logDirectory, StringComparison.Ordinal) || _logFileSize != newFileSize) + { + CloseLogFile(); + OpenLogFile(newLogDirectory, newFileSize); + } + + if (!newEventLevel.Equals(_logEventLevel)) + { + _eventListener?.Dispose(); + _eventListener = new SelfDiagnosticsEventListener(newEventLevel, this, _timeProvider); + _logEventLevel = newEventLevel; + } + } + else + { + CloseLogFile(); + } + } + + private void CloseLogFile() + { + MemoryMappedFile? mmf = Interlocked.CompareExchange(ref _memoryMappedFile, null, _memoryMappedFile); + if (mmf != null) + { + // Each thread has its own MemoryMappedViewStream created from the only one MemoryMappedFile. + // Once worker thread closes the MemoryMappedFile, all the ViewStream objects should be disposed + // properly. + foreach (var stream in ViewStream.Values) + { + stream?.Dispose(); + } + + mmf.Dispose(); + } + + FileStream? fs = Interlocked.CompareExchange( + ref _underlyingFileStreamForMemoryMappedFile, + null, + _underlyingFileStreamForMemoryMappedFile); + fs?.Dispose(); + } + + [SuppressMessage("Performance", + "R9A017:Use asynchronous operations instead of legacy thread blocking code", + Justification = "Borrowed from OpenTelemetry-dotnet as is.")] + private void OpenLogFile(string newLogDirectory, int newFileSize) + { + try + { + _ = Directory.CreateDirectory(newLogDirectory); + + var filePath = Path.Combine(newLogDirectory, _logFileName); + + // Because the API [MemoryMappedFile.CreateFromFile][1](the string version) behaves differently on + // .NET Framework and .NET Core, here I am using the [FileStream version][2] of it. + // Taking the last four parameter values from [.NET Framework] + // (https://referencesource.microsoft.com/#system.core/System/IO/MemoryMappedFiles/MemoryMappedFile.cs,148) + // and [.NET Core] + // (https://github.com/dotnet/runtime/blob/master/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.cs#L152) + // The parameter for FileAccess is different in type but the same in rules, both are Read and Write. + // The parameter for FileShare is different in values and in behavior. + // .NET Framework doesn't allow sharing but .NET Core allows reading by other programs. + // The last two parameters are the same values for both frameworks. +#pragma warning disable S103 // can't split the line because it is a link. + + // [1]: https://docs.microsoft.com/dotnet/api/system.io.memorymappedfiles.memorymappedfile.createfromfile?view=net-5.0#System_IO_MemoryMappedFiles_MemoryMappedFile_CreateFromFile_System_String_System_IO_FileMode_System_String_System_Int64_ + // [2]: https://docs.microsoft.com/dotnet/api/system.io.memorymappedfiles.memorymappedfile.createfromfile?view=net-5.0#System_IO_MemoryMappedFiles_MemoryMappedFile_CreateFromFile_System_IO_FileStream_System_String_System_Int64_System_IO_MemoryMappedFiles_MemoryMappedFileAccess_System_IO_HandleInheritability_System_Boolean_ +#pragma warning restore S103 + _underlyingFileStreamForMemoryMappedFile = + new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 0x1000, FileOptions.None); + + // The parameter values for MemoryMappedFileSecurity, HandleInheritability and leaveOpen are the same + // values for .NET Framework and .NET Core: + // https://referencesource.microsoft.com/#system.core/System/IO/MemoryMappedFiles/MemoryMappedFile.cs,172 + // https://github.com/dotnet/runtime/blob/master/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.cs#L168-L179 + _memoryMappedFile = MemoryMappedFile.CreateFromFile( + _underlyingFileStreamForMemoryMappedFile, + null, + newFileSize, + MemoryMappedFileAccess.ReadWrite, + HandleInheritability.None, + false); + _logDirectory = newLogDirectory; + _logFileSize = newFileSize; + _logFilePosition = MessageOnNewFile.Length; + using var stream = _memoryMappedFile.CreateViewStream(); + stream.Write(MessageOnNewFile, 0, MessageOnNewFile.Length); + } +#pragma warning disable CA1031 // Do not catch general exception types - this tools is nice-to-have and good if it just works, it should not never throw if anything happens. + catch (Exception ex) + { + SelfDiagnosticsEventSource.Log.SelfDiagnosticsFileCreateException(newLogDirectory, ex); + } +#pragma warning restore CA1031 // Do not catch general exception types + } + + private void Dispose(bool disposing) + { + if (_disposedValue) + { + return; + } + + if (disposing) + { + _cancellationTokenSource.Cancel(false); + + try + { +#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits + _worker.Wait(); +#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits + } + catch (AggregateException) + { + // do nothing. + } + finally + { + _cancellationTokenSource.Dispose(); + } + + // Dispose EventListener before files, because EventListener writes to files. + _eventListener?.Dispose(); + + // Ensure worker thread properly finishes. + // Or it might have created another MemoryMappedFile in that thread + // after the CloseLogFile() below is called. + CloseLogFile(); + + // Dispose ThreadLocal variables after the file handles are disposed. + ViewStream.Dispose(); + MemoryMappedFileCache.Dispose(); + } + + _disposedValue = true; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsEventListener.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsEventListener.cs new file mode 100644 index 0000000000..bde4c9a40a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsEventListener.cs @@ -0,0 +1,356 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Internal; + +/// +/// SelfDiagnosticsEventListener class enables the events from OpenTelemetry event sources +/// and write the events to a local file in a circular way. +/// +/// +/// This is copied from the OpenTelemetry-dotnet repo +/// https://github.com/open-telemetry/opentelemetry-dotnet/blob/952c3b17fc2eaa0622f5f3efd336d4cf103c2813/src/OpenTelemetry/Internal/SelfDiagnosticsEventListener.cs +/// as the class is internal and not visible to this project. This will be removed from R9 library +/// in one of the two conditions below. +/// - OpenTelemetry-dotnet will make it internalVisible to R9 library. +/// - This class will be added to OpenTelemetry-dotnet project as public. +/// +internal sealed class SelfDiagnosticsEventListener : EventListener +{ + // Buffer size of the log line. A UTF-16 encoded character in C# can take up to 4 bytes if encoded in UTF-8. + private const int BufferSize = 4 * 5120; + private const string EventSourceNamePrefix = "R9-"; + private readonly object _lockObj = new(); + private readonly EventLevel _logLevel; +#pragma warning disable CA2213 // Disposable fields should be disposed - keeping as is from OpenTelemetry .NET. Disposing it would make tests failing. + private readonly SelfDiagnosticsConfigRefresher _configRefresher; +#pragma warning restore CA2213 // Disposable fields should be disposed + private readonly ThreadLocal _writeBuffer = new(() => null); + private readonly List? _eventSourcesBeforeConstructor = new(); + private readonly TimeProvider _timeProvider; + + private bool _disposedValue; + + public SelfDiagnosticsEventListener(EventLevel logLevel, SelfDiagnosticsConfigRefresher configRefresher, TimeProvider timeProvider) + { + _logLevel = logLevel; + _configRefresher = Throw.IfNull(configRefresher); + _timeProvider = timeProvider; + + List? eventSources; + lock (_lockObj) + { + eventSources = _eventSourcesBeforeConstructor; + _eventSourcesBeforeConstructor = null; + } + + if (eventSources is not null) + { + foreach (var eventSource in eventSources) + { + EnableEvents(eventSource, _logLevel, EventKeywords.All); + } + } + } + + /// + /// Encode a string into the designated position in a buffer of bytes, which will be written as log. + /// If isParameter is true, wrap "{}" around the string. + /// The buffer should not be filled to full, leaving at least one byte empty space to fill a '\n' later. + /// If the buffer cannot hold all characters, truncate the string and replace extra content with "...". + /// The buffer is not guaranteed to be filled until the last byte due to variable encoding length of UTF-8, + /// in order to prioritize speed over space. + /// + /// The string to be encoded. + /// Whether the string is a parameter. If true, "{}" will be wrapped around the string. + /// The byte array to contain the resulting sequence of bytes. + /// The position at which to start writing the resulting sequence of bytes. + /// The position of the buffer after the last byte of the resulting sequence. + public static int EncodeInBuffer(string? str, bool isParameter, byte[] buffer, int position) + { + if (string.IsNullOrEmpty(str)) + { + return position; + } + + int charCount = str!.Length; + int ellipses = isParameter ? "{...}\n".Length : "...\n".Length; + + // Ensure there is space for "{...}\n" or "...\n". + if (buffer.Length - position - ellipses < 0) + { + return position; + } + + int estimateOfCharacters = (buffer.Length - position - ellipses) / 2; + + // Ensure the UTF-16 encoded string can fit in buffer UTF-8 encoding. + // And leave space for "{...}\n" or "...\n". + if (charCount > estimateOfCharacters) + { + charCount = estimateOfCharacters; + } + + if (isParameter) + { + buffer[position++] = (byte)'{'; + } + + position += Encoding.UTF8.GetBytes(str, 0, charCount, buffer, position); + if (charCount != str.Length) + { + buffer[position++] = (byte)'.'; + buffer[position++] = (byte)'.'; + buffer[position++] = (byte)'.'; + } + + if (isParameter) + { + buffer[position++] = (byte)'}'; + } + + return position; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte GetHoursSign(int hours) => (byte)(hours >= 0 ? '+' : '-'); + + /// + /// Write the datetime formatted string into bytes byte-array starting at byteIndex position. + /// + /// [DateTimeKind.Utc] + /// format: yyyy - MM - dd T HH : mm : ss . fffffff Z (i.e. 2020-12-09T10:20:50.4659412Z). + /// + /// + /// [DateTimeKind.Local] + /// format: yyyy - MM - dd T HH : mm : ss . fffffff +|- HH : mm (i.e. 2020-12-09T10:20:50.4659412-08:00). + /// + /// + /// [DateTimeKind.Unspecified] + /// format: yyyy - MM - dd T HH : mm : ss . fffffff (i.e. 2020-12-09T10:20:50.4659412). + /// + /// + /// + /// The bytes array must be large enough to write 27-33 characters from the byteIndex starting position. + /// + /// DateTime. + /// Array of bytes to write. + /// Starting index into bytes array. + /// The number of bytes written. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S109:Magic numbers should not be used", Justification = "Conversion to a decimal number.")] +#pragma warning disable CA1822 // Mark members as static + public int DateTimeGetBytes(DateTime datetime, byte[] bytes, int byteIndex) +#pragma warning restore CA1822 // Mark members as static + { + int pos = byteIndex; + + int num = datetime.Year; + bytes[pos++] = (byte)('0' + ((num / 1000) % 10)); + bytes[pos++] = (byte)('0' + ((num / 100) % 10)); + bytes[pos++] = (byte)('0' + ((num / 10) % 10)); + bytes[pos++] = (byte)('0' + (num % 10)); + + bytes[pos++] = (byte)'-'; + + num = datetime.Month; + bytes[pos++] = (byte)('0' + ((num / 10) % 10)); + bytes[pos++] = (byte)('0' + (num % 10)); + + bytes[pos++] = (byte)'-'; + + num = datetime.Day; + bytes[pos++] = (byte)('0' + ((num / 10) % 10)); + bytes[pos++] = (byte)('0' + (num % 10)); + + bytes[pos++] = (byte)'T'; + + num = datetime.Hour; + bytes[pos++] = (byte)('0' + ((num / 10) % 10)); + bytes[pos++] = (byte)('0' + (num % 10)); + + bytes[pos++] = (byte)':'; + + num = datetime.Minute; + bytes[pos++] = (byte)('0' + ((num / 10) % 10)); + bytes[pos++] = (byte)('0' + (num % 10)); + + bytes[pos++] = (byte)':'; + + num = datetime.Second; + bytes[pos++] = (byte)('0' + ((num / 10) % 10)); + bytes[pos++] = (byte)('0' + (num % 10)); + + bytes[pos++] = (byte)'.'; + + num = (int)(Math.Round(datetime.TimeOfDay.TotalMilliseconds * 10000) % 10_000_000); + bytes[pos++] = (byte)('0' + ((num / 1_000_000) % 10)); + bytes[pos++] = (byte)('0' + ((num / 100000) % 10)); + bytes[pos++] = (byte)('0' + ((num / 10000) % 10)); + bytes[pos++] = (byte)('0' + ((num / 1000) % 10)); + bytes[pos++] = (byte)('0' + ((num / 100) % 10)); + bytes[pos++] = (byte)('0' + ((num / 10) % 10)); + bytes[pos++] = (byte)('0' + (num % 10)); + + switch (datetime.Kind) + { + case DateTimeKind.Utc: + bytes[pos++] = (byte)'Z'; + break; + + case DateTimeKind.Local: + TimeSpan ts = TimeZoneInfo.Local.GetUtcOffset(datetime); + + bytes[pos++] = GetHoursSign(ts.Hours); + + num = Math.Abs(ts.Hours); + bytes[pos++] = (byte)('0' + ((num / 10) % 10)); + bytes[pos++] = (byte)('0' + (num % 10)); + + bytes[pos++] = (byte)':'; + + num = ts.Minutes; + bytes[pos++] = (byte)('0' + ((num / 10) % 10)); + bytes[pos++] = (byte)('0' + (num % 10)); + break; + + case DateTimeKind.Unspecified: + default: + // Skip + break; + } + + return pos - byteIndex; + } + + /// + public override void Dispose() + { + Dispose(true); + base.Dispose(); + } + + public void WriteEvent(string? eventMessage, ReadOnlyCollection? payload) + { + try + { + var buffer = _writeBuffer.Value; + if (buffer == null) + { + buffer = new byte[BufferSize]; + _writeBuffer.Value = buffer; + } + + var pos = DateTimeGetBytes(_timeProvider.GetUtcNow().UtcDateTime, buffer, 0); + buffer[pos++] = (byte)':'; + pos = EncodeInBuffer(eventMessage, false, buffer, pos); + if (payload != null) + { + // Not using foreach because it can cause allocations + for (int i = 0; i < payload.Count; ++i) + { + object? obj = payload[i]; + if (obj != null) + { + pos = EncodeInBuffer(obj.ToString(), true, buffer, pos); + } + else + { + pos = EncodeInBuffer("null", true, buffer, pos); + } + } + } + + buffer[pos++] = (byte)'\n'; + int byteCount = pos - 0; +#pragma warning disable CA2000 // Dispose objects before losing scope + if (_configRefresher.TryGetLogStream(byteCount, out var stream, out int availableByteCount)) +#pragma warning restore CA2000 // Dispose objects before losing scope + { + if (availableByteCount >= byteCount) + { + stream.Write(buffer, 0, byteCount); + } + else + { + stream.Write(buffer, 0, availableByteCount); + _ = stream.Seek(0, SeekOrigin.Begin); + stream.Write(buffer, availableByteCount, byteCount - availableByteCount); + } + } + } +#pragma warning disable CA1031 // Do not catch general exception types - this tools is nice-to-have and good if it just works, it should not never throw if anything happens. + catch (Exception) + { + // Fail to allocate memory for buffer, or + // A concurrent condition: memory mapped file is disposed in other thread after TryGetLogStream() finishes. + // In this case, silently fail. + } +#pragma warning restore CA1031 // Do not catch general exception types + } + + [ExcludeFromCodeCoverage] + protected override void OnEventSourceCreated(EventSource eventSource) + { + if (eventSource.Name.StartsWith(EventSourceNamePrefix, StringComparison.Ordinal)) + { + // If there are EventSource classes already initialized as of now, this method would be called from + // the base class constructor before the first line of code in SelfDiagnosticsEventListener constructor. + // In this case logLevel is always its default value, "LogAlways". + // Thus we should save the event source and enable them later, when code runs in constructor. + if (_eventSourcesBeforeConstructor != null) + { + lock (_lockObj) + { + if (_eventSourcesBeforeConstructor != null) + { + _eventSourcesBeforeConstructor.Add(eventSource); + return; + } + } + } + + EnableEvents(eventSource, _logLevel, EventKeywords.All); + } + + base.OnEventSourceCreated(eventSource); + } + + /// + /// This method records the events from event sources to a local file, which is provided as a stream object by + /// SelfDiagnosticsConfigRefresher class. The file size is bound to a upper limit. Once the write position + /// reaches the end, it will be reset to the beginning of the file. + /// + /// Data of the EventSource event. + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + WriteEvent(eventData.Message, eventData.Payload); + } + +#pragma warning disable S2953 // Methods named "Dispose" should implement "IDisposable.Dispose" - parent class implements it. + private void Dispose(bool disposing) +#pragma warning restore S2953 // Methods named "Dispose" should implement "IDisposable.Dispose" + { + if (_disposedValue) + { + return; + } + + if (disposing) + { + _writeBuffer.Dispose(); + } + + _disposedValue = true; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsEventSource.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsEventSource.cs new file mode 100644 index 0000000000..643b4ed57e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/SelfDiagnosticsEventSource.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.Tracing; + +namespace Microsoft.Extensions.Telemetry.Internal; + +[EventSource(Name = "R9-SelfDiagnostics")] +internal sealed class SelfDiagnosticsEventSource : EventSource +{ + public const int FileCreateExceptionEventId = 26; + + public static readonly SelfDiagnosticsEventSource Log = new(); + + [Event(FileCreateExceptionEventId, Message = "Failed to create file. LogDirectory ='{0}', Id = '{1}'.", Level = EventLevel.Warning)] + public void SelfDiagnosticsFileCreateException(string logDirectory, string exception) + { + WriteEvent(FileCreateExceptionEventId, logDirectory, exception); + } + + [NonEvent] + public void SelfDiagnosticsFileCreateException(string logDirectory, Exception ex) + { + if (IsEnabled(EventLevel.Warning, EventKeywords.All)) + { + SelfDiagnosticsFileCreateException(logDirectory, ex.ToString()); + } + } +} + diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/TelemetryCommonExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/TelemetryCommonExtensions.cs new file mode 100644 index 0000000000..022a87a524 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry.Internal/TelemetryCommonExtensions.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry.Internal; + +/// +/// Extensions for common telemetry utilities. +/// +internal static class TelemetryCommonExtensions +{ + internal const string UnsupportedEnumValueExceptionMessage = $"Unsupported value for enum type {nameof(HttpRouteParameterRedactionMode)}."; + + /// + /// Gets the request name. + /// + /// Request metadata. + /// Returns the name of the request. + public static string GetRequestName(this RequestMetadata metadata) + { + _ = Throw.IfNull(metadata); + + if (metadata.RequestName == TelemetryConstants.Unknown) + { + return metadata.RequestRoute; + } + + return metadata.RequestName; + } + + /// + /// Adds http route processing elements. + /// + /// object. + /// Returns object. + public static IServiceCollection AddHttpRouteProcessor(this IServiceCollection services) + { + services.TryAddActivatedSingleton(); + services.TryAddActivatedSingleton(); + return services; + } + + /// + /// Adds HTTP headers redactor. + /// + /// object. + /// Returns object. + public static IServiceCollection AddHttpHeadersRedactor(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + services.TryAddActivatedSingleton(); + return services; + } + + /// + /// Adds instance to services collection. + /// + /// object instance. + /// object for chaining. + public static IServiceCollection AddOutgoingRequestContext(this IServiceCollection services) + { + services.TryAddSingleton(); + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/Constants.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/Constants.cs new file mode 100644 index 0000000000..567c362262 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/Constants.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Http.Telemetry; + +namespace Microsoft.Extensions.Telemetry; + +/// +/// Common telemetry constants used by various telemetry libraries. +/// +internal static class Constants +{ + public const int ASCIICharCount = 128; + + public const char DefaultRouteEndDelim = '?'; + + public static class HttpWebConstants + { + /// + /// Request Route HTTP Header key. + /// + public const string RequestRouteHeader = $"X-{TelemetryConstants.RequestMetadataKey}-{nameof(RequestMetadata.RequestRoute)}"; + + /// + /// Request Name HTTP Header key. + /// + public const string RequestNameHeader = $"X-{TelemetryConstants.RequestMetadataKey}-{nameof(RequestMetadata.RequestName)}"; + + /// + /// Dependency Name HTTP Header key. + /// + public const string DependencyNameHeader = $"X-{TelemetryConstants.RequestMetadataKey}-{nameof(RequestMetadata.DependencyName)}"; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/DownstreamDependencyMetadataManager.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/DownstreamDependencyMetadataManager.cs new file mode 100644 index 0000000000..d598253ad4 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/DownstreamDependencyMetadataManager.cs @@ -0,0 +1,398 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Http; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Telemetry.Internal; + +namespace Microsoft.Extensions.Telemetry; + +internal sealed class DownstreamDependencyMetadataManager : IDownstreamDependencyMetadataManager +{ + internal readonly struct ProcessedMetadata + { + public FrozenRequestMetadataTrieNode[] Nodes { get; init; } + public RequestMetadata[] RequestMetadatas { get; init; } + } + + private const char AsteriskChar = '*'; + private static readonly Regex _routeRegex = DownstreamDependencyMetadataManagerRegex.MakeRouteRegex(); + private static readonly char[] _toUpper = MakeToUpperArray(); + + private readonly HostSuffixTrieNode _hostSuffixTrieRoot = new(); + private readonly FrozenDictionary _frozenProcessedMetadataMap; + + public DownstreamDependencyMetadataManager(IEnumerable downstreamDependencyMetadata) + { + Dictionary dependencyTrieMap = new(); + foreach (var dependency in downstreamDependencyMetadata) + { + AddDependency(dependency, dependencyTrieMap); + } + + _frozenProcessedMetadataMap = ProcessDownstreamDependencyMetadata(dependencyTrieMap).ToFrozenDictionary(StringComparer.Ordinal, optimizeForReading: true); + } + + public RequestMetadata? GetRequestMetadata(HttpRequestMessage requestMessage) + { +#pragma warning disable CA1031 // Do not catch general exception types + try + { + if (requestMessage.RequestUri == null) + { + return null; + } + + string dependencyName = GetHostDependencyName(requestMessage.RequestUri.Host); + return GetRequestMetadataInternal(requestMessage.Method.Method, requestMessage.RequestUri.AbsolutePath, dependencyName); + } + catch (Exception) + { + // Catch exceptions here to avoid impacting services if a bug ever gets introduced in this path. + return null; + } +#pragma warning restore CA1031 // Do not catch general exception types + } + + public RequestMetadata? GetRequestMetadata(HttpWebRequest requestMessage) + { +#pragma warning disable CA1031 // Do not catch general exception types + try + { + string dependencyName = GetHostDependencyName(requestMessage.RequestUri.Host); + return GetRequestMetadataInternal(requestMessage.Method, requestMessage.RequestUri.AbsolutePath, dependencyName); + } + catch (Exception) + { + // Catch exceptions here to avoid impacting services if a bug ever gets introduced in this path. + return null; + } +#pragma warning restore CA1031 // Do not catch general exception types + } + + private static char[] MakeToUpperArray() + { + // Initialize the _toUpper array for quick conversion of any ascii char to upper + // without incurring cost of checking whether the character requires conversion. + + var a = new char[Constants.ASCIICharCount]; + for (int i = 0; i < Constants.ASCIICharCount; i++) + { + a[i] = char.ToUpperInvariant((char)i); + } + + return a; + } + + private static void AddRouteToTrie(RequestMetadata routeMetadata, Dictionary dependencyTrieMap) + { + if (!dependencyTrieMap.TryGetValue(routeMetadata.DependencyName, out var routeMetadataTrieRoot)) + { + routeMetadataTrieRoot = new RequestMetadataTrieNode(); + dependencyTrieMap.Add(routeMetadata.DependencyName, routeMetadataTrieRoot); + } + + var trieCurrent = routeMetadataTrieRoot; + trieCurrent.Parent = trieCurrent; + + if (routeMetadata.RequestRoute[0] != '/') + { + routeMetadata.RequestRoute = $"/{routeMetadata.RequestRoute}"; + } + + var route = _routeRegex.Replace(routeMetadata.RequestRoute, "*"); + route = route.ToUpperInvariant(); + for (int i = 0; i < route.Length; i++) + { + char ch = route[i]; + if (ch >= Constants.ASCIICharCount) + { + return; + } + + trieCurrent.Nodes[ch] ??= new(); + trieCurrent.YoungestChild = ch < trieCurrent.YoungestChild ? ch : trieCurrent.YoungestChild; + trieCurrent.EldestChild = ch > trieCurrent.EldestChild ? ch : trieCurrent.EldestChild; + trieCurrent.ChildNodesCount = (byte)(trieCurrent.EldestChild - trieCurrent.YoungestChild + 1); + trieCurrent.Nodes[ch].Parent = trieCurrent; + trieCurrent = trieCurrent.Nodes[ch]; + + // When we find an * then the next character is the delimiter where next part of the path begins + // Store it to use it when looking up the trie to find where the part of the route after param value start. + if (ch == AsteriskChar && i < route.Length - 1) + { + trieCurrent.Delimiter = route[i + 1]; + } + } + + var httpMethod = routeMetadata.MethodType.ToUpperInvariant(); + for (int j = 0; j < httpMethod.Length; j++) + { + char ch = httpMethod[j]; + if (ch >= Constants.ASCIICharCount) + { + return; + } + + trieCurrent.Nodes[ch] ??= new(); + trieCurrent.YoungestChild = ch < trieCurrent.YoungestChild ? ch : trieCurrent.YoungestChild; + trieCurrent.EldestChild = ch > trieCurrent.EldestChild ? ch : trieCurrent.EldestChild; + trieCurrent.ChildNodesCount = (byte)(trieCurrent.EldestChild - trieCurrent.YoungestChild + 1); + trieCurrent.Nodes[ch].Parent = trieCurrent; + trieCurrent = trieCurrent.Nodes[ch]; + } + + trieCurrent.RequestMetadata = routeMetadata; + } + + private static Dictionary ProcessDownstreamDependencyMetadata(Dictionary dependencyTrieMap) + { + Dictionary finalArrayDict = new(); + foreach (var dep in dependencyTrieMap) + { + var finalArray = ProcessDownstreamDependencyMetadataInternal(dep.Value); + finalArrayDict.Add(dep.Key, finalArray); + } + + return finalArrayDict; + } + + // This method has 100% coverage but there is some issue with the code coverage tool in the CI pipeline which makes it + // buggy and complain about some parts the code in this method as not covered. If you make changes to this method, please + // remove the ExlcudeCodeCoverage attribute and ensure it's covered fully using local runs and enable it back before + // pushing the change to PR. + [ExcludeFromCodeCoverage] + private static ProcessedMetadata ProcessDownstreamDependencyMetadataInternal(RequestMetadataTrieNode requestMetadataTrieRoot) + { + Queue queue = new(); + queue.Enqueue(requestMetadataTrieRoot); + int finalArraySize = 0; + int requestMetadataArraySize = 1; + while (queue.Count > 0) + { + var trieNode = queue.Dequeue(); + finalArraySize += trieNode.ChildNodesCount; + for (int i = 0; i < Constants.ASCIICharCount; i++) + { + var node = trieNode.Nodes[i]; + if (node != null) + { + if (node.RequestMetadata != null) + { + requestMetadataArraySize++; + } + + queue.Enqueue(node); + } + } + } + + var requestMetadatas = new RequestMetadata[requestMetadataArraySize + 1]; + requestMetadatas[0] = requestMetadataTrieRoot.RequestMetadata!; + + var processedNodes = new FrozenRequestMetadataTrieNode[finalArraySize + 1]; + processedNodes[0] = new FrozenRequestMetadataTrieNode + { + ChildStartIndex = 1, + YoungestChild = requestMetadataTrieRoot.YoungestChild, + ChildNodesCount = 1, + RequestMetadataEntryIndex = 0 + }; + + queue.Enqueue(requestMetadataTrieRoot); + int processedNodeIndex = 1; + int childStartIndex = 2; + int requestMetadataIndex = 0; + while (queue.Count > 0) + { + var trieNode = queue.Dequeue(); + for (int i = 0; i < Constants.ASCIICharCount; i++) + { + var node = trieNode.Nodes[i]; + if (node != null) + { + var d = new FrozenRequestMetadataTrieNode + { + ChildStartIndex = childStartIndex, + Delimiter = node.Delimiter, + ChildNodesCount = node.ChildNodesCount, + YoungestChild = node.YoungestChild + }; + + if (node.RequestMetadata != null) + { + d.RequestMetadataEntryIndex = ++requestMetadataIndex; + requestMetadatas[requestMetadataIndex] = node.RequestMetadata; + } + + processedNodes[processedNodeIndex + i - node.Parent!.YoungestChild] = d; + + childStartIndex += node.ChildNodesCount; + + queue.Enqueue(node); + } + } + + processedNodeIndex += trieNode.ChildNodesCount; + } + + return new ProcessedMetadata { Nodes = processedNodes, RequestMetadatas = requestMetadatas }; + } + + private static FrozenRequestMetadataTrieNode? GetChildNode(char ch, FrozenRequestMetadataTrieNode node, ProcessedMetadata routeMetadataRoot) + { + bool isValid = ch >= node.YoungestChild && ch <= (node.YoungestChild + node.ChildNodesCount); + if (isValid) + { + return routeMetadataRoot.Nodes![node.ChildStartIndex + ch - node.YoungestChild]; + } + + return null; + } + + private void AddDependency(IDownstreamDependencyMetadata downstreamDependencyMetadata, Dictionary dependencyTrieMap) + { + foreach (var hostNameSuffix in downstreamDependencyMetadata.UniqueHostNameSuffixes) + { + // Add hostname to hostname suffix trie + AddHostnameToTrie(hostNameSuffix, downstreamDependencyMetadata.DependencyName); + } + + foreach (var routeMetadata in downstreamDependencyMetadata.RequestMetadata) + { + routeMetadata.DependencyName = downstreamDependencyMetadata.DependencyName; + + // Add route metadata to the route per dependency trie + AddRouteToTrie(routeMetadata, dependencyTrieMap); + } + } + + private void AddHostnameToTrie(string hostNameSuffix, string dependencyName) + { + hostNameSuffix = hostNameSuffix.ToUpperInvariant(); + var trieCurrent = _hostSuffixTrieRoot; + for (int i = hostNameSuffix.Length - 1; i >= 0; i--) + { + char ch = hostNameSuffix[i]; + if (ch >= Constants.ASCIICharCount) + { + return; + } + + trieCurrent.Nodes[ch] ??= new HostSuffixTrieNode(); + trieCurrent = trieCurrent.Nodes[ch]; + } + + trieCurrent.DependencyName = dependencyName; + } + + private string GetHostDependencyName(string host) + { + string dependencyName = string.Empty; + var trieCurrent = _hostSuffixTrieRoot; + for (int i = host.Length - 1; i >= 0; i--) + { + char ch = host[i]; + if (ch >= Constants.ASCIICharCount) + { + return string.Empty; + } + + ch = _toUpper[ch]; + if (trieCurrent.Nodes[ch] == null) + { + break; + } + + trieCurrent = trieCurrent.Nodes[ch]; + if (!string.IsNullOrEmpty(trieCurrent.DependencyName)) + { + dependencyName = trieCurrent.DependencyName; + } + } + + return dependencyName; + } + + private RequestMetadata? GetRequestMetadataInternal(string httpMethod, string requestPath, string dependencyName) + { + if (!_frozenProcessedMetadataMap.TryGetValue(dependencyName, out var routeMetadataTrieRoot)) + { + return null; + } + + var trieCurrent = routeMetadataTrieRoot.Nodes[0]; + var lastStartNode = trieCurrent; + var requestPathEndIndex = requestPath.Length; + for (int i = 0; i < requestPathEndIndex; i++) + { + char ch = _toUpper[requestPath[i]]; + var childNode = GetChildNode(ch, trieCurrent, routeMetadataTrieRoot); + if (childNode == null) + { + trieCurrent = lastStartNode; + var asteriskChildNode = GetChildNode(AsteriskChar, trieCurrent, routeMetadataTrieRoot); + if (asteriskChildNode == null) + { + break; + } + + // advance the trie to next delimiter + trieCurrent = asteriskChildNode; + + if (trieCurrent.Delimiter == Constants.DefaultRouteEndDelim) + { + break; + } + + var nextDelimiterIndex = requestPath.IndexOf(trieCurrent.Delimiter, i, requestPathEndIndex - i); + + // if we reached end of the request path or end of trie, break + var delimChildNode = GetChildNode(trieCurrent.Delimiter, trieCurrent, routeMetadataTrieRoot); + if (nextDelimiterIndex == -1 || delimChildNode == null) + { + break; + } + + // Advance i to the next separator index +#pragma warning disable S127 // "for" loop stop conditions should be invariant + i = nextDelimiterIndex; +#pragma warning restore S127 // "for" loop stop conditions should be invariant + + trieCurrent = delimChildNode; + + // Set lastStartNode to trieCurrent + lastStartNode = trieCurrent; + + continue; + } + + trieCurrent = childNode; + if (GetChildNode(AsteriskChar, trieCurrent, routeMetadataTrieRoot) != null) + { + lastStartNode = trieCurrent; + } + } + + // Now that path is found, Find the method type branch of the trie + for (int j = 0; j < httpMethod.Length; j++) + { + char ch = _toUpper[httpMethod[j]]; + var childNode = GetChildNode(ch, trieCurrent, routeMetadataTrieRoot); + if (childNode == null) + { + return null; + } + + trieCurrent = childNode; + } + + return trieCurrent.RequestMetadataEntryIndex == -1 ? null : routeMetadataTrieRoot.RequestMetadatas[trieCurrent.RequestMetadataEntryIndex]; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/DownstreamDependencyMetadataManagerRegex.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/DownstreamDependencyMetadataManagerRegex.cs new file mode 100644 index 0000000000..45fec4a36b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/DownstreamDependencyMetadataManagerRegex.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.RegularExpressions; + +namespace Microsoft.Extensions.Telemetry; + +#if NET7_0_OR_GREATER +internal static partial class DownstreamDependencyMetadataManagerRegex +#else +internal static class DownstreamDependencyMetadataManagerRegex +#endif +{ + private const string RouteRegexString = @"(\{[^\}]+\})+"; + +#if NET7_0_OR_GREATER + + [GeneratedRegex(RouteRegexString)] + public static partial Regex MakeRouteRegex(); + +#else + + public static Regex MakeRouteRegex() => new(RouteRegexString, RegexOptions.Compiled); + +#endif +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/FrozenRequestMetadataTrieNode.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/FrozenRequestMetadataTrieNode.cs new file mode 100644 index 0000000000..41b82ab5b2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/FrozenRequestMetadataTrieNode.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Telemetry; + +internal sealed class FrozenRequestMetadataTrieNode +{ + public char Delimiter { get; set; } = Constants.DefaultRouteEndDelim; + public byte ChildNodesCount { get; set; } + public char YoungestChild { get; set; } + public int ChildStartIndex { get; set; } + public int RequestMetadataEntryIndex { get; set; } = -1; +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/HostSuffixTrieNode.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/HostSuffixTrieNode.cs new file mode 100644 index 0000000000..a1a36e830e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/HostSuffixTrieNode.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Telemetry; +internal sealed class HostSuffixTrieNode +{ + private const int ASCIICharCount = 128; + + public string DependencyName { get; set; } = string.Empty; + + public HostSuffixTrieNode[] Nodes { get; } = new HostSuffixTrieNode[ASCIICharCount]; +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/RequestMetadataTrieNode.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/RequestMetadataTrieNode.cs new file mode 100644 index 0000000000..e8b9fee7a0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/RequestMetadataTrieNode.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Http.Telemetry; + +namespace Microsoft.Extensions.Telemetry; +internal sealed class RequestMetadataTrieNode +{ + public byte ChildNodesCount { get; set; } + public char YoungestChild { get; set; } = (char)Constants.ASCIICharCount; + public char EldestChild { get; set; } + public char Delimiter { get; set; } = Constants.DefaultRouteEndDelim; + + public RequestMetadata? RequestMetadata { get; set; } + + public RequestMetadataTrieNode? Parent { get; set; } + + // The property has actually 100% coverage, but due to a bug in the code coverage tool, + // a lower number is reported. Therefore, we temporarily exclude this property + // from the coverage measurements. Once the bug in the code coverage tool is fixed, + // the exclusion attribute can be removed. + [ExcludeFromCodeCoverage] + public RequestMetadataTrieNode[] Nodes { get; } = new RequestMetadataTrieNode[Constants.ASCIICharCount]; +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/TelemetryExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/TelemetryExtensions.cs new file mode 100644 index 0000000000..1d106201a9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Telemetry/TelemetryExtensions.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +using System.Collections.Generic; +#endif +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Telemetry; + +/// +/// Extensions for telemetry utilities. +/// +public static class TelemetryExtensions +{ + /// + /// Sets metadata for outgoing requests to be used for telemetry purposes. + /// + /// object. + /// Metadata for the request. + [Experimental] + public static void SetRequestMetadata(this HttpWebRequest request, RequestMetadata metadata) + { + _ = Throw.IfNull(request); + _ = Throw.IfNull(metadata); + + request.Headers.Add(Constants.HttpWebConstants.RequestRouteHeader, metadata.RequestRoute); + request.Headers.Add(Constants.HttpWebConstants.RequestNameHeader, metadata.RequestName); + request.Headers.Add(Constants.HttpWebConstants.DependencyNameHeader, metadata.DependencyName); + } + + /// + /// Sets metadata for outgoing requests to be used for telemetry purposes. + /// + /// object. + /// Metadata for the request. + public static void SetRequestMetadata(this HttpRequestMessage request, RequestMetadata metadata) + { + _ = Throw.IfNull(request); + _ = Throw.IfNull(metadata); + +#if NET5_0_OR_GREATER + _ = request.Options.TryAdd(TelemetryConstants.RequestMetadataKey, metadata); +#else + request.Properties.Add(TelemetryConstants.RequestMetadataKey, metadata); +#endif + } + + /// + /// Gets metadata for outgoing requests to be used for telemetry purposes. + /// + /// object. + /// Request metadata. + [Experimental] + public static RequestMetadata? GetRequestMetadata(this HttpWebRequest request) + { + _ = Throw.IfNull(request); + + string? requestRoute = request.Headers.Get(Constants.HttpWebConstants.RequestRouteHeader); + + if (requestRoute == null) + { + return null; + } + + string? dependencyName = request.Headers.Get(Constants.HttpWebConstants.DependencyNameHeader); + string? requestName = request.Headers.Get(Constants.HttpWebConstants.RequestNameHeader); + + var requestMetadata = new RequestMetadata + { + RequestRoute = requestRoute, + RequestName = string.IsNullOrEmpty(requestName) ? TelemetryConstants.Unknown : requestName, + DependencyName = string.IsNullOrEmpty(dependencyName) ? TelemetryConstants.Unknown : dependencyName + }; + + return requestMetadata; + } + + /// + /// Gets metadata for outgoing requests to be used for telemetry purposes. + /// + /// object. + /// Request metadata or . + public static RequestMetadata? GetRequestMetadata(this HttpRequestMessage request) + { + _ = Throw.IfNull(request); + +#if NET5_0_OR_GREATER + _ = request.Options.TryGetValue(new HttpRequestOptionsKey(TelemetryConstants.RequestMetadataKey), out var metadata); + return metadata; +#else + _ = request.Properties.TryGetValue(TelemetryConstants.RequestMetadataKey, out var metadata); + return (RequestMetadata?)metadata; +#endif + } + + /// + /// Adds dependency metadata. + /// + /// object instance. + /// DownstreamDependencyMetadata object to add. + /// object for chaining. + public static IServiceCollection AddDownstreamDependencyMetadata(this IServiceCollection services, IDownstreamDependencyMetadata downstreamDependencyMetadata) + { + _ = Throw.IfNull(services); + services.TryAddSingleton(); + _ = services.AddSingleton(downstreamDependencyMetadata); + + return services; + } + + /// + /// Adds dependency metadata. + /// + /// instance to be registered. + /// object instance. + /// object for chaining. + public static IServiceCollection AddDownstreamDependencyMetadata(this IServiceCollection services) + where T : class, IDownstreamDependencyMetadata + { + _ = Throw.IfNull(services); + services.TryAddSingleton(); + _ = services.AddSingleton(); + + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/Internal/SamplingOptionsAutoValidator.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/Internal/SamplingOptionsAutoValidator.cs new file mode 100644 index 0000000000..083fd1c358 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/Internal/SamplingOptionsAutoValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Telemetry.Tracing.Internal; + +[OptionsValidator] +internal sealed partial class SamplingOptionsAutoValidator : IValidateOptions +{ +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/Internal/SamplingOptionsCustomValidator.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/Internal/SamplingOptionsCustomValidator.cs new file mode 100644 index 0000000000..d8fe605ba4 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/Internal/SamplingOptionsCustomValidator.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Telemetry.Tracing.Internal; + +internal sealed class SamplingOptionsCustomValidator : IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, SamplingOptions o) => + o.SamplerType switch + { + SamplerType.TraceIdRatioBased + when o.TraceIdRatioBasedSamplerOptions is null => + ValidateOptionsResult.Fail( + "Sampler type is set for trace Id ratio based " + + "but options are not set for it."), + SamplerType.ParentBased + when o.ParentBasedSamplerOptions is null => + ValidateOptionsResult.Fail( + "Sampler type is set for parent based " + + "but options are not set for it."), + SamplerType.ParentBased + when o.ParentBasedSamplerOptions.RootSamplerType == SamplerType.ParentBased => + ValidateOptionsResult.Fail( + "Sampler type is set for parent based " + + "but the root sampler is also set to parent based."), + SamplerType.ParentBased + when o.ParentBasedSamplerOptions.RootSamplerType == SamplerType.TraceIdRatioBased + && o.TraceIdRatioBasedSamplerOptions is null => + ValidateOptionsResult.Fail( + "Sampler type is set for parent based with trace Id ratio based root sampler " + + "but the trace Id ratio based options are not set."), + SamplerType st when !Enum.IsDefined(typeof(SamplerType), st) => + ValidateOptionsResult.Fail( + $"Unknown sampler type '{st}'."), + _ => + ValidateOptionsResult.Success + }; +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/ParentBasedSamplerOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/ParentBasedSamplerOptions.cs new file mode 100644 index 0000000000..0f74d3ec97 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/ParentBasedSamplerOptions.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Telemetry.Tracing; + +/// +/// Options for the parent based sampler. +/// +public class ParentBasedSamplerOptions +{ + /// + /// Gets or sets the type of sampler to be used for making sampling decision for root activity. + /// + /// + /// Defaults to the sampler. + /// + public SamplerType RootSamplerType { get; set; } = SamplerType.AlwaysOn; +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/SamplerType.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/SamplerType.cs new file mode 100644 index 0000000000..c331269dcd --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/SamplerType.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Telemetry.Tracing; + +/// +/// Sampler type. +/// +public enum SamplerType +{ + /// + /// Always samples traces. + /// + AlwaysOn, + + /// + /// Never samples traces. + /// + AlwaysOff, + + /// + /// Samples traces according to the specified probability. + /// + TraceIdRatioBased, + + /// + /// Samples traces if the parent Activity is sampled. + /// + ParentBased +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/SamplingExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/SamplingExtensions.cs new file mode 100644 index 0000000000..e7cf4df1a6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/SamplingExtensions.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Extensions.Telemetry.Tracing.Internal; +using Microsoft.Shared.Diagnostics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Telemetry.Tracing; + +/// +/// Extension methods for setting up trace sampling. +/// +public static class SamplingExtensions +{ + /// + /// Adds sampling for traces. + /// + /// The to add enricher. + /// The configuration delegate. + /// The so that additional calls can be chained. + /// The argument is . + /// Provided is not bound to a . + public static TracerProviderBuilder AddSampling( + this TracerProviderBuilder builder, + Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + return builder + .ConfigureServices(services => services.Configure(configure)) + .AddSamplingInternal(); + } + + /// + /// Adds sampling for traces. + /// + /// The to add enricher. + /// The to use for configuring . + /// The so that additional calls can be chained. + /// The argument is . + /// Provided is not bound to a . + public static TracerProviderBuilder AddSampling( + this TracerProviderBuilder builder, + IConfigurationSection section) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(section); + + return builder + .ConfigureServices(services => services.Configure(section)) + .AddSamplingInternal(); + } + + private static TracerProviderBuilder AddSamplingInternal(this TracerProviderBuilder builder) + { + return builder.ConfigureServices(services => + { + _ = services.AddValidatedOptions(); + _ = services.AddValidatedOptions(); + + _ = services.ConfigureOpenTelemetryTracerProvider((serviceProvider, builder) => + { + var o = serviceProvider.GetRequiredService>().Value; + + Sampler sampler = o.SamplerType switch + { + SamplerType.AlwaysOn => + new AlwaysOnSampler(), + SamplerType.AlwaysOff => + new AlwaysOffSampler(), + SamplerType.TraceIdRatioBased => + new TraceIdRatioBasedSampler( + o.TraceIdRatioBasedSamplerOptions!.Probability), + SamplerType.ParentBased + when o!.ParentBasedSamplerOptions!.RootSamplerType == SamplerType.AlwaysOn => + new ParentBasedSampler( + new AlwaysOnSampler()), + SamplerType.ParentBased + when o!.ParentBasedSamplerOptions!.RootSamplerType == SamplerType.AlwaysOff => + new ParentBasedSampler( + new AlwaysOffSampler()), + SamplerType.ParentBased + when o!.ParentBasedSamplerOptions!.RootSamplerType == SamplerType.TraceIdRatioBased => + new ParentBasedSampler( + new TraceIdRatioBasedSampler( + o!.TraceIdRatioBasedSamplerOptions!.Probability)), + SamplerType st => + throw new InvalidOperationException($"Invalid sampling configuration for '{st}'.") + }; + + _ = builder.SetSampler(sampler); + }); + }); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/SamplingOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/SamplingOptions.cs new file mode 100644 index 0000000000..3319d70a7f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/SamplingOptions.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Telemetry.Tracing; + +/// +/// Options for sampling. +/// +public class SamplingOptions +{ + /// + /// Gets or sets the type of the sampler. + /// + /// + /// Defaults to the sampler. + /// + public SamplerType SamplerType { get; set; } = SamplerType.AlwaysOn; + + /// + /// Gets or sets options for the parent based sampler. + /// + /// + /// Defaults to . + /// + public ParentBasedSamplerOptions? ParentBasedSamplerOptions { get; set; } + + /// + /// Gets or sets options for the trace Id ratio based sampler. + /// + /// + /// Defaults to . + /// + [ValidateObjectMembers] + public TraceIdRatioBasedSamplerOptions? TraceIdRatioBasedSamplerOptions { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/TraceIdRatioBasedSamplerOptions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/TraceIdRatioBasedSamplerOptions.cs new file mode 100644 index 0000000000..6df54411b9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Tracing.Sampling/TraceIdRatioBasedSamplerOptions.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Extensions.Telemetry.Tracing; + +/// +/// Options for the trace Id ratio based sampler. +/// +public class TraceIdRatioBasedSamplerOptions +{ + /// + /// Gets or sets the desired probability of sampling. + /// + /// + /// Defaults to 1. + /// + [Range(0.0, 1.0)] + public double Probability { get; set; } = 1.0; +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Tracing/TraceEnrichmentProcessor.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Tracing/TraceEnrichmentProcessor.cs new file mode 100644 index 0000000000..caa3ba499a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Tracing/TraceEnrichmentProcessor.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.Extensions.Telemetry.Enrichment; +using OpenTelemetry; + +namespace Microsoft.Extensions.Telemetry.Enrichment; + +internal sealed class TraceEnrichmentProcessor : BaseProcessor +{ + private readonly ITraceEnricher[] _traceEnrichers; + + public TraceEnrichmentProcessor(IEnumerable traceEnrichers) + { + _traceEnrichers = traceEnrichers.ToArray(); + } + +#if NETCOREAPP3_1_OR_GREATER + public override void OnStart(Activity activity) + { + foreach (var enricher in _traceEnrichers) + { + enricher.EnrichOnActivityStart(activity); + } + } +#endif + public override void OnEnd(Activity activity) + { + foreach (var enricher in _traceEnrichers) + { + enricher.Enrich(activity); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Telemetry/Tracing/TracingEnricherExtensions.cs b/src/Libraries/Microsoft.Extensions.Telemetry/Tracing/TracingEnricherExtensions.cs new file mode 100644 index 0000000000..36cc62c5b4 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Telemetry/Tracing/TracingEnricherExtensions.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Shared.Diagnostics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Telemetry.Enrichment; + +/// +/// Extension methods for Tracing. +/// +public static class TracingEnricherExtensions +{ + /// + /// Adds an enricher to enrich all traces. + /// + /// Enricher object type. + /// The to add enricher. + /// The so that additional calls can be chained. + /// The argument is . + public static TracerProviderBuilder AddTraceEnricher(this TracerProviderBuilder builder) + where T : class, ITraceEnricher + { + _ = Throw.IfNull(builder); + + return builder.ConfigureServices(services => services.AddTraceEnricher()); + } + + /// + /// Adds an enricher to enrich all traces. + /// + /// The to add enricher. + /// The enricher to be added for enriching traces. + /// The so that additional calls can be chained. + /// The argument or is . + public static TracerProviderBuilder AddTraceEnricher(this TracerProviderBuilder builder, ITraceEnricher enricher) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(enricher); + + return builder.ConfigureServices(services => services.AddTraceEnricher(enricher)); + } + + /// + /// Adds an enricher to enrich all traces. + /// + /// Enricher object type. + /// The to add this enricher to. + /// The so that additional calls can be chained. + /// The argument is . + [Experimental] + public static IServiceCollection AddTraceEnricher(this IServiceCollection services) + where T : class, ITraceEnricher + { + _ = Throw.IfNull(services); + + return services + .AddSingleton() + .TryAddEnrichmentProcessor(); + } + + /// + /// Adds an enricher to enrich all traces. + /// + /// The to add this enricher to. + /// Enricher to be added. + /// The so that additional calls can be chained. + /// The argument or is . + [Experimental] + public static IServiceCollection AddTraceEnricher(this IServiceCollection services, ITraceEnricher enricher) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(enricher); + + return services + .AddSingleton(enricher) + .TryAddEnrichmentProcessor(); + } + + private static IServiceCollection TryAddEnrichmentProcessor(this IServiceCollection services) + { + // Stryker disable once Linq + if (!services.Any(x => x.ServiceType == typeof(TraceEnrichmentProcessor))) + { + _ = services + .AddSingleton() + .ConfigureOpenTelemetryTracerProvider((sp, builder) => + { + var proc = sp.GetRequiredService(); + _ = builder.AddProcessor(proc); + }); + } + + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/FakeTimeProvider.cs b/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/FakeTimeProvider.cs new file mode 100644 index 0000000000..c2590f6516 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/FakeTimeProvider.cs @@ -0,0 +1,185 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Threading; + +namespace Microsoft.Extensions.Time.Testing; + +/// +/// A synthetic clock used to provide deterministic behavior in tests. +/// +public class FakeTimeProvider : TimeProvider +{ + internal static readonly DateTimeOffset Epoch = new(2000, 1, 1, 0, 0, 0, 0, TimeSpan.Zero); + + internal readonly List Waiters = new(); + + private DateTimeOffset _now = Epoch; + private TimeZoneInfo _localTimeZone; + + /// + /// Initializes a new instance of the class. + /// + /// + /// This creates a clock whose time is set to midnight January 1st 2000. + /// The clock is set to not automatically advance every time it is read. + /// + public FakeTimeProvider() + { + _localTimeZone = TimeZoneInfo.Utc; + } + + /// + /// Initializes a new instance of the class. + /// + /// The initial time reported by the clock. + public FakeTimeProvider(DateTimeOffset startTime) + : this() + { + _now = startTime; + } + + /// + public override DateTimeOffset GetUtcNow() + { + return _now; + } + + /// + /// Sets the date and time in the UTC timezone. + /// + /// The date and time in the UTC timezone. + public void SetUtcNow(DateTimeOffset value) + { + List waiters; + lock (Waiters) + { + _now = value; + waiters = GetWaitersToWake(); + } + + WakeWaiters(waiters); + } + + /// + /// Advances the clock's time by a specific amount. + /// + /// The amount of time to advance the clock by. + public void Advance(TimeSpan delta) + { + List waiters; + lock (Waiters) + { + _now += delta; + waiters = GetWaitersToWake(); + } + + WakeWaiters(waiters); + } + + /// + /// Advances the clock's time by one millisecond. + /// + public void Advance() => Advance(TimeSpan.FromMilliseconds(1)); + + /// + public override long GetTimestamp() + { + // Notionally we're multiplying by frequency and dividing by ticks per second, + // which are the same value for us. Don't actually do the math as the full + // precision of ticks (a long) cannot be represented in a double during division. + // For test stability we want a reproducible result. + // + // The same issue could occur converting back, in GetElapsedTime(). Unfortunately + // that isn't virtual so we can't do the same trick. However, if tests advance + // the clock in multiples of 1ms or so this loss of precision will not be visible. + Debug.Assert(TimestampFrequency == TimeSpan.TicksPerSecond, "Assuming frequency equals ticks per second"); + return _now.Ticks; + } + + /// + public override TimeZoneInfo LocalTimeZone => _localTimeZone; + + /// + /// Sets the local timezone. + /// + /// The local timezone. + public void SetLocalTimeZone(TimeZoneInfo localTimeZone) + { + _localTimeZone = localTimeZone; + } + + /// + /// Gets the amount that the value from increments per second. + /// + /// + /// We fix it here for test instability which would otherwise occur within + /// when the result of multiplying underlying ticks + /// by frequency and dividing by ticks per second is truncated to long. + /// + /// Similarly truncation could occur when reversing this calculation to figure a time + /// interval from the difference between two timestamps. + /// + /// As ticks per second is always 10^7, setting frequency to 10^7 is convenient. + /// It happens that the system usually uses 10^9 or 10^6 so this could expose + /// any assumption made that it is one of those values. + /// + public override long TimestampFrequency => TimeSpan.TicksPerSecond; + + /// + /// Returns a string representation this clock's current time. + /// + /// A string representing the clock's current time. + public override string ToString() => GetUtcNow().ToString("yyyy-MM-ddTHH:mm:ss.fff", CultureInfo.InvariantCulture); + + /// + public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) + { + return new FakeTimeProviderTimer(this, dueTime, period, callback, state); + } + + internal void AddWaiter(FakeTimeProviderTimer.Waiter waiter) + { + lock (Waiters) + { + Waiters.Add(waiter); + } + } + + internal void RemoveWaiter(FakeTimeProviderTimer.Waiter waiter) + { + lock (Waiters) + { + _ = Waiters.Remove(waiter); + } + } + + private List GetWaitersToWake() + { + var l = new List(Waiters.Count); + foreach (var w in Waiters) + { + if (_now >= w.WakeupTime) + { + l.Add(w); + } + } + + return l; + } + + private void WakeWaiters(List waiters) + { + foreach (var w in waiters) + { + if (_now >= w.WakeupTime) + { + w.TriggerAndSchedule(false); + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/FakeTimeProviderTimer.cs b/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/FakeTimeProviderTimer.cs new file mode 100644 index 0000000000..45989de13d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/FakeTimeProviderTimer.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Time.Testing; + +internal sealed class FakeTimeProviderTimer : ITimer +{ + private Waiter? _waiter; + + public FakeTimeProviderTimer(FakeTimeProvider fakeTimeProvider, TimeSpan dueTime, TimeSpan period, TimerCallback callback, object? state) + { + _waiter = new Waiter(fakeTimeProvider, dueTime, period, callback, state); + fakeTimeProvider.AddWaiter(_waiter); + _waiter.TriggerAndSchedule(true); + } + + public bool Change(TimeSpan dueTime, TimeSpan period) + { + if (_waiter == null) + { + return false; + } + + _waiter.ChangeAndValidateDurations(dueTime, period); + _waiter.TriggerAndSchedule(true); + + return true; + } + + // In case the timer is not disposed, this will remove the Waiter instance from the provider. + ~FakeTimeProviderTimer() => Dispose(false); + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public ValueTask DisposeAsync() + { + Dispose(true); + GC.SuppressFinalize(this); +#if NET5_0_OR_GREATER + return ValueTask.CompletedTask; +#else + return default; +#endif + } + + private void Dispose(bool _) + { + _waiter?.Dispose(); + _waiter = null; + } + + // We keep all timer state here in order to prevent Timer instances from being self-referential, + // which would block them being collected when someone forgets to call Dispose on the timer. With + // this arrangement, the Timer object will always be collectible, which will end up calling Dispose + // on this object due to the timer's finalizer. + internal sealed class Waiter : IDisposable + { + private const uint MaxSupportedTimeout = 0xfffffffe; + + private readonly FakeTimeProvider _fakeTimeProvider; + private readonly TimerCallback _callback; + private readonly object? _state; + + private long _periodMs; + private long _dueTimeMs; + public DateTimeOffset WakeupTime { get; set; } + + public Waiter(FakeTimeProvider fakeTimeProvider, TimeSpan dueTime, TimeSpan period, TimerCallback callback, object? state) + { + _fakeTimeProvider = fakeTimeProvider; + _callback = callback; + _state = state; + ChangeAndValidateDurations(dueTime, period); + } + + public void ChangeAndValidateDurations(TimeSpan dueTime, TimeSpan period) + { + _dueTimeMs = (long)dueTime.TotalMilliseconds; + _periodMs = (long)period.TotalMilliseconds; + +#pragma warning disable S3236 // Caller information arguments should not be provided explicitly + _ = Throw.IfOutOfRange(_dueTimeMs, -1, MaxSupportedTimeout, nameof(dueTime)); + _ = Throw.IfOutOfRange(_periodMs, -1, MaxSupportedTimeout, nameof(period)); +#pragma warning restore S3236 // Caller information arguments should not be provided explicitly + + } + + public void TriggerAndSchedule(bool restart) + { + if (restart) + { + WakeupTime = DateTimeOffset.MaxValue; + + if (_dueTimeMs == 0) + { + // If dueTime is zero, callback is invoked immediately + + _callback(_state); + } + else if (_dueTimeMs == Timeout.Infinite) + { + // If dueTime is Timeout.Infinite, callback is not invoked; the timer is disabled + + return; + } + else + { + // Schedule next event on dueTime + + WakeupTime = _fakeTimeProvider.GetUtcNow() + TimeSpan.FromMilliseconds(_dueTimeMs); + + return; + } + } + else + { + _callback(_state); + } + + // Schedule next event based on Period + + if (_periodMs == 0 || _periodMs == Timeout.Infinite) + { + WakeupTime = DateTimeOffset.MaxValue; + } + else + { + WakeupTime = _fakeTimeProvider.GetUtcNow() + TimeSpan.FromMilliseconds(_periodMs); + } + } + + public void Dispose() + { + _fakeTimeProvider.RemoveWaiter(this); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/Microsoft.Extensions.TimeProvider.Testing.csproj b/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/Microsoft.Extensions.TimeProvider.Testing.csproj new file mode 100644 index 0000000000..4f69cf72e9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/Microsoft.Extensions.TimeProvider.Testing.csproj @@ -0,0 +1,25 @@ + + + Microsoft.Extensions.TimeProvider.Testing + Hand-crafted fakes to make time-related testing easier. + Fundamentals + Testing + $(PackageTags);Testing + + + + normal + 97 + 100 + 96 + + + + + + + + + + + diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Exceptions/DatabaseClientException.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Exceptions/DatabaseClientException.cs new file mode 100644 index 0000000000..5bffb5912a --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Exceptions/DatabaseClientException.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace System.Cloud.DocumentDb; + +/// +/// Exception represent the operation is failed w/ a specific reason and should not retry. +/// +/// +/// Please check the log and eliminate this kind of requests. +/// Http code 400, 401, 403, 413. +/// Covered codes may vary on specific engine requirements. +/// +public class DatabaseClientException : DatabaseException +{ + /// + /// Initializes a new instance of the class. + /// + public DatabaseClientException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + public DatabaseClientException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// The exception related to the missing data. + public DatabaseClientException(string message, Exception innerException) + : base(message, innerException) + { + } +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Exceptions/DatabaseException.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Exceptions/DatabaseException.cs new file mode 100644 index 0000000000..b3e526ed17 --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Exceptions/DatabaseException.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; + +namespace System.Cloud.DocumentDb; + +/// +/// Base type for exceptions thrown by storage adapter. +/// +public class DatabaseException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public DatabaseException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + public DatabaseException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// The inner exception causing this exception. + public DatabaseException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// Exception status code. + /// Exception sub status code. + /// The request. + public DatabaseException(string message, int statusCode, int subStatusCode, RequestInfo requestInfo) + : base(message) + { + StatusCode = statusCode; + SubStatusCode = subStatusCode; + RequestInfo = requestInfo; + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// The inner exception causing this exception. + /// Exception status code. + /// Exception sub status code. + /// The request. + public DatabaseException(string message, Exception innerException, int statusCode, int subStatusCode, RequestInfo requestInfo) + : base(message, innerException) + { + StatusCode = statusCode; + SubStatusCode = subStatusCode; + RequestInfo = requestInfo; + } + + /// + /// Gets the status code indicating the exception root cause. + /// + public int StatusCode { get; } = (int)HttpStatusCode.InternalServerError; + + /// + /// Gets the status code indicating the exception root cause. + /// + public HttpStatusCode HttpStatusCode => (HttpStatusCode)StatusCode; + + /// + /// Gets the status code indicating the exception root cause. + /// + public int SubStatusCode { get; } + + /// + /// Gets the request information. + /// + public RequestInfo RequestInfo { get; } +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Exceptions/DatabaseRetryableException.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Exceptions/DatabaseRetryableException.cs new file mode 100644 index 0000000000..104498ad7d --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Exceptions/DatabaseRetryableException.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace System.Cloud.DocumentDb; + +/// +/// Exception represent the operation is failed w/ a specific reason and it's eligible to retry in future. +/// +/// +/// Http code 429, 503, 408. +/// Covered codes may vary on specific engine requirements. +/// +public class DatabaseRetryableException : DatabaseException +{ + /// + /// Initializes a new instance of the class. + /// + public DatabaseRetryableException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + public DatabaseRetryableException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// Exception related to the missing data. + public DatabaseRetryableException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// Exception related to the missing data. + /// Exception status code. + /// Exception sub status code. + /// Retry after timespan. + /// The request. + public DatabaseRetryableException( + string message, + Exception innerException, + int statusCode, + int subStatusCode, + TimeSpan? retryAfter, + RequestInfo requestInfo) + : base(message, innerException, statusCode, subStatusCode, requestInfo) + { + if (retryAfter.HasValue) + { + RetryAfter = retryAfter.Value; + } + } + + /// + /// Gets a value indicate the retry after time. + /// + public TimeSpan RetryAfter { get; } = TimeSpan.Zero; +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Exceptions/DatabaseServerException.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Exceptions/DatabaseServerException.cs new file mode 100644 index 0000000000..20ebdfb603 --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Exceptions/DatabaseServerException.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace System.Cloud.DocumentDb; + +/// +/// Exception represent the operation is failed w/o a specific reason. +/// +/// +/// It might due to some failures on server side. +/// Please ask engineer to investigate on this case and escalate if necessary. +/// Http code 500. +/// +public class DatabaseServerException : DatabaseException +{ + /// + /// Initializes a new instance of the class. + /// + public DatabaseServerException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + public DatabaseServerException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// Exception related to the missing data. + public DatabaseServerException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// Exception related to the missing data. + /// Exception status code. + /// Exception sub status code. + /// The request info. + public DatabaseServerException(string message, Exception innerException, int statusCode, int subStatusCode, RequestInfo requestInfo) + : base(message, innerException, statusCode, subStatusCode, requestInfo) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// Exception status code. + /// Exception sub status code. + /// The request info. + public DatabaseServerException(string message, int statusCode, int subStatusCode, RequestInfo requestInfo) + : base(message, statusCode, subStatusCode, requestInfo) + { + } +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/IDocumentDatabase.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/IDocumentDatabase.cs new file mode 100644 index 0000000000..4ad2bb41e8 --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/IDocumentDatabase.cs @@ -0,0 +1,165 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; + +namespace System.Cloud.DocumentDb; + +/// +/// An interface for managing a document database. +/// +/// +/// It plays a role of database, table and connection management. +/// Also it allows constructing readers and writers for tables. +/// +public interface IDocumentDatabase +{ + /// + /// Gets a document reader for a table and a document type provided. + /// + /// The table options. + /// The document reader. + /// + /// The document entity type to be used as a table schema. + /// Operation results of request will be mapped to instance of this type. + /// + /// Thrown when an error occurred on a client side. + /// For example on a bad request, permissions error or client timeout. + IDocumentReader GetDocumentReader(TableOptions options) + where TDocument : notnull; + + /// + /// Gets a document writer for a table and a document type provided. + /// + /// The table options. + /// The document writer. + /// + /// The document entity type to be used as a table schema. + /// Operation results of request will be mapped to instance of this type. + /// + /// Thrown when an error occurred on a client side. + /// For example on a bad request, permissions error or client timeout. + IDocumentWriter GetDocumentWriter(TableOptions options) + where TDocument : notnull; + + /// + /// Initializes connections and optionally creates database if not exists. + /// + /// Specifies whether database should be created if not exists. + /// The cancelation token. + /// A representing the result of the asynchronous operation. + Task ConnectAsync(bool createIfNotExists, CancellationToken cancellationToken); + + /// + /// Deletes database this instance is responsible for. + /// + /// The cancellation token. + /// + /// A containing a with + /// value if successfully deleted and otherwise. + /// + /// Thrown when an error occurred on a client side. + /// For example on a bad request, permissions error or client timeout. + /// Thrown when an error occurred on a database server side, + /// including internal server error. + /// Thrown when a request failed but can be retried. + /// This includes throttling and server not available cases. + /// A generic exception thrown in all other not covered above cases. + Task> DeleteDatabaseAsync(CancellationToken cancellationToken); + + /// + /// Reads provided table settings. + /// + /// The table options with and region information provided. + /// The request options. + /// The token represents request cancellation. + /// A containing a which wraps the table information. + /// Thrown when an error occurred on a client side. + /// For example on a bad request, permissions error or client timeout. + /// Thrown when an error occurred on a database server side, + /// including internal server error. + /// Thrown when a request failed but can be retried. + /// This includes throttling and server not available cases. + /// A generic exception thrown in all other not covered above cases. + Task> ReadTableSettingsAsync( + TableOptions tableOptions, + RequestOptions requestOptions, + CancellationToken cancellationToken); + + /// + /// Updates existing table settings. + /// + /// The table options with and region information provided. + /// The request options. + /// The token represents request cancellation. + /// + /// A containing a which wraps the asynchronous operation result. + /// The result is when the throughput replaced successfully. + /// The indicating the operation is pending. + /// + /// Thrown when an error occurred on a client side. + /// For example on a bad request, permissions error or client timeout. + /// Thrown when an error occurred on a database server side, + /// including internal server error. + /// Thrown when a request failed but can be retried. + /// This includes throttling and server not available cases. + /// A generic exception thrown in all other not covered above cases. + /// . + Task> UpdateTableSettingsAsync( + TableOptions tableOptions, + RequestOptions requestOptions, + CancellationToken cancellationToken); + + /// + /// Creates table using provided parameters. + /// + /// The table options. + /// The request options. + /// The token represents request cancellation. + /// A containing a which wraps the table information. + /// Thrown when an error occurred on a client side. + /// For example on a bad request, permissions error or client timeout. + /// Thrown when an error occurred on a database server side, + /// including internal server error. + /// Thrown when a request failed but can be retried. + /// This includes throttling and server not available cases. + /// A generic exception thrown in all other not covered above cases. + Task> CreateTableAsync( + TableOptions tableOptions, + RequestOptions requestOptions, + CancellationToken cancellationToken); + + /// + /// Deletes table using provided parameters. + /// + /// The table options with and region information provided. + /// The request options. + /// The token represents request cancellation. + /// + /// A containing a with + /// value if table successfully deleted and otherwise. + /// + /// Thrown when an error occurred on a client side. + /// For example on a bad request, permissions error or client timeout. + /// Thrown when an error occurred on a database server side, + /// including internal server error. + /// Thrown when a request failed but can be retried. + /// This includes throttling and server not available cases. + /// A generic exception thrown in all other not covered above cases. + Task> DeleteTableAsync( + TableOptions tableOptions, + RequestOptions requestOptions, + CancellationToken cancellationToken); +} + +/// +/// An interface for injecting to a specific context. +/// +/// The context type, indicating injection preferences. +[System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S4023:Interfaces should not be empty", + Justification = "It is designed for adding an indicator type only, not members.")] +public interface IDocumentDatabase : IDocumentDatabase + where TContext : class +{ +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/IDocumentReader.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/IDocumentReader.cs new file mode 100644 index 0000000000..1cae9ab65b --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/IDocumentReader.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Cloud.DocumentDb; + +/// +/// An interface to clients for all document oriented (or NoSQL) databases for document read operations. +/// https://en.wikipedia.org/wiki/Document-oriented_database. +/// +/// +/// The document entity type to be used as a table schema. +/// Request results will be mapped to instance of this type. +/// +public interface IDocumentReader + where TDocument : notnull +{ + /// + /// Reads a document. + /// + /// The request options. + /// The document id requested to read. + /// The token represents request cancellation. + /// A containing a which wraps the result document. + /// Thrown when an error occurred on a client side. + /// For example on a bad request, permissions error or client timeout. + /// Thrown when an error occurred on a database server side, + /// including internal server error. + /// Thrown when a request failed but can be retried. + /// This includes throttling and server not available cases. + /// A generic exception thrown in all other not covered above cases. + Task> ReadDocumentAsync( + RequestOptions requestOptions, + string id, + CancellationToken cancellationToken); + + /// + /// Fetches a collection of documents. + /// + /// The request options. + /// The fetch condition function. + /// The token represents request cancellation. + /// A containing a which wraps enumerable of fetched documents. + /// Thrown when an error occurred on a client side. + /// For example on a bad request, permissions error or client timeout. + /// Thrown when an error occurred on a database server side, + /// including internal server error. + /// Thrown when a request failed but can be retried. + /// This includes throttling and server not available cases. + /// A generic exception thrown in all other not covered above cases. + /// The type used to map results from after condition is applied. + Task>> FetchDocumentsAsync( + QueryRequestOptions options, + Func, IQueryable>? condition, + CancellationToken cancellationToken) + where TOutputDocument : notnull; + + /// + /// Fetches a collection of documents using a custom query provided. + /// + /// The query request options. + /// The query to fetch items. + /// The token represents request cancellation. + /// A containing a which wraps enumerable of fetched documents. + /// Thrown when an error occurred on a client side. + /// For example on a bad request, permissions error or client timeout. + /// Thrown when an error occurred on a database server side, + /// including internal server error. + /// Thrown when a request failed but can be retried. + /// This includes throttling and server not available cases. + /// A generic exception thrown in all other not covered above cases. + Task>> QueryDocumentsAsync( + QueryRequestOptions options, + Query query, + CancellationToken cancellationToken); + + /// + /// Counts documents which satisfy a query conditions in a table. + /// + /// The query request options. + /// The condition function. + /// The token represents request cancellation. + /// A containing a count of documents. + /// Thrown when an error occurred on a client side. + /// For example on a bad request, permissions error or client timeout. + /// Thrown when an error occurred on a database server side, + /// including internal server error. + /// Thrown when a request failed but can be retried. + /// This includes throttling and server not available cases. + /// A generic exception thrown in all other not covered above cases. + Task> CountDocumentsAsync( + QueryRequestOptions options, + Func, IQueryable>? condition, + CancellationToken cancellationToken); +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/IDocumentWriter.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/IDocumentWriter.cs new file mode 100644 index 0000000000..e482ec9226 --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/IDocumentWriter.cs @@ -0,0 +1,180 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Cloud.DocumentDb; + +/// +/// An interface to clients for all document oriented (or NoSQL) databases for document write operations. +/// https://en.wikipedia.org/wiki/Document-oriented_database. +/// +/// +/// The document entity type to be used as a table schema. +/// Request results will be mapped to instance of this type. +/// +public interface IDocumentWriter + where TDocument : notnull +{ + /// + /// Patches a document. + /// + /// The request options. + /// The document id requested to patched. + /// The patch operations to be applied. + /// The predicate filter to be checked before patch applied. + /// The token represents request cancellation. + /// A containing a which wraps the result document. + /// Thrown when an error occurred on a client side. + /// For example on a bad request, permissions error or client timeout. + /// Thrown when an error occurred on a database server side, + /// including internal server error. + /// Thrown when a request failed but can be retried. + /// This includes throttling and server not available cases. + /// A generic exception thrown in all other not covered above cases. + Task> PatchDocumentAsync( + RequestOptions options, + string id, + IReadOnlyList patchOperations, + string? filter, + CancellationToken cancellationToken); + + /// + /// Creates a document provided at . + /// + /// + /// The request will fail if an item already exists. + /// + /// The request options. + /// The token represents request cancellation. + /// A containing a which wraps the created document. + /// Thrown when an error occurred on a client side. + /// For example on a bad request, permissions error or client timeout. + /// Thrown when an error occurred on a database server side, + /// including internal server error. + /// Thrown when a request failed but can be retried. + /// This includes throttling and server not available cases. + /// A generic exception thrown in all other not covered above cases. + Task> CreateDocumentAsync( + RequestOptions options, + CancellationToken cancellationToken); + + /// + /// Replaces a document having provided id with . + /// + /// + /// The request will fail if a document having the provided id does not exist. + /// If the id in the document different from the id provided in , + /// the id will be updated too. After the operation succeed there will be no document with the old id anymore. + /// + /// The request options. + /// Id of the document to replace. + /// The token represents request cancellation. + /// A containing a which wraps the updated document. + /// Thrown when an error occurred on a client side. + /// For example on a bad request, permissions error or client timeout. + /// Thrown when an error occurred on a database server side, + /// including internal server error. + /// Thrown when a request failed but can be retried. + /// This includes throttling and server not available cases. + /// A generic exception thrown in all other not covered above cases. + Task> ReplaceDocumentAsync( + RequestOptions options, + string id, + CancellationToken cancellationToken); + + /// + /// Tries to insert or update a document with provided document id with value provided in . + /// + /// + /// This function should only be called if existence status of the target item is unknown. + /// This is different from by providing a method for resolving conflicts. + /// If the id in the document different from the id provided, + /// the id will be updated too. After the operation succeed there will be no document with the old id. + /// + /// The request options. + /// The document id. + /// Func used to resolve conflict if there are documents in DB. + /// The token represents request cancellation. + /// A containing a which wraps the updated document. + /// Thrown when an error occurred on a client side. + /// For example on a bad request, permissions error or client timeout. + /// Thrown when an error occurred on a database server side, + /// including internal server error. + /// Thrown when a request failed but can be retried. + /// This includes throttling and server not available cases. + /// A generic exception thrown in all other not covered above cases. + Task> InsertOrUpdateDocumentAsync( + RequestOptions options, + string id, + Func conflictResolveFunc, + CancellationToken cancellationToken); + + /// + /// Upserts a document provided in . + /// + /// + /// This method is suitable when existence of a document is unknown, and replace is always suitable. + /// For conditional replace should be used instead. + /// + /// The request options. + /// The token represents request cancellation. + /// A containing a which wraps the updated document. + /// Thrown when an error occurred on a client side. + /// For example on a bad request, permissions error or client timeout. + /// Thrown when an error occurred on a database server side, + /// including internal server error. + /// Thrown when a request failed but can be retried. + /// This includes throttling and server not available cases. + /// A generic exception thrown in all other not covered above cases. + Task> UpsertDocumentAsync( + RequestOptions options, + CancellationToken cancellationToken); + + /// + /// Deletes a document associated with the id. + /// + /// The request options. + /// The id of the document to be deleted. + /// The token represents request cancellation. + /// A containing a which wraps the asynchronous operation result. + /// Result of the operation is true when deletion succeed, false if failed. + /// Thrown when an error occurred on a client side. + /// For example on a bad request, permissions error or client timeout. + /// Thrown when an error occurred on a database server side, + /// including internal server error. + /// Thrown when a request failed but can be retried. + /// This includes throttling and server not available cases. + /// A generic exception thrown in all other not covered above cases. + Task> DeleteDocumentAsync( + RequestOptions options, + string id, + CancellationToken cancellationToken); + + /// + /// Transactionally executes a set of operations. + /// + /// + /// Transactional batch describes a group of point operations that + /// need to either succeed or fail. If all operations, in the order that are described in the transactional batch, + /// succeed, the transaction is committed. If any operation fails, the entire transaction is rolled back. + /// + /// The request options. + /// to perform transaction batch. + /// The token represents request cancellation. + /// A containing a of which wraps transaction operation response. + /// Thrown when an error occurred on a client side. + /// For example on a bad request, permissions error or client timeout. + /// Thrown when an error occurred on a database server side, + /// including internal server error. + /// Thrown when a request failed but can be retried. + /// This includes throttling and server not available cases. + /// A generic exception thrown in all other not covered above cases. + Task>>> ExecuteTransactionalBatchAsync( + RequestOptions requestOptions, + IReadOnlyList> itemsToPerformTransactionalBatch, + CancellationToken cancellationToken); +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/BatchItem.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/BatchItem.cs new file mode 100644 index 0000000000..e0d629b2b5 --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/BatchItem.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Cloud.DocumentDb; + +/// +/// Batch operation item to be used in transactional operations like . +/// +/// The type of the item the response contains. +[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", + Justification = "Not to be used as a key in key value structures.")] +public readonly struct BatchItem +{ + /// + /// Initializes a new instance of the struct. + /// + /// The operation. + /// The document. + /// The document id. + /// The item version. + public BatchItem( + BatchOperation operation, + T? item = default, + string? id = null, + string? itemVersion = null) + { + Operation = operation; + Item = item; + Id = id; + ItemVersion = itemVersion; + } + + /// + /// Gets the operation for this item. + /// + public BatchOperation Operation { get; } + + /// + /// Gets the batch item payload. + /// + public T? Item { get; } + + /// + /// Gets the item id required for operation. + /// + public string? Id { get; } + + /// + /// Gets the item version for if match condition. + /// + /// + /// For HTTP based protocols it could be based on ETag property. + /// It can be obtained from + /// by doing operation providing item as result. + /// + public string? ItemVersion { get; } +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/BatchOperation.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/BatchOperation.cs new file mode 100644 index 0000000000..5714e1e2b8 --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/BatchOperation.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Cloud.DocumentDb; + +/// +/// The operation used in to indicate the action to perform. +/// +public enum BatchOperation +{ + /// + /// Create item. + /// + Create, + + /// + /// Read item. + /// + Read, + + /// + /// Replace item. + /// + Replace, + + /// + /// Delete item. + /// + Delete, + + /// + /// Upsert item. + /// + Upsert, +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/ConsistencyLevel.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/ConsistencyLevel.cs new file mode 100644 index 0000000000..f96f2517cf --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/ConsistencyLevel.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Cloud.DocumentDb; + +/// +/// Define possible consistency levels. +/// +/// +/// Supported values may vary for different APIs and Engines. +/// If requested level is not supported by an API, the API should throw +/// indicating supported options. +/// +public enum ConsistencyLevel +{ + /// + /// Defines a Strong Consistency for operation. + /// + /// + /// Strong Consistency guarantees that read operations always return the value that was last written. + /// + Strong = 0, + + /// + /// Defines a Bounded Staleness Consistency for operation. + /// + /// + /// Bounded Staleness guarantees that reads are not too out-of-date. + /// + BoundedStaleness = 1, + + /// + /// Defines a Session Consistency for operation. + /// + /// + /// Session Consistency guarantees monotonic reads, all reads and writes + /// in a scope of session executed in the order they came. + /// If a session is specified, reads never gets an old data. + /// + Session = 2, + + /// + /// Defines a Eventual Consistency for operation. + /// + /// + /// Eventual Consistency guarantees if no new updates are made to a given data item, + /// eventually all accesses to that item will return the last updated value. + /// + Eventual = 3, + + /// + /// Defines a Consistent Prefix Consistency for operation. + /// + /// + /// Consistent Prefix Consistency guarantees that reads will return some prefix of + /// all writes with no gaps. All writes will be eventually be available for reads. + /// + ConsistentPrefix = 4 +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/DatabaseOptions.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/DatabaseOptions.cs new file mode 100644 index 0000000000..89cc438759 --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/DatabaseOptions.cs @@ -0,0 +1,112 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using Microsoft.Shared.Data.Validation; + +namespace System.Cloud.DocumentDb; + +/// +/// The class representing configurations for database. +/// +public class DatabaseOptions +{ + /// + /// Gets or sets the global database name. + /// + /// + /// Default is . + /// The value is required. + /// + [Required] + public string DatabaseName { get; set; } = string.Empty; + + /// + /// Gets or sets default name for a database in regions. + /// + /// + /// Default is . + /// This name can be override by specific region config . + /// The value is required if regional name has not overridden. + /// + public string? DefaultRegionalDatabaseName { get; set; } + + /// + /// Gets or sets the key to the account or resource token. + /// + /// + /// Default is . + /// + public string? PrimaryKey { get; set; } + + /// + /// Gets or sets the global database endpoint uri. + /// + /// + /// Default is . + /// The value is required. + /// + [Required] + public Uri? Endpoint { get; set; } + + /// + /// Gets or sets timeout before unused connection will be closed. + /// + /// + /// Default is . + /// By default, idle connections should be kept open indefinitely. + /// Value must be greater than or equal to 10 minutes. + /// Recommended values are between 20 minutes and 24 hours. + /// Mainly useful for sparse infrequent access to a large database account. + /// Works for all global and regional connections. + /// + [TimeSpan("00:10:00", "30.00:00:00")] + public TimeSpan? IdleTcpConnectionTimeout { get; set; } + + /// + /// Gets or sets the throughput value. + /// + /// + /// The default is . + /// The throughput is in database defined units. + /// e.g. Cosmos DB throughput measured in RUs (Request Units) per second: + /// Azure Cosmos DB service quotas. + /// + public Throughput Throughput { get; set; } = Throughput.Unlimited; + + /// + /// Gets or sets json serializer options. + /// + /// + /// Default is the default . + /// Those options will be used by compatible APIs to serialize input before sending to server and deserialize output. + /// This includes sent/received documents. + /// + public JsonSerializerOptions JsonSerializerOptions { get; set; } = new(); + + /// + /// Gets or sets a list of preferred regions used for SDK to define failover order for global database. + /// + /// + /// Default set to empty . + /// + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", + Justification = "Options pattern.")] + public IList FailoverRegions { get; set; } + = new List(); + + /// + /// Gets or sets a list of region specific configurations for the database. + /// + /// + /// Default set to empty . + /// + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", + Justification = "Options pattern.")] + public IDictionary RegionalDatabaseOptions { get; set; } + = new Dictionary(); +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/FetchMode.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/FetchMode.cs new file mode 100644 index 0000000000..3fc7286fdf --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/FetchMode.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Cloud.DocumentDb; + +/// +/// Fetch condition of query. +/// +public enum FetchMode +{ + /// + /// Indicating whether we should fetch all documents. + /// + FetchAll, + + /// + /// Indicating whether we should fetch only one page of results. + /// + /// + /// Page represents a physical group of data located on specific machine. + /// The page should represent a partition, + /// in a case of cross partition fetch each call will return data of a single partition. + /// If a database implementation allows to distribute a partition data across servers, + /// this page can be a subset of partition. + /// + FetchSinglePage, + + /// + /// Indicating whether we should ensure fetching at least the number of max item count. + /// + /// + /// This parameter should only being served on cross partition query. + /// For instance, if you set the to 50. + /// On in partition query, it will return you exactly 50 items if there is that much. + /// But for cross partition query, it might return you only 30 items on a single fetch. + /// In a case of only 30 items will be returned with a continuation token to + /// let you fetch forward. + /// In a case of , another round of single fetch query will be requested with same + /// , which means 80 items at maximum can be returned. + /// + FetchMaxResults, +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/IDatabaseResponse.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/IDatabaseResponse.cs new file mode 100644 index 0000000000..ea941582fb --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/IDatabaseResponse.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; + +namespace System.Cloud.DocumentDb; + +/// +/// The result interface for document storage responses. +/// +public interface IDatabaseResponse +{ + /// + /// Gets the response status code. + /// + /// + /// For databases using HTTP protocol the value would be the of response. + /// + int Status { get; } + + /// + /// Gets the request information. + /// + RequestInfo RequestInfo { get; } + + /// + /// Gets the item version string. + /// + /// + /// For HTTP based protocols it could be based on ETag property. + /// If provided this version can be used in update APIs for consistency control. + /// + string? ItemVersion { get; } + + /// + /// Gets a value indicating whether an operation succeeded. + /// + bool Succeeded { get; } + + /// + /// Gets a value indicate the start point of next batch read. + /// + string? ContinuationToken { get; } + + /// + /// Gets count of items in result. + /// + /// + /// If hold a list, this property returns the number of items in the list. Otherwise it returns 1. + /// This should be used when type is unknown in a context, + /// and count only needed for telemetry or logging. + /// + int ItemCount { get; } +} + +/// +/// The result interface including item for document storage responses. +/// +/// The type of the item the response contains. +public interface IDatabaseResponse : IDatabaseResponse + where T : notnull +{ + /// + /// Gets response item. + /// + T? Item { get; } +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/ITableLocator.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/ITableLocator.cs new file mode 100644 index 0000000000..f716c377e7 --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/ITableLocator.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Cloud.DocumentDb; + +/// +/// The interface provides user a way to adjust table parameters based on a specific document. +/// +/// +/// This may be useful if table settings such as a name or a region differs on a data provided. +/// For example specific tenants data should be isolated from other, or encrypted differently or live in other region. +/// This can be done on user side, however if user has a lot places to access, it is troublesome and error prone to be done in all places. +/// Instead a customer can delegate the logic to adapter, to be applied every time a client requested. +/// +public interface ITableLocator +{ + /// + /// Provides user a way to adjust table and request parameters for specific request. + /// + /// The original table options. + /// The target request. + /// + /// This method will be called only in cases is set to true. + /// The input table options should not be modified, those are original options used to initialize reader / writer. + /// The method can adjust table name, region in request or other options specific to the provided document and/or request. + /// e.g. + /// - specific region may have a different table name, throughput requirements, TTL, etc. + /// - specific document may have a region or table requirement different from original. + /// Notes: + /// - The object is not shared between calls, it can be modified by the method directly. + /// - The is the same provided for the API call. + /// If document is needed to implement locate logic please use for requests. + /// + /// A new table options, or same if no adjustments needed. + TableInfo? LocateTable(in TableInfo options, RequestOptions request); +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/PatchOperation.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/PatchOperation.cs new file mode 100644 index 0000000000..f38a4d1174 --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/PatchOperation.cs @@ -0,0 +1,106 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Cloud.DocumentDb; + +/// +/// Describes patch operation details. +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", + "CA1815:Override equals and operator equals on value types", + Justification = "Not to be used as a key in key value collections.")] +public readonly struct PatchOperation +{ + /// + /// Gets the patch operation type. + /// + public PatchOperationType OperationType { get; } + + /// + /// Gets the patch operation path. + /// + public string Path { get; } + + /// + /// Gets the patch operation value. + /// + public object Value { get; } + + /// + /// Creates patch details for add operation. + /// + /// The type of value to be patched. + /// The path to be patched. + /// The value to be patched. + /// Created patch operation. + public static PatchOperation Add(string path, T value) + where T : notnull + { + return new PatchOperation(PatchOperationType.Add, path, value); + } + + /// + /// Creates patch details for remove operation. + /// + /// The path to be patched. + /// Created patch operation. + public static PatchOperation Remove(string path) + { + return new PatchOperation(PatchOperationType.Remove, path, string.Empty); + } + + /// + /// Creates patch details for replace operation. + /// + /// The type of value to be patched. + /// The path to be patched. + /// The value to be patched. + /// Created patch operation. + public static PatchOperation Replace(string path, T value) + where T : notnull + { + return new PatchOperation(PatchOperationType.Replace, path, value); + } + + /// + /// Creates patch details for set operation. + /// + /// The type of value to be patched. + /// The path to be patched. + /// The value to be patched. + /// Created patch operation. + public static PatchOperation Set(string path, T value) + where T : notnull + { + return new PatchOperation(PatchOperationType.Set, path, value); + } + + /// + /// Creates patch details for increment by long operation. + /// + /// The path to be patched. + /// The long value to be incremented by. + /// Created patch operation. + public static PatchOperation Increment(string path, long value) + { + return new PatchOperation(PatchOperationType.Increment, path, value); + } + + /// + /// Creates patch details for increment by double operation. + /// + /// The path to be patched. + /// The double value to be incremented by. + /// Created patch operation. + public static PatchOperation Increment(string path, double value) + { + return new PatchOperation(PatchOperationType.Increment, path, value); + } + + internal PatchOperation(PatchOperationType type, string path, object value) + { + OperationType = type; + Path = path; + Value = value; + } +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/PatchOperationType.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/PatchOperationType.cs new file mode 100644 index 0000000000..51f84a0f50 --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/PatchOperationType.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Cloud.DocumentDb; + +/// +/// Enum representing patch operation type. +/// +public enum PatchOperationType +{ + /// + /// Represents add operation. + /// + Add, + + /// + /// Represents remove operation. + /// + Remove, + + /// + /// Represents replace operation. + /// + Replace, + + /// + /// Represents set operation. + /// + Set, + + /// + /// Represents increment operation. + /// + Increment +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/Query.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/Query.cs new file mode 100644 index 0000000000..8499d1cf7e --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/Query.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Shared.Collections; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.DocumentDb; + +/// +/// The class representing a query with parameters. +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", + Justification = "Not to be used as a key in key value structs.")] +public readonly struct Query +{ + /// + /// Initializes a new instance of the struct. + /// + /// The query text. + /// The query parameters. + public Query(string queryText, IReadOnlyDictionary parameters) + { + QueryText = Throw.IfNull(queryText); + Parameters = Throw.IfNull(parameters); + } + + /// + /// Initializes a new instance of the struct. + /// + /// The query text. + public Query(string queryText) + : this(queryText, Empty.ReadOnlyDictionary()) + { + } + + /// + /// Gets the query text. + /// + public string QueryText { get; } + + /// + /// Gets the query parameters. + /// + public IReadOnlyDictionary Parameters { get; } +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/QueryRequestOptions.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/QueryRequestOptions.cs new file mode 100644 index 0000000000..1fc1013dbf --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/QueryRequestOptions.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; + +namespace System.Cloud.DocumentDb; + +/// +/// Defines parameters to be used by store engine for queries. +/// +/// +/// The document entity type to be used as a table schema. +/// Request results will be mapped to instance of this type. +/// +public class QueryRequestOptions : RequestOptions + where TDocument : notnull +{ + private const int MaxTokenSize = 10_000; + private const int MaxBufferedCount = 1_000_000; + private const int MaxResultCount = 1_000_000; + private const int MaxConcurency = 1000; + + /// + /// Gets or sets the continuation token size limit. + /// + /// + /// Default is . + /// + [Range(1, MaxTokenSize)] + public int? ResponseContinuationTokenLimitInKb { get; set; } + + /// + /// Gets or sets the option to enable scans on the queries which couldn't be served + /// as indexing was opted out on the requested paths. + /// + /// + /// Default is . + /// Set true to enable and false to disable scan in query. + /// When set to null, client will use database configured or default option. + /// If operation does not support the option, this parameter will be ignored. + /// + public bool? EnableScan { get; set; } + + /// + /// Gets or sets the option to enable low precision order by. + /// + /// + /// Default is . + /// + public bool? EnableLowPrecisionOrderBy { get; set; } + + /// + /// Gets or sets the maximum number of items that can be buffered client side during parallel query execution. + /// + /// + /// Default is . + /// A positive property value limits the number of buffered items to the set value. + /// If this is set to , the system automatically decides the number of items to buffer. + /// This is only suggestive and cannot be abide by in certain cases. + /// + [Range(1, MaxBufferedCount)] + public int? MaxBufferedItemCount { get; set; } + + /// + /// Gets or sets the maximum number of items to be returned. + /// + /// + /// Default is . + /// + [Range(1, MaxResultCount)] + public int? MaxResults { get; set; } + + /// + /// Gets or sets the number of concurrent operations run client side during parallel query execution. + /// + /// + /// Default is . + /// A positive property value limits the number of concurrent operations to the set value. + /// If this is set to , the system automatically decides the number of concurrent operations to run. + /// + [Range(1, MaxConcurency)] + public int? MaxConcurrency { get; set; } + + /// + /// Gets or sets continuation token to continue reading from a breakpoint. + /// + /// + /// Default is and reading would start from the begin. + /// + public string? ContinuationToken { get; set; } + + /// + /// Gets or sets the fetch condition. + /// + /// + /// Default is . + /// This value is indicate the fetch condition of query. + /// + public FetchMode FetchCondition { get; set; } = FetchMode.FetchAll; +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/RegionalDatabaseOptions.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/RegionalDatabaseOptions.cs new file mode 100644 index 0000000000..15b4f67d87 --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/RegionalDatabaseOptions.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; + +namespace System.Cloud.DocumentDb; + +/// +/// The class representing region specific configurations for database. +/// +public class RegionalDatabaseOptions +{ + /// + /// Gets or sets the regional database name. + /// + /// + /// Default is . + /// If the value is not specified will be used. + /// + public string? DatabaseName { get; set; } + + /// + /// Gets or sets the regional database endpoint. + /// + /// + /// The value is required. + /// + [Required] + public Uri? Endpoint { get; set; } + + /// + /// Gets or sets the key to the account or resource token. + /// + /// + /// Default is . + /// + public string? PrimaryKey { get; set; } + + /// + /// Gets or sets a list of preferred regions used for SDK to define failover order for regional database. + /// + /// + /// Default set to empty . + /// + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", + Justification = "Options pattern.")] + public IList FailoverRegions { get; set; } + = new List(); +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/RequestInfo.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/RequestInfo.cs new file mode 100644 index 0000000000..cd10dc3e71 --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/RequestInfo.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace System.Cloud.DocumentDb; + +/// +/// Describes the request information. +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", + Justification = "Not to be compared or used as a key of key value maps.")] +public readonly struct RequestInfo +{ + /// + /// Initializes a new instance of the struct. + /// + /// The request region. + /// The request table name. + /// The request cost. + /// The endpoint used for request. + public RequestInfo(string? region = null, string? tableName = null, double? cost = null, Uri? endpoint = null) + { + Region = region; + TableName = tableName; + Cost = cost; + Endpoint = endpoint; + } + + /// + /// Gets target region, if available. + /// + public string? Region { get; } + + /// + /// Gets target table name, if available. + /// + public string? TableName { get; } + + /// + /// Gets the cost of the request in database defined units if available. + /// + public double? Cost { get; } + + /// + /// Gets the endpoint used for request. + /// + public Uri? Endpoint { get; } +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/RequestOptions.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/RequestOptions.cs new file mode 100644 index 0000000000..48e9223d54 --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/RequestOptions.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace System.Cloud.DocumentDb; + +#pragma warning disable SA1402 // File may only contain a single type + +/// +/// Defines parameters to be used by store engine. +/// +/// +/// Not all parameters supported by all APIs and engines. +/// Not supported parameters will be ignored. +/// +/// +/// The document entity type to be used as a table schema. +/// Operation results from database will be mapped to instance of this type. +/// +public class RequestOptions : RequestOptions + where TDocument : notnull +{ + /// + /// Gets or sets the document value. + /// + public TDocument? Document { get; set; } +} + +/// +/// Defines parameters to be used by store engine. +/// +/// +/// Not all parameters supported by all APIs and engines. +/// Not supported parameters will be ignored. +/// +public class RequestOptions +{ + /// + /// Gets or sets a value indicating whether written object should be returned back after write operations. + /// + /// + /// Indicating whether written object should be returned back after write operations like Create, Upsert, Patch and Replace. + /// Setting the option to false will cause the response to have a null item. + /// This reduces networking and CPU load by not sending the resource back over the network and serializing it on the client. + /// Default is . + /// + public bool ContentResponseOnWrite { get; set; } + + /// + /// Gets or sets the partition key elements for the current request. + /// + /// + /// Default is . + /// + public IReadOnlyList? PartitionKey { get; set; } + + /// + /// Gets or sets the consistency level required for the request. + /// + /// + /// Default is . + /// + public ConsistencyLevel? ConsistencyLevel { get; set; } + + /// + /// Gets or sets the token for use with session consistency. + /// + /// + /// Default is . + /// + public string? SessionToken { get; set; } + + /// + /// Gets or sets the item version parameter to control item version for concurrent modifications. + /// + /// + /// Default is . + /// For HTTP based protocols it could be based on ETag property. + /// It can be obtained from + /// by doing operation providing item as result. + /// + public string? ItemVersion { get; set; } + + /// + /// Gets or sets the region id. + /// + /// + /// Default is . + /// If the region is not set, request will work with global database. + /// Otherwise it should operate with database of specified region. + /// + public string? Region { get; set; } +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/TableInfo.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/TableInfo.cs new file mode 100644 index 0000000000..eaf3a27c3d --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/TableInfo.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.DocumentDb; + +/// +/// The struct representing read only table configurations. +/// +/// +/// Contains similar information as , +/// but can not be extended and modified. +/// It is designed to be used in a hot pass, +/// and having 8x performance comparing to using . +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", + "CA1815:Override equals and operator equals on value types", + Justification = "Not to be compared anywhere.")] +public readonly struct TableInfo +{ + /// + /// Gets the table name. + /// + /// + /// Default is . + /// The value is required. + /// + public string TableName { get; } + + /// + /// Gets the time to live for table items. + /// + /// + /// Default is . + /// If not specified, records will not expire. + /// 1s is the minimum value. + /// + public TimeSpan TimeToLive { get; } + + /// + /// Gets the partition id path for store. + /// + /// + /// Default is . + /// + public string? PartitionIdPath { get; } + + /// + /// Gets a value indicating whether table is regionally replicated or a global. + /// + /// + /// Default is , which means table is global. + /// When enabling regional tables + /// - All required region endpoints should be configured in client. + /// - Requests should contain provided. + /// + public bool IsRegional { get; } + + /// + /// Gets the table throughput value. + /// + /// + /// Default is . + /// . + /// + public Throughput Throughput { get; } + + /// + /// Gets a value indicating whether a required to be used with this table. + /// + /// + /// Default is , which means locator will not be used even if configured. + /// If locator is required, requests will require provided to API to provide . + /// This is the protection mechanism to avoid engineers not designed specific table to forget provide documents when table locator is in use. + /// + public bool IsLocatorRequired { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// The table options. + public TableInfo(TableOptions options) + { + options = Throw.IfNull(options); + + TableName = options.TableName; + TimeToLive = options.TimeToLive; + PartitionIdPath = options.PartitionIdPath; + IsRegional = options.IsRegional; + Throughput = options.Throughput; + IsLocatorRequired = options.IsLocatorRequired; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The source table info. + /// The table name. + /// Is the table regional. + public TableInfo(in TableInfo info, string? tableNameOverride = null, bool? isRegionalOverride = null) + { + TableName = tableNameOverride ?? info.TableName; + TimeToLive = info.TimeToLive; + PartitionIdPath = info.PartitionIdPath; + IsRegional = isRegionalOverride ?? info.IsRegional; + Throughput = info.Throughput; + IsLocatorRequired = info.IsLocatorRequired; + } +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/TableOptions.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/TableOptions.cs new file mode 100644 index 0000000000..0fe8280410 --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/TableOptions.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading; +using Microsoft.Shared.Data.Validation; + +namespace System.Cloud.DocumentDb; + +/// +/// The class representing table configurations. +/// +public class TableOptions +{ + /// + /// Gets or sets the table name. + /// + /// + /// Default is . + /// The value is required. + /// + [Required] + public string TableName { get; set; } = string.Empty; + + /// + /// Gets or sets the time to live for table items. + /// + /// + /// Default is . + /// If not specified, records will not expire. + /// 1s is the minimum value. + /// + [TimeSpan(1000)] + public TimeSpan TimeToLive { get; set; } = Timeout.InfiniteTimeSpan; + + /// + /// Gets or sets the partition id path for store. + /// + /// + /// Default is . + /// + public string? PartitionIdPath { get; set; } + + /// + /// Gets or sets a value indicating whether table is regionally replicated or a global. + /// + /// + /// Default is , which means table is global. + /// When enabling regional tables + /// - All required region endpoints should be configured in client. + /// - Requests should contain provided. + /// + public bool IsRegional { get; set; } + + /// + /// Gets or sets the table throughput value. + /// + /// + /// Default is . + /// . + /// + public Throughput Throughput { get; set; } = Throughput.Unlimited; + + /// + /// Gets or sets a value indicating whether a required to be used with this table. + /// + /// + /// Default is , which means locator will not be used even if configured. + /// If locator is required, requests will require provided to API to provide . + /// This is the protection mechanism to avoid engineers not designed specific table to forget provide documents when table locator is in use. + /// + public bool IsLocatorRequired { get; set; } +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/Throughput.cs b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/Throughput.cs new file mode 100644 index 0000000000..72ea6ecda2 --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/Model/Throughput.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Cloud.DocumentDb; + +/// +/// The structure to define throughput. +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", + Justification = "The struct should not be used as a hash key.")] +public readonly struct Throughput +{ + /// + /// The constant for unlimited throughput. + /// + public static readonly Throughput Unlimited = new(null); + + /// + /// Gets throughput value. + /// + /// + /// The throughput is in database defined units. + /// e.g. Cosmos DB throughput measured in RUs (Request Units) per second: + /// Azure Cosmos DB service quotas. + /// + public int? Value { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// The throughput. + /// + /// See for more details. + /// + public Throughput(int? throughput) + { + Value = throughput; + } +} diff --git a/src/Libraries/System.Cloud.DocumentDb.Abstractions/System.Cloud.DocumentDb.Abstractions.csproj b/src/Libraries/System.Cloud.DocumentDb.Abstractions/System.Cloud.DocumentDb.Abstractions.csproj new file mode 100644 index 0000000000..760deddf9b --- /dev/null +++ b/src/Libraries/System.Cloud.DocumentDb.Abstractions/System.Cloud.DocumentDb.Abstractions.csproj @@ -0,0 +1,29 @@ + + + System.Cloud.DocumentDb + Contracts for document-oriented database clients. + $(PackageTags);Cloud + Fundamentals + + + + true + true + true + + + + dev + 94 + 100 + + + + + + + + + + + diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageCancelledTokenFeatureExtensions.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageCancelledTokenFeatureExtensions.cs new file mode 100644 index 0000000000..2dc6faad12 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageCancelledTokenFeatureExtensions.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging; + +/// +/// Extension methods for for setting/retrieving . +/// +public static class MessageCancelledTokenFeatureExtensions +{ + /// + /// Sets the and the corresponding in the . + /// + /// . + /// . + /// If any of the parameters is null. + public static void SetMessageCancelledTokenSource(this MessageContext context, CancellationTokenSource cancellationTokenSource) + { + _ = Throw.IfNull(context); + _ = Throw.IfNull(context.Features); + _ = Throw.IfNull(cancellationTokenSource); + + context.Features.Set(cancellationTokenSource); + context.MessageCancelledToken = cancellationTokenSource.Token; + } + + /// + /// Try to get the for the . + /// + /// . + /// The to store the representing the message payload. + /// and if , a corresponding . + /// If any of the parameters is null. + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Handled by TryGet pattern.")] + public static bool TryGetMessageCancelledTokenSource(this MessageContext context, out CancellationTokenSource? cancellationTokenSource) + { + _ = Throw.IfNull(context); + _ = Throw.IfNull(context.Features); + + try + { + cancellationTokenSource = context.Features.Get(); + return cancellationTokenSource != null; + } + catch (Exception) + { + cancellationTokenSource = null; + return false; + } + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageCompleteActionFeatureExtensions.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageCompleteActionFeatureExtensions.cs new file mode 100644 index 0000000000..d53c793bf3 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageCompleteActionFeatureExtensions.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging; + +/// +/// Extension methods for for . +/// +public static class MessageCompleteActionFeatureExtensions +{ + /// + /// Marks the message processing as complete. + /// + /// + /// Implementation libraries should ensure to set the via + /// typically in their implementations. + /// + /// . + /// . + /// . + /// If any of the parameters is null. + public static ValueTask MarkCompleteAsync(this MessageContext context, CancellationToken cancellationToken) + { + _ = Throw.IfNull(context); + _ = Throw.IfNull(context.Features); + _ = Throw.IfNull(cancellationToken); + + IMessageCompleteActionFeature? feature = context.Features.Get(); + _ = Throw.IfNull(feature); + + return feature.MarkCompleteAsync(cancellationToken); + } + + /// + /// Sets the to the . + /// + /// . + /// . + /// If any of the parameters is null. + public static void SetMessageCompleteActionFeature(this MessageContext context, IMessageCompleteActionFeature messageCompleteActionFeature) + { + _ = Throw.IfNull(context); + _ = Throw.IfNull(context.Features); + _ = Throw.IfNull(messageCompleteActionFeature); + + context.Features.Set(messageCompleteActionFeature); + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageDestinationFeatureExtensions.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageDestinationFeatureExtensions.cs new file mode 100644 index 0000000000..8363f7ed8c --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageDestinationFeatureExtensions.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Cloud.Messaging.Internal; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging; + +/// +/// Extension methods for to be used for . +/// +public static class MessageDestinationFeatureExtensions +{ + /// + /// Sets the in . + /// + /// + /// Implementation libraries should set the features to in their implementations. + /// + /// . + /// . + /// If any of the parameters is null. + public static void SetMessageDestinationFeatures(this MessageContext context, IFeatureCollection destinationFeatures) + { + _ = Throw.IfNull(context); + _ = Throw.IfNull(context.Features); + _ = Throw.IfNull(destinationFeatures); + + context.Features.Set(new MessageDestinationFeatures(destinationFeatures)); + } + + /// + /// Try get the from the . + /// + /// + /// Implementation libraries should set the features via the . + /// + /// . + /// . + /// value indicating whether the source features was obtained from the . + /// If any of the parameters is null. + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Handled by TryGet pattern.")] + public static bool TryGetMessageDestinationFeatures(this MessageContext context, out IFeatureCollection? destinationFeatures) + { + _ = Throw.IfNull(context); + _ = Throw.IfNull(context.Features); + + try + { + destinationFeatures = context.Features.Get()?.Features; + return destinationFeatures != null; + } + catch (Exception) + { + destinationFeatures = null; + return false; + } + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageDestinationPayloadFeatureExtensions.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageDestinationPayloadFeatureExtensions.cs new file mode 100644 index 0000000000..7a43f276f7 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageDestinationPayloadFeatureExtensions.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Cloud.Messaging.Internal; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging; + +/// +/// Extension methods for for writing to a messages. +/// +public static class MessageDestinationPayloadFeatureExtensions +{ + /// + /// Gets the message payload for . + /// + /// . + /// Message payload as . + public static ReadOnlyMemory GetDestinationPayload(this MessageContext context) + { + _ = context.TryGetMessageDestinationFeatures(out IFeatureCollection? destinationFeatures); + _ = Throw.IfNull(destinationFeatures); + + IMessagePayloadFeature? payloadFeature = destinationFeatures.Get(); + _ = Throw.IfNull(payloadFeature); + + return payloadFeature!.Payload; + } + + /// + /// Sets the message payload in the for . + /// + /// . + /// Message payload in of . + public static void SetDestinationPayload(this MessageContext context, ReadOnlyMemory payload) + { + _ = Throw.IfNull(payload); + + _ = context.TryGetMessageDestinationFeatures(out IFeatureCollection? destinationFeatures); + destinationFeatures ??= new FeatureCollection(); + + destinationFeatures.Set(new MessagePayloadFeature(payload)); + context.SetMessageDestinationFeatures(destinationFeatures); + } + + /// + /// Sets the message payload in the for . + /// + /// . + /// Message payload in array. + /// If any of the parameters is null. + public static void SetDestinationPayload(this MessageContext context, byte[] payload) + { + _ = Throw.IfNull(payload); + + _ = context.TryGetMessageDestinationFeatures(out IFeatureCollection? destinationFeatures); + destinationFeatures ??= new FeatureCollection(); + + destinationFeatures.Set(new MessagePayloadFeature(payload)); + context.SetMessageDestinationFeatures(destinationFeatures); + } + + /// + /// Try to get the message payload for as of . + /// + /// . + /// The to store the representing the message payload. + /// and if , a corresponding . + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Handled by TryGet pattern.")] + public static bool TryGetDestinationPayload(this MessageContext context, out ReadOnlyMemory? payload) + { + try + { + payload = context.GetDestinationPayload(); + return payload.HasValue; + } + catch (Exception) + { + payload = null; + return false; + } + } + + /// + /// Gets the message payload for as in . + /// + /// + /// Implementation copied from public override unsafe string ToString() method of BinaryData. + /// No special treatment is given to the contents of the data, it is merely decoded as a UTF-8 string. + /// For a JPEG or other binary file format the string will largely be nonsense with many embedded NUL characters, + /// and UTF-8 JSON values will look like their file/network representation, + /// including starting and stopping quotes on a string. + /// + /// . + /// The to store the resultant payload as . + /// Message payload as . + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Try Get Pattern.")] + public static bool TryGetDestinationPayloadAsUTF8String(this MessageContext context, out string utf8StringPayload) + { + ReadOnlyMemory payload = context.GetDestinationPayload(); + try + { + utf8StringPayload = UTF8ConverterUtils.ConvertToUTF8StringUnsafe(payload); + return utf8StringPayload != null; + } + catch (Exception) + { + utf8StringPayload = string.Empty; + return false; + } + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageLatencyContextFeatureExtensions.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageLatencyContextFeatureExtensions.cs new file mode 100644 index 0000000000..33f5e292d9 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageLatencyContextFeatureExtensions.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging; + +/// +/// Extension methods for for setting/retrieving . +/// +public static class MessageLatencyContextFeatureExtensions +{ + /// + /// Sets the in . + /// + /// . + /// . + /// If any of the parameters is null. + public static void SetLatencyContext(this MessageContext context, ILatencyContext latencyContext) + { + _ = Throw.IfNull(context); + _ = Throw.IfNull(context.Features); + _ = Throw.IfNull(latencyContext); + + context.Features.Set(latencyContext); + } + + /// + /// Try get the from the . + /// + /// + /// Application should set the features via the . + /// + /// . + /// . + /// value indicating whether the was obtained from the . + /// If any of the parameters is null. + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Handled by TryGet pattern.")] + public static bool TryGetLatencyContext(this MessageContext context, out ILatencyContext? latencyContext) + { + _ = Throw.IfNull(context); + _ = Throw.IfNull(context.Features); + + try + { + latencyContext = context.Features.Get(); + return latencyContext != null; + } + catch (Exception) + { + latencyContext = null; + return false; + } + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessagePostponeActionFeatureExtensions.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessagePostponeActionFeatureExtensions.cs new file mode 100644 index 0000000000..3adcbd1d94 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessagePostponeActionFeatureExtensions.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging; + +/// +/// Extension methods for for . +/// +public static class MessagePostponeActionFeatureExtensions +{ + /// + /// Postpones the message processing asynchronously. + /// + /// + /// Implementation libraries should ensure to set the via + /// typically in their implementations. + /// + /// . + /// by which message processing is to be delayed. + /// . + /// . + /// If any of the parameters is null. + public static ValueTask PostponeAsync(this MessageContext context, TimeSpan delay, CancellationToken cancellationToken) + { + _ = Throw.IfNull(context); + _ = Throw.IfNull(context.Features); + _ = Throw.IfNull(cancellationToken); + + IMessagePostponeActionFeature? feature = context.Features.Get(); + _ = Throw.IfNull(feature); + + return feature.PostponeAsync(delay, cancellationToken); + } + + /// + /// Sets the to the . + /// + /// . + /// . + /// If any of the parameters is null. + public static void SetMessagePostponeActionFeature(this MessageContext context, IMessagePostponeActionFeature messagePostponeActionFeature) + { + _ = Throw.IfNull(context); + _ = Throw.IfNull(context.Features); + _ = Throw.IfNull(messagePostponeActionFeature); + + context.Features.Set(messagePostponeActionFeature); + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageSourceFeatureExtensions.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageSourceFeatureExtensions.cs new file mode 100644 index 0000000000..78dc3ac8ec --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageSourceFeatureExtensions.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Cloud.Messaging.Internal; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging; + +/// +/// Extension methods for used during for . +/// +public static class MessageSourceFeatureExtensions +{ + /// + /// Sets the in . + /// + /// + /// Implementation libraries should set the features to in their implementations. + /// + /// . + /// . + /// If any of the parameters is null. + public static void SetMessageSourceFeatures(this MessageContext context, IFeatureCollection sourceFeatures) + { + _ = Throw.IfNull(context); + _ = Throw.IfNull(context.Features); + _ = Throw.IfNull(sourceFeatures); + + context.Features.Set(new MessageSourceFeatures(sourceFeatures)); + } + + /// + /// Try get the from the . + /// + /// + /// Implementation libraries should set the features via the . + /// + /// . + /// . + /// value indicating whether the source features was obtained from the . + /// If any of the parameters is null. + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Handled by TryGet pattern.")] + public static bool TryGetMessageSourceFeatures(this MessageContext context, out IFeatureCollection? sourceFeatures) + { + _ = Throw.IfNull(context); + _ = Throw.IfNull(context.Features); + + try + { + sourceFeatures = context.Features.Get()?.Features; + return sourceFeatures != null; + } + catch (Exception) + { + sourceFeatures = null; + return false; + } + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageSourcePayloadFeatureExtensions.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageSourcePayloadFeatureExtensions.cs new file mode 100644 index 0000000000..754902b988 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageSourcePayloadFeatureExtensions.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Cloud.Messaging.Internal; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging; + +/// +/// Extension methods for obtained from for . +/// +public static class MessageSourcePayloadFeatureExtensions +{ + /// + /// Gets the message payload obtained from . + /// + /// . + /// Message payload as . + public static ReadOnlyMemory GetSourcePayload(this MessageContext context) + { + _ = context.TryGetMessageSourceFeatures(out IFeatureCollection? sourceFeatures); + _ = Throw.IfNull(sourceFeatures); + + IMessagePayloadFeature? payloadFeature = sourceFeatures.Get(); + _ = Throw.IfNull(payloadFeature); + + return payloadFeature!.Payload; + } + + /// + /// Sets the message payload in the obtained from . + /// + /// . + /// Message payload in of . + public static void SetSourcePayload(this MessageContext context, ReadOnlyMemory payload) + { + _ = Throw.IfNull(payload); + + _ = context.TryGetMessageSourceFeatures(out IFeatureCollection? sourceFeatures); + sourceFeatures ??= new FeatureCollection(); + + sourceFeatures.Set(new MessagePayloadFeature(payload)); + context.SetMessageSourceFeatures(sourceFeatures); + } + + /// + /// Sets the message payload in the obtained from . + /// + /// . + /// Message payload in array. + /// If any of the parameters is null. + public static void SetSourcePayload(this MessageContext context, byte[] payload) + { + _ = Throw.IfNull(payload); + + _ = context.TryGetMessageSourceFeatures(out IFeatureCollection? sourceFeatures); + sourceFeatures ??= new FeatureCollection(); + + sourceFeatures.Set(new MessagePayloadFeature(payload)); + context.SetMessageSourceFeatures(sourceFeatures); + } + + /// + /// Try to get the message payload obtained from . + /// + /// . + /// The to store the representing the message payload. + /// and if , a corresponding . + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Handled by TryGet pattern.")] + public static bool TryGetSourcePayload(this MessageContext context, out ReadOnlyMemory? payload) + { + try + { + payload = context.GetSourcePayload(); + return payload.HasValue; + } + catch (Exception) + { + payload = null; + return false; + } + } + + /// + /// Gets the message payload obtained from as . + /// + /// + /// Implementation copied from public override unsafe string ToString() method of BinaryData. + /// No special treatment is given to the contents of the data, it is merely decoded as a UTF-8 string. + /// For a JPEG or other binary file format the string will largely be nonsense with many embedded NUL characters, + /// and UTF-8 JSON values will look like their file/network representation, + /// including starting and stopping quotes on a string. + /// + /// . + /// The to store the resultant payload as . + /// Message payload as . + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Try Get Pattern.")] + public static bool TryGetSourcePayloadAsUTF8String(this MessageContext context, out string utf8StringPayload) + { + ReadOnlyMemory payload = context.GetSourcePayload(); + try + { + utf8StringPayload = UTF8ConverterUtils.ConvertToUTF8StringUnsafe(payload); + return utf8StringPayload != null; + } + catch (Exception) + { + utf8StringPayload = string.Empty; + return false; + } + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageVisibilityDelayFeatureExtensions.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageVisibilityDelayFeatureExtensions.cs new file mode 100644 index 0000000000..313429f033 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/MessageVisibilityDelayFeatureExtensions.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Cloud.Messaging.Internal; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging; + +/// +/// Extension methods for for setting and retrieving visibility delay using . +/// +public static class MessageVisibilityDelayFeatureExtensions +{ + /// + /// Sets with the provided in the . + /// + /// . + /// . + /// If any of the arguments is null. + public static void SetVisibilityDelay(this MessageContext context, TimeSpan visibilityDelay) + { + _ = context.TryGetMessageSourceFeatures(out IFeatureCollection? sourceFeatures); + sourceFeatures ??= new FeatureCollection(); + + sourceFeatures.Set(new MessageVisibilityDelayFeature(visibilityDelay)); + context.SetMessageSourceFeatures(sourceFeatures); + } + + /// + /// Try to get in the provided from the . + /// + /// . + /// . + /// and if , a corresponding . + /// If any of the arguments is null. + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Handled by TryGet pattern.")] + public static bool TryGetVisibilityDelay(this MessageContext context, out IMessageVisibilityDelayFeature? visibilityDelay) + { + _ = context.TryGetMessageSourceFeatures(out IFeatureCollection? sourceFeatures); + _ = Throw.IfNull(sourceFeatures); + + try + { + visibilityDelay = sourceFeatures.Get(); + return visibilityDelay != null; + } + catch (Exception) + { + visibilityDelay = null; + return false; + } + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/SerializedMessagePayloadFeatureExtensions.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/SerializedMessagePayloadFeatureExtensions.cs new file mode 100644 index 0000000000..cc247b2099 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/MessageContext/SerializedMessagePayloadFeatureExtensions.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Cloud.Messaging.Internal; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging; + +/// +/// Extension methods for obtained from for . +/// +public static class SerializedMessagePayloadFeatureExtensions +{ + /// + /// Gets the message payload obtained from . + /// + /// + /// Ensure the serialized message payload is set in the pipeline via before calling this method. + /// + /// Type of the serialized message payload. + /// . + /// Message payload as . + public static T GetSerializedPayload(this MessageContext context) + where T : notnull + { + _ = context.TryGetMessageSourceFeatures(out IFeatureCollection? sourceFeatures); + _ = Throw.IfNull(sourceFeatures); + + var feature = sourceFeatures.Get>(); + _ = Throw.IfNull(feature); + + return feature!.Payload; + } + + /// + /// Sets the message payload in the obtained from . + /// + /// Type of the serialized message payload. + /// . + /// Message payload in of . + public static void SetSerializedPayload(this MessageContext context, T payload) + where T : notnull + { + _ = Throw.IfNull(payload); + + _ = context.TryGetMessageSourceFeatures(out IFeatureCollection? sourceFeatures); + sourceFeatures ??= new FeatureCollection(); + + sourceFeatures.Set>(new SerializedMessagePayloadFeature(payload)); + context.SetMessageSourceFeatures(sourceFeatures); + } + + /// + /// Try to get the message payload obtained from . + /// + /// Type of the serialized message. + /// . + /// The to store the representing the message payload. + /// and if , a corresponding . + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Handled by TryGet pattern.")] + public static bool TryGetSerializedPayload(this MessageContext context, out T? payload) + where T : notnull + { + try + { + payload = context.GetSerializedPayload(); + return true; + } + catch (Exception) + { + payload = default; + return false; + } + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Middleware/LatencyRecorderMiddlewareExtensions.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Middleware/LatencyRecorderMiddlewareExtensions.cs new file mode 100644 index 0000000000..08b782b0e9 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Middleware/LatencyRecorderMiddlewareExtensions.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Cloud.Messaging.Internal; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging; + +/// +/// Add extension methods to to create to record latency. +/// +public static class LatencyRecorderMiddlewareExtensions +{ + /// + /// Adds the implementation to register the with for recording latency + /// in the pipeline. + /// + /// . + /// to chain additional calls. + /// If any of the parameters is null. + public static IAsyncProcessingPipelineBuilder AddLatencyContextMiddleware(this IAsyncProcessingPipelineBuilder pipelineBuilder) + { + return AddLatencyContextMiddleware(pipelineBuilder, sp => sp.GetRequiredService(), sp => sp.GetServices()); + } + + /// + /// Adds the implementation to register the with for recording latency + /// in the pipeline. + /// + /// The type of implementation. + /// . + /// The implementation factory. + /// The . + /// to chain additional calls. + /// If any of the parameters is null. + public static IAsyncProcessingPipelineBuilder AddLatencyContextMiddleware(this IAsyncProcessingPipelineBuilder pipelineBuilder, + Func implementationFactory, + Func> exporterFactory) + where T : class, ILatencyContextProvider + { + _ = Throw.IfNull(pipelineBuilder); + _ = Throw.IfNull(implementationFactory); + _ = Throw.IfNull(exporterFactory); + + _ = pipelineBuilder.Services.AddNamedSingleton(pipelineBuilder.PipelineName, + sp => new LatencyContextProviderMiddleware(implementationFactory(sp), exporterFactory(sp))); + return pipelineBuilder; + } + + /// + /// Adds the for recording latency of the underlying pipeline + /// in the associated with . + /// Refer . + /// + /// + /// Ensure to register the OR + /// + /// before calling this method. + /// + /// . + /// Success . + /// Failure . + /// to chain additional calls. + /// If any of the parameters is null. + public static IAsyncProcessingPipelineBuilder AddLatencyRecorderMessageMiddleware(this IAsyncProcessingPipelineBuilder pipelineBuilder, + MeasureToken successMeasureToken, + MeasureToken failureMeasureToken) + { + _ = Throw.IfNull(pipelineBuilder); + _ = Throw.IfNull(successMeasureToken); + _ = Throw.IfNull(failureMeasureToken); + + _ = pipelineBuilder.Services.AddNamedSingleton(pipelineBuilder.PipelineName, sp => new LatencyRecorderMiddleware(successMeasureToken, failureMeasureToken)); + return pipelineBuilder; + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Startup/AsyncProcessingPipelineBuilderExtensions.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Startup/AsyncProcessingPipelineBuilderExtensions.cs new file mode 100644 index 0000000000..a71374f951 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Startup/AsyncProcessingPipelineBuilderExtensions.cs @@ -0,0 +1,303 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Cloud.Messaging.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging; + +/// +/// Extension methods for . +/// +public static class AsyncProcessingPipelineBuilderExtensions +{ + /// + /// Register any singletons required against the . + /// + /// Type of singleton. + /// . + /// to chain additional calls. + /// If any of the parameters is null. + public static IAsyncProcessingPipelineBuilder AddNamedSingleton(this IAsyncProcessingPipelineBuilder pipelineBuilder) + where T : class + { + _ = Throw.IfNull(pipelineBuilder); + + _ = pipelineBuilder.Services.AddNamedSingleton(pipelineBuilder.PipelineName); + return pipelineBuilder; + } + + /// + /// Add any singletons required with the provided . + /// + /// Type of singleton. + /// . + /// Implementation for . + /// to chain additional calls. + /// If any of the parameters is null. + public static IAsyncProcessingPipelineBuilder AddNamedSingleton(this IAsyncProcessingPipelineBuilder pipelineBuilder, + Func implementationFactory) + where T : class + { + _ = Throw.IfNull(pipelineBuilder); + _ = Throw.IfNull(implementationFactory); + + _ = pipelineBuilder.Services.AddNamedSingleton(pipelineBuilder.PipelineName, implementationFactory); + return pipelineBuilder; + } + + /// + /// Add any singletons required with the provided against the . + /// + /// Type of singleton. + /// . + /// The pipeline name. + /// Implementation for . + /// to chain additional calls. + /// If any of the parameters is null. + public static IAsyncProcessingPipelineBuilder AddNamedSingleton(this IAsyncProcessingPipelineBuilder pipelineBuilder, + string pipelineName, + Func implementationFactory) + where T : class + { + _ = Throw.IfNull(pipelineBuilder); + _ = Throw.IfNullOrEmpty(pipelineName); + _ = Throw.IfNull(implementationFactory); + + _ = pipelineBuilder.Services.AddNamedSingleton(pipelineName, implementationFactory); + return pipelineBuilder; + } + + /// + /// Configures with implementation for the . + /// + /// Type of implementation. + /// . + /// to chain additional calls. + /// If any of the parameters is null. + public static IAsyncProcessingPipelineBuilder ConfigureMessageDestination(this IAsyncProcessingPipelineBuilder pipelineBuilder) + where TDestination : class, IMessageDestination + { + _ = Throw.IfNull(pipelineBuilder); + + _ = pipelineBuilder.Services.AddNamedSingleton(pipelineBuilder.PipelineName, sp => sp.GetRequiredService()); + return pipelineBuilder; + } + + /// + /// Configures with implementation for the . + /// + /// Type of implementation. + /// . + /// Implementation for . + /// to chain additional calls. + /// If any of the parameters is null. + public static IAsyncProcessingPipelineBuilder ConfigureMessageDestination(this IAsyncProcessingPipelineBuilder pipelineBuilder, + Func implementationFactory) + where TDestination : class, IMessageDestination + { + _ = Throw.IfNull(pipelineBuilder); + _ = Throw.IfNull(implementationFactory); + + _ = pipelineBuilder.Services.AddNamedSingleton(pipelineBuilder.PipelineName, implementationFactory); + return pipelineBuilder; + } + + /// + /// Configures with implementation for the . + /// + /// Type of implementation. + /// . + /// The pipeline name. + /// Implementation for . + /// to chain additional calls. + /// If any of the parameters is null. + public static IAsyncProcessingPipelineBuilder ConfigureMessageDestination(this IAsyncProcessingPipelineBuilder pipelineBuilder, + string pipelineName, + Func implementationFactory) + where TDestination : class, IMessageDestination + { + _ = Throw.IfNull(pipelineBuilder); + _ = Throw.IfNullOrEmpty(pipelineName); + _ = Throw.IfNull(implementationFactory); + + _ = pipelineBuilder.Services.AddNamedSingleton(pipelineName, implementationFactory); + return pipelineBuilder; + } + + /// + /// Configures with implementation for the . + /// + /// Type of implementation. + /// . + /// to chain additional calls. + /// If any of the parameters is null. + public static IAsyncProcessingPipelineBuilder ConfigureMessageSource(this IAsyncProcessingPipelineBuilder pipelineBuilder) + where TSource : class, IMessageSource + { + _ = Throw.IfNull(pipelineBuilder); + + _ = pipelineBuilder.Services.AddNamedSingleton(pipelineBuilder.PipelineName); + return pipelineBuilder; + } + + /// + /// Configures with implementation for the . + /// + /// Type of implementation. + /// . + /// Implementation for . + /// to chain additional calls. + /// If any of the parameters is null. + public static IAsyncProcessingPipelineBuilder ConfigureMessageSource(this IAsyncProcessingPipelineBuilder pipelineBuilder, + Func implementationFactory) + where TSource : class, IMessageSource + { + _ = Throw.IfNull(pipelineBuilder); + _ = Throw.IfNull(implementationFactory); + + _ = pipelineBuilder.Services.AddNamedSingleton(pipelineBuilder.PipelineName, implementationFactory); + return pipelineBuilder; + } + + /// + /// Adds the with implementation to the pipeline. + /// + /// + /// Ordering of the in the pipeline is determined by the order of the calls to this method. + /// + /// Type of implementation. + /// . + /// to chain additional calls. + /// If any of the parameters is null. + public static IAsyncProcessingPipelineBuilder AddMessageMiddleware(this IAsyncProcessingPipelineBuilder pipelineBuilder) + where TMiddleware : class, IMessageMiddleware + { + _ = Throw.IfNull(pipelineBuilder); + + _ = pipelineBuilder.Services.AddNamedSingleton(pipelineBuilder.PipelineName); + return pipelineBuilder; + } + + /// + /// Adds the with implementation to the pipeline. + /// + /// + /// Ordering of the in the pipeline is determined by the order of the calls to this method. + /// + /// Type of implementation. + /// . + /// Implementation for . + /// to chain additional calls. + /// If any of the parameters is null. + public static IAsyncProcessingPipelineBuilder AddMessageMiddleware(this IAsyncProcessingPipelineBuilder pipelineBuilder, + Func implementationFactory) + where TMiddleware : class, IMessageMiddleware + { + _ = Throw.IfNull(pipelineBuilder); + _ = Throw.IfNull(implementationFactory); + + _ = pipelineBuilder.Services.AddNamedSingleton(pipelineBuilder.PipelineName, implementationFactory); + return pipelineBuilder; + } + + /// + /// Configures the terminal with implementation to the pipeline. + /// + /// + /// Ensure to add the required in the pipeline via: + /// 1. OR + /// 2. + /// before calling this method. + /// + /// Type of implementation. + /// . + /// to chain additional calls. + /// If any of the parameters is null. + public static IAsyncProcessingPipelineBuilder ConfigureTerminalMessageDelegate(this IAsyncProcessingPipelineBuilder pipelineBuilder) + where TDelegate : class, IMessageDelegate + { + _ = Throw.IfNull(pipelineBuilder); + + _ = pipelineBuilder.Services.AddNamedSingleton>(pipelineBuilder.PipelineName, sp => sp => sp.GetRequiredService()); + return pipelineBuilder; + } + + /// + /// Configures the terminal with implementation to the pipeline. + /// + /// + /// Ensure to add the required in the pipeline via: + /// 1. OR + /// 2. + /// before calling this method. + /// + /// Type of implementation. + /// . + /// Implementation for . + /// to chain additional calls. + /// If any of the parameters is null. + public static IAsyncProcessingPipelineBuilder ConfigureTerminalMessageDelegate(this IAsyncProcessingPipelineBuilder pipelineBuilder, + Func implementationFactory) + where TDelegate : class, IMessageDelegate + { + _ = Throw.IfNull(pipelineBuilder); + _ = Throw.IfNull(implementationFactory); + + _ = pipelineBuilder.Services.AddNamedSingleton>(pipelineBuilder.PipelineName, sp => implementationFactory); + return pipelineBuilder; + } + + /// + /// Configures the with implementation. + /// + /// Type of implementation. + /// . + /// to chain additional calls. + /// If any of the parameters is null. + public static IAsyncProcessingPipelineBuilder ConfigureMessageConsumer(this IAsyncProcessingPipelineBuilder pipelineBuilder) + where TConsumer : class, IMessageConsumer + { + _ = Throw.IfNull(pipelineBuilder); + + _ = pipelineBuilder.Services.AddNamedSingleton(pipelineBuilder.PipelineName, sp => sp.GetRequiredService()); + return pipelineBuilder; + } + + /// + /// Configures the with implementation. + /// + /// Type of implementation. + /// . + /// Implementation for . + /// to chain additional calls. + /// If any of the parameters is null. + public static IAsyncProcessingPipelineBuilder ConfigureMessageConsumer(this IAsyncProcessingPipelineBuilder pipelineBuilder, + Func implementationFactory) + where TConsumer : class, IMessageConsumer + { + _ = Throw.IfNull(pipelineBuilder); + _ = Throw.IfNull(implementationFactory); + + _ = pipelineBuilder.Services.AddNamedSingleton(pipelineBuilder.PipelineName, sp => implementationFactory(sp)); + return pipelineBuilder; + } + + /// + /// Configures the previously registered as a . + /// + /// . + /// If any of the parameters is null. + public static void RunConsumerAsBackgroundService(this IAsyncProcessingPipelineBuilder pipelineBuilder) + { + _ = Throw.IfNull(pipelineBuilder); + _ = pipelineBuilder.Services.AddSingleton(serviceProvider => + { + INamedServiceProvider namedMessageConsumerProvider = serviceProvider.GetRequiredService>(); + IMessageConsumer messageConsumer = namedMessageConsumerProvider.GetRequiredService(pipelineBuilder.PipelineName); + return new ConsumerBackgroundService(messageConsumer); + }); + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Startup/IAsyncProcessingPipelineBuilder.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Startup/IAsyncProcessingPipelineBuilder.cs new file mode 100644 index 0000000000..edc410afdd --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Startup/IAsyncProcessingPipelineBuilder.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; + +namespace System.Cloud.Messaging; + +/// +/// Interface to build a and associate for the same. +/// +public interface IAsyncProcessingPipelineBuilder +{ + /// + /// Gets the name of the message pipeline. + /// + public string PipelineName { get; } + + /// + /// Gets the . + /// + public IServiceCollection Services { get; } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Startup/IPipelineDelegateFactory.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Startup/IPipelineDelegateFactory.cs new file mode 100644 index 0000000000..5d2f80779a --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Startup/IPipelineDelegateFactory.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace System.Cloud.Messaging; + +/// +/// Factory interface for obtaining a composable from the registered pipeline of +/// and a terminal types which can act on the messages from . +/// +public interface IPipelineDelegateFactory +{ + /// + /// Creates the given . + /// + /// Name of the pipeline. + /// Function of and . + public IMessageDelegate Create(string pipelineName); +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Startup/ServiceCollectionExtensions.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Startup/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..7bd561eb32 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Extensions/Startup/ServiceCollectionExtensions.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Cloud.Messaging.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging; + +/// +/// Extension methods for . +/// +public static class ServiceCollectionExtensions +{ + /// + /// Configures the delegate factory for . + /// + /// . + /// implementation. + /// The . + /// If any of the parameters is null. + public static IServiceCollection WithPipelineDelegateFactory(this IServiceCollection services, Func factory) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(factory); + + _ = services.AddSingleton(factory); + return services; + } + + /// + /// Create a message processing pipeline with the provided . + /// + /// The . + /// The name of the pipeline. + /// The . + /// If any of the parameters is null. + public static IAsyncProcessingPipelineBuilder AddAsyncPipeline(this IServiceCollection services, string pipelineName) + { + _ = Throw.IfNull(services); + _ = Throw.IfNullOrEmpty(pipelineName); + + services.TryAddSingleton(sp => new PipelineDelegateFactory(sp)); + _ = services.AddNamedSingleton(pipelineName, sp => + { + IPipelineDelegateFactory pipeline = sp.GetRequiredService(); + return pipeline.Create(pipelineName); + }); + + return new AsyncProcessingPipelineBuilder(pipelineName, services); + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessageCompleteActionFeature.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessageCompleteActionFeature.cs new file mode 100644 index 0000000000..a5db00fbb4 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessageCompleteActionFeature.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; + +namespace System.Cloud.Messaging; + +/// +/// Feature interface for marking the message processing as complete. +/// +public interface IMessageCompleteActionFeature +{ + /// + /// Marks the message processing to be completed asynchronously. + /// + /// . + /// . + ValueTask MarkCompleteAsync(CancellationToken cancellationToken); +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessageDestinationFeatures.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessageDestinationFeatures.cs new file mode 100644 index 0000000000..c43af33b9f --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessageDestinationFeatures.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.Features; + +namespace System.Cloud.Messaging; + +/// +/// Interface for features used for writing messages to . +/// +public interface IMessageDestinationFeatures +{ + /// + /// Gets the associated . + /// + public IFeatureCollection Features { get; } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessagePayloadFeature.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessagePayloadFeature.cs new file mode 100644 index 0000000000..d4a1351dad --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessagePayloadFeature.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace System.Cloud.Messaging; + +/// +/// Feature interface for setting/retrieving the message payload. +/// +public interface IMessagePayloadFeature +{ + /// + /// Gets the message payload. + /// + ReadOnlyMemory Payload { get; } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessagePostponeActionFeature.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessagePostponeActionFeature.cs new file mode 100644 index 0000000000..588e760bad --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessagePostponeActionFeature.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Cloud.Messaging; + +/// +/// Feature interface for postponing the message processing. +/// +public interface IMessagePostponeActionFeature +{ + /// + /// Postpones the message processing asynchronously. + /// + /// by which message processing is to be postponed. + /// . + /// . + ValueTask PostponeAsync(TimeSpan delay, CancellationToken cancellationToken); +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessageSourceFeatures.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessageSourceFeatures.cs new file mode 100644 index 0000000000..a98a614336 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessageSourceFeatures.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.Features; + +namespace System.Cloud.Messaging; + +/// +/// Interface for features read from . +/// +public interface IMessageSourceFeatures +{ + /// + /// Gets the associated . + /// + public IFeatureCollection Features { get; } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessageVisibilityDelayFeature.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessageVisibilityDelayFeature.cs new file mode 100644 index 0000000000..5aae29629b --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Features/IMessageVisibilityDelayFeature.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace System.Cloud.Messaging; + +/// +/// Feature interface for setting/retrieving the visibility delay. +/// +public interface IMessageVisibilityDelayFeature +{ + /// + /// Gets the visibility delay. + /// + TimeSpan VisibilityDelay { get; } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Features/ISerializedMessagePayloadFeature.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Features/ISerializedMessagePayloadFeature.cs new file mode 100644 index 0000000000..02fa1fdf78 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Features/ISerializedMessagePayloadFeature.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Cloud.Messaging; + +/// +/// Feature interface for setting/retrieving the serialized message payload. +/// +/// Type of the message payload. +public interface ISerializedMessagePayloadFeature + where T : notnull +{ + /// + /// Gets the message payload. + /// + T Payload { get; } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Implementations/BaseMessageConsumer.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Implementations/BaseMessageConsumer.cs new file mode 100644 index 0000000000..16cdc9e280 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Implementations/BaseMessageConsumer.cs @@ -0,0 +1,184 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Cloud.Messaging.Internal; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging; + +/// +/// Reference base class implementation for . +/// +public class BaseMessageConsumer : IMessageConsumer +{ + /// + /// Gets the underlying . + /// + protected IMessageSource MessageSource { get; } + + /// + /// Gets the . + /// + protected IMessageDelegate MessageDelegate { get; } + + /// + /// Gets the . + /// + protected ILogger Logger { get; } + + /// + /// Initializes a new instance of the class. + /// + /// . + /// . + /// . + protected BaseMessageConsumer(IMessageSource messageSource, IMessageDelegate messageDelegate, ILogger logger) + { + MessageSource = Throw.IfNull(messageSource); + MessageDelegate = Throw.IfNull(messageDelegate); + Logger = Throw.IfNull(logger); + } + + /// + public async virtual ValueTask ExecuteAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await ProcessingStepAsync(cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Handles exception occured during processing message. + /// + /// Default behaviour is to rethrow the exception. + /// . + /// . + /// . + [SuppressMessage("Resilience", "R9A061:The async method doesn't support cancellation", Justification = $"{nameof(MessageContext)} has {nameof(CancellationToken)}")] + protected virtual ValueTask OnMessageProcessingFailureAsync(MessageContext context, Exception exception) + { + ExceptionDispatchInfo.Capture(exception).Throw(); + return default; + } + + /// + /// Handles the message processing completion. + /// + /// . + /// . + [SuppressMessage("Resilience", "R9A061:The async method doesn't support cancellation", Justification = $"{nameof(MessageContext)} has {nameof(CancellationToken)}")] + protected virtual ValueTask OnMessageProcessingCompletionAsync(MessageContext context) => default; + + /// + /// Represents processing steps for message(s). + /// + /// + /// Different implementation of the consumer can override this method and execute the + /// in parallel or in any other way using Task Parallel Library (TPL) / DataFlow or any other abstractions. + /// + /// . + /// . + protected virtual async ValueTask ProcessingStepAsync(CancellationToken cancellationToken) + { + await FetchAndProcessMessageAsync(cancellationToken).ConfigureAwait(false); + } + + /// + /// Process a single message asynchronously. + /// + /// . + /// . + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = $"Handled by {nameof(Logger)}.")] + protected virtual async ValueTask FetchAndProcessMessageAsync(CancellationToken cancellationToken) + { + MessageContext messageContext; + try + { + messageContext = await FetchMessageAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception messageFetchException) + { + Log.MessageSourceFailedDuringReadingMessage(Logger, nameof(MessageSource), messageFetchException); + return; + } + + if (messageContext == null) + { + Log.MessageSourceReturnedNullMessageContext(Logger, nameof(MessageSource)); + return; + } + + try + { + await ProcessMessageAsync(messageContext).ConfigureAwait(false); + + try + { + await OnMessageProcessingCompletionAsync(messageContext).ConfigureAwait(false); + } + catch (Exception handlerException) + { + Log.ExceptionOccuredDuringHandlingMessageProcessingCompletion(Logger, handlerException); + } + } + catch (Exception processingException) + { + try + { + await OnMessageProcessingFailureAsync(messageContext, processingException).ConfigureAwait(false); + } + catch (Exception handlerException) + { + Log.ExceptionOccuredDuringHandlingMessageProcessingFailure(Logger, processingException, handlerException); + throw; + } + } + finally + { + try + { + ReleaseContext(messageContext); + } + catch (Exception releaseException) + { + Log.MessageSourceFailedDuringReleasingContext(Logger, nameof(MessageSource), releaseException); + } + } + } + + /// + /// Process a message asynchronously. + /// + /// . + /// . + [SuppressMessage("Resilience", "R9A061:The async method doesn't support cancellation", Justification = $"{nameof(MessageContext)} has {nameof(CancellationToken)}")] + protected virtual async ValueTask ProcessMessageAsync(MessageContext context) + { + _ = Throw.IfNull(context); + + await MessageDelegate.InvokeAsync(context).ConfigureAwait(false); + } + + /// + /// Fetch message from message source. + /// + /// Cancellation Token. + /// of nullable . + protected virtual async ValueTask FetchMessageAsync(CancellationToken cancellationToken) + { + MessageContext message = await MessageSource.ReadAsync(cancellationToken).ConfigureAwait(false); + return message; + } + + /// + /// Release context. + /// + /// . + protected virtual void ReleaseContext(MessageContext messageContext) => MessageSource.Release(messageContext); +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageConsumer.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageConsumer.cs new file mode 100644 index 0000000000..0e40d8f7c5 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageConsumer.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; + +namespace System.Cloud.Messaging; + +/// +/// Interface for consuming and processing messages. +/// +public interface IMessageConsumer +{ + /// + /// Start processing the messages. + /// + /// to stop processing messages. + /// . + ValueTask ExecuteAsync(CancellationToken cancellationToken); +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageDelegate.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageDelegate.cs new file mode 100644 index 0000000000..c3200a7edd --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageDelegate.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Cloud.Messaging; + +/// +/// The message delegate called by to continue processing the message in the pipeline chain. +/// +/// +/// It is inspired from the next delegate in the ASP.NET Core Middleware pipeline. +/// +[SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Analogous to ASP.NET Core RequestDelegate.")] +public interface IMessageDelegate +{ + /// + /// Handles the message asynchronously. + /// + /// . + /// . + [SuppressMessage("Resilience", "R9A061:The async method doesn't support cancellation", Justification = $"{nameof(MessageContext)} has {nameof(CancellationToken)}")] + public ValueTask InvokeAsync(MessageContext context); +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageDestination.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageDestination.cs new file mode 100644 index 0000000000..3f79daf2bd --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageDestination.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Cloud.Messaging; + +/// +/// Interface for writing message to a destination. +/// +public interface IMessageDestination +{ + /// + /// Write message asynchronously. + /// + /// . + /// . + [SuppressMessage("Resilience", "R9A061:The async method doesn't support cancellation", Justification = $"{nameof(MessageContext)} has {nameof(CancellationToken)}")] + public ValueTask WriteAsync(MessageContext context); +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageMiddleware.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageMiddleware.cs new file mode 100644 index 0000000000..099c8778d8 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageMiddleware.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Cloud.Messaging; + +/// +/// Interface for a middleware which uses and the next in the pipeline to process the message. +/// +/// +/// Inspired from ASP.NET Core Middleware which uses HttpContext and the next RequestDelegate in the pipeline. +/// +public interface IMessageMiddleware +{ + /// + /// Handles the message. + /// + /// . + /// . + /// . + [SuppressMessage("Resilience", "R9A061:The async method doesn't support cancellation", Justification = $"{nameof(MessageContext)} has {nameof(CancellationToken)}")] + ValueTask InvokeAsync(MessageContext context, IMessageDelegate nextHandler); +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageSource.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageSource.cs new file mode 100644 index 0000000000..ea4acfcef3 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/IMessageSource.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; + +namespace System.Cloud.Messaging; + +/// +/// Interface for a message source. +/// +public interface IMessageSource +{ + /// + /// Reads message asynchronously. + /// + /// Cancellation Token. + /// . + ValueTask ReadAsync(CancellationToken cancellationToken); + + /// + /// Release the context. + /// + /// . + void Release(MessageContext context); +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/MessageContext.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/MessageContext.cs new file mode 100644 index 0000000000..506f45c990 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Interfaces/MessageContext.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging; + +/// +/// Represents the context for storing different during processing of message(s). +/// +/// Inspired from ASP.NET Core HttpContext. +public sealed class MessageContext +{ + /// + /// Gets the for the message. + /// + public IFeatureCollection Features { get; } + + /// + /// Gets or sets the for the cancelling the message processing. + /// + public CancellationToken MessageCancelledToken { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// . + /// If any of the arguments is null. + public MessageContext(IFeatureCollection features) + { + Features = Throw.IfNull(features); + MessageCancelledToken = CancellationToken.None; + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Delegate/PipelineMessageDelegateStitcher.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Delegate/PipelineMessageDelegateStitcher.cs new file mode 100644 index 0000000000..c5df2bfa76 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Delegate/PipelineMessageDelegateStitcher.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging.Internal; + +/// +/// Utility class to combine the and the next to create an equivalent . +/// +internal sealed class PipelineMessageDelegateStitcher : IMessageDelegate +{ + private readonly IMessageMiddleware _middleware; + private readonly IMessageDelegate _nextHandler; + + /// + /// Initializes a new instance of the class. + /// + /// . + /// . + /// If any of the parameters is null. + public PipelineMessageDelegateStitcher(IMessageMiddleware middleware, IMessageDelegate nextHandler) + { + _middleware = Throw.IfNull(middleware); + _nextHandler = Throw.IfNull(nextHandler); + } + + /// + public ValueTask InvokeAsync(MessageContext context) + { + return _middleware.InvokeAsync(context, _nextHandler); + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Extensions/MessageMiddlewareExtensions.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Extensions/MessageMiddlewareExtensions.cs new file mode 100644 index 0000000000..5c473c85ce --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Extensions/MessageMiddlewareExtensions.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging.Internal; + +/// +/// Extension methods for . +/// +internal static class MessageMiddlewareExtensions +{ + /// + /// Generate a composable from the and . + /// + /// . + /// . + /// Combined . + /// If any of the parameters is null. + public static IMessageDelegate Stitch(this IMessageMiddleware middleware, IMessageDelegate nextHandler) + { + _ = Throw.IfNull(middleware); + _ = Throw.IfNull(nextHandler); + + return new PipelineMessageDelegateStitcher(middleware, nextHandler); + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/MessageDestinationFeatures.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/MessageDestinationFeatures.cs new file mode 100644 index 0000000000..cc4d1c77f2 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/MessageDestinationFeatures.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.Features; + +namespace System.Cloud.Messaging.Internal; + +/// +/// Implements . +/// +internal sealed class MessageDestinationFeatures : IMessageDestinationFeatures +{ + /// + /// Initializes a new instance of the class. + /// + /// . + public MessageDestinationFeatures(IFeatureCollection features) + { + Features = features; + } + + /// + public IFeatureCollection Features { get; } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/MessagePayloadFeature.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/MessagePayloadFeature.cs new file mode 100644 index 0000000000..426563423e --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/MessagePayloadFeature.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace System.Cloud.Messaging.Internal; + +/// +/// Implements . +/// +internal sealed class MessagePayloadFeature : IMessagePayloadFeature +{ + /// + /// Initializes a new instance of the class. + /// + /// array of payload. + public MessagePayloadFeature(byte[] payload) + { + Payload = payload; + } + + /// + /// Initializes a new instance of the class. + /// + /// Payload. + public MessagePayloadFeature(ReadOnlyMemory payload) + { + Payload = payload; + } + + /// + public ReadOnlyMemory Payload { get; } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/MessageSourceFeatures.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/MessageSourceFeatures.cs new file mode 100644 index 0000000000..797ef46349 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/MessageSourceFeatures.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.Features; + +namespace System.Cloud.Messaging.Internal; + +/// +/// Implements . +/// +internal sealed class MessageSourceFeatures : IMessageSourceFeatures +{ + /// + /// Initializes a new instance of the class. + /// + /// . + public MessageSourceFeatures(IFeatureCollection features) + { + Features = features; + } + + /// + public IFeatureCollection Features { get; } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/MessageVisibilityDelayFeature.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/MessageVisibilityDelayFeature.cs new file mode 100644 index 0000000000..89af0fcd00 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/MessageVisibilityDelayFeature.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace System.Cloud.Messaging.Internal; + +/// +/// Implements . +/// +internal sealed class MessageVisibilityDelayFeature : IMessageVisibilityDelayFeature +{ + /// + /// Initializes a new instance of the class. + /// + /// representing visibility delay. + public MessageVisibilityDelayFeature(TimeSpan visibilityDelay) + { + VisibilityDelay = visibilityDelay; + } + + /// + public TimeSpan VisibilityDelay { get; } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/SerializedMessagePayloadFeature.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/SerializedMessagePayloadFeature.cs new file mode 100644 index 0000000000..873c870753 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Features/SerializedMessagePayloadFeature.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Cloud.Messaging.Internal; + +/// +/// Implements . +/// +internal sealed class SerializedMessagePayloadFeature : ISerializedMessagePayloadFeature + where T : notnull +{ + /// + /// Initializes a new instance of the class. + /// + /// of payload. + public SerializedMessagePayloadFeature(T payload) + { + Payload = payload; + } + + /// + public T Payload { get; } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Middlewares/LatencyContextProviderMiddleware.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Middlewares/LatencyContextProviderMiddleware.cs new file mode 100644 index 0000000000..e10a947ad7 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Middlewares/LatencyContextProviderMiddleware.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Pools; + +namespace System.Cloud.Messaging.Internal; + +/// +/// An implementation to register to record latency for . +/// +internal sealed class LatencyContextProviderMiddleware : IMessageMiddleware +{ + private static readonly ObjectPool> _exporterTaskPool = PoolFactory.CreateListPool(); + + private readonly ILatencyContextProvider _latencyContextProvider; + private readonly ILatencyDataExporter[] _latencyDataExporters; + + /// + /// Initializes a new instance of the class. + /// + /// . + /// The list of exporters for latency data. + public LatencyContextProviderMiddleware(ILatencyContextProvider latencyContextProvider, + IEnumerable latencyDataExporters) + { + _latencyContextProvider = Throw.IfNull(latencyContextProvider); + _latencyDataExporters = Throw.IfNullOrEmpty(latencyDataExporters).ToArray(); + } + + /// + public async ValueTask InvokeAsync(MessageContext context, IMessageDelegate nextHandler) + { + _ = Throw.IfNull(context); + _ = Throw.IfNull(nextHandler); + + ILatencyContext latencyContext = _latencyContextProvider.CreateContext(); + context.SetLatencyContext(latencyContext); + + await nextHandler.InvokeAsync(context).ConfigureAwait(false); + + latencyContext.Freeze(); + await ExportAsync(latencyContext.LatencyData, context.MessageCancelledToken).ConfigureAwait(false); + } + + private async Task ExportAsync(LatencyData latencyData, CancellationToken cancellationToken) + { + List exports = _exporterTaskPool.Get(); + foreach (ILatencyDataExporter latencyDataExporter in _latencyDataExporters) + { + exports.Add(latencyDataExporter.ExportAsync(latencyData, cancellationToken)); + } + + await Task.WhenAll(exports).ConfigureAwait(false); + + _exporterTaskPool.Return(exports); + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Middlewares/LatencyRecorderMiddleware.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Middlewares/LatencyRecorderMiddleware.cs new file mode 100644 index 0000000000..ea286df35a --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Middlewares/LatencyRecorderMiddleware.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging.Internal; + +/// +/// An implementation to record latency for . +/// +internal sealed class LatencyRecorderMiddleware : IMessageMiddleware +{ + internal TimeProvider TimeProvider = TimeProvider.System; + + private const string NotAvailable = "N/A"; + + private readonly MeasureToken _successMeasureToken; + private readonly MeasureToken _failureMeasureToken; + + /// + /// Initializes a new instance of the class. + /// + /// Success . + /// Failed . + public LatencyRecorderMiddleware(MeasureToken successMeasureToken, MeasureToken failureMeasureToken) + { + _successMeasureToken = successMeasureToken; + _failureMeasureToken = failureMeasureToken; + } + + /// + public async ValueTask InvokeAsync(MessageContext context, IMessageDelegate nextHandler) + { + _ = Throw.IfNull(context); + _ = Throw.IfNull(nextHandler); + + _ = context.TryGetLatencyContext(out ILatencyContext? latencyContext); + _ = Throw.IfNull(latencyContext); + + Exception? exception = null; + var timestamp = TimeProvider.GetTimestamp(); + try + { + await nextHandler.InvokeAsync(context).ConfigureAwait(false); + } + catch (Exception e) + { + exception = e; + throw; + } + finally + { + long latency = (long)TimeProvider.GetElapsedTime(timestamp, TimeProvider.GetTimestamp()).TotalMilliseconds; + + if (exception != null) + { + latencyContext.AddMeasure(_failureMeasureToken, latency); + latencyContext.SetTag(new TagToken($"{_failureMeasureToken.Name}_Exception_Message", _failureMeasureToken.Position), exception.Message); + latencyContext.SetTag(new TagToken($"{_failureMeasureToken.Name}_Exception_Class", _failureMeasureToken.Position), exception.GetType()?.FullName ?? NotAvailable); + } + else + { + latencyContext.AddMeasure(_successMeasureToken, latency); + } + } + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Startup/AsyncProcessingPipelineBuilder.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Startup/AsyncProcessingPipelineBuilder.cs new file mode 100644 index 0000000000..1976a24b28 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Startup/AsyncProcessingPipelineBuilder.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging.Internal; + +/// +/// Implementation of . +/// +internal sealed class AsyncProcessingPipelineBuilder : IAsyncProcessingPipelineBuilder +{ + /// + public string PipelineName { get; } + + /// + public IServiceCollection Services { get; } + + /// + /// Initializes a new instance of the class. + /// + public AsyncProcessingPipelineBuilder(string pipelineName, IServiceCollection services) + { + PipelineName = Throw.IfNullOrEmpty(pipelineName); + Services = Throw.IfNull(services); + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Startup/ConsumerBackgroundService.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Startup/ConsumerBackgroundService.cs new file mode 100644 index 0000000000..b4bb3a82d5 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Startup/ConsumerBackgroundService.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging.Internal; + +/// +/// An implementation of which operates on the provided . +/// +internal sealed class ConsumerBackgroundService : BackgroundService +{ + private readonly IMessageConsumer _consumer; + + /// + /// Initializes a new instance of the class. + /// + /// . + /// If any of the parameters is null. + public ConsumerBackgroundService(IMessageConsumer consumer) + { + _consumer = Throw.IfNull(consumer); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await Task.Yield(); + await _consumer.ExecuteAsync(stoppingToken).ConfigureAwait(false); + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Startup/PipelineDelegateFactory.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Startup/PipelineDelegateFactory.cs new file mode 100644 index 0000000000..5691c992fd --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Startup/PipelineDelegateFactory.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Shared.Diagnostics; + +namespace System.Cloud.Messaging.Internal; + +/// +/// Implementation for . +/// +internal sealed class PipelineDelegateFactory : IPipelineDelegateFactory +{ + public const string IncorrectTerminalDelegateConfigurationError = $"Please check the configuration for the injected terminal {nameof(IMessageDelegate)}."; + public const string IncorrectMiddlewaresConfigurationError = $"Please check the configuration for the injected pipeline of {nameof(IMessageMiddleware)}."; + + private readonly IServiceProvider _serviceProvider; + + /// + /// Initializes a new instance of the class. + /// + /// . + public PipelineDelegateFactory(IServiceProvider serviceProvider) + { + _serviceProvider = Throw.IfNull(serviceProvider); + } + + /// + /// If the any of the parameters is null/empty. + public IMessageDelegate Create(string pipelineName) + { + _ = Throw.IfNullOrEmpty(pipelineName); + + var namedMessageDelegateFactory = _serviceProvider.GetRequiredService>>(); + Func messageDelegateFactory = namedMessageDelegateFactory.GetRequiredService(pipelineName); + _ = Throw.IfNull(messageDelegateFactory); + + IMessageDelegate currentMessageDelegate; + try + { + currentMessageDelegate = messageDelegateFactory(_serviceProvider); + } + catch (Exception ex) + { + throw new InvalidOperationException(IncorrectTerminalDelegateConfigurationError, ex); + } + + _ = Throw.IfNull(currentMessageDelegate); + + IEnumerable middlewares; + try + { + var middlewaresProvider = _serviceProvider.GetRequiredService>(); + middlewares = middlewaresProvider.GetServices(pipelineName); + } + catch (Exception ex) + { + throw new InvalidOperationException(IncorrectMiddlewaresConfigurationError, ex); + } + + Stack middlewaresStack = new(middlewares); + while (middlewaresStack.Count > 0) + { + IMessageMiddleware messageMiddleware = middlewaresStack.Pop(); + _ = Throw.IfNull(messageMiddleware); + + currentMessageDelegate = messageMiddleware.Stitch(currentMessageDelegate); + _ = Throw.IfNull(currentMessageDelegate); + } + + return currentMessageDelegate; + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Utilities/Log.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Utilities/Log.cs new file mode 100644 index 0000000000..0f1d8d2618 --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Utilities/Log.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace System.Cloud.Messaging.Internal; + +/// +/// Log utilities. +/// +[SuppressMessage("Major Code Smell", "S109:Magic numbers should not be used", Justification = "Used for EventId.")] +internal static partial class Log +{ + [LogMethod(0, LogLevel.Warning, "{messageSource} failed during reading message.")] + public static partial void MessageSourceFailedDuringReadingMessage(ILogger logger, string messageSource, Exception messageFetchException); + + [LogMethod(1, LogLevel.Warning, "{messageSource} returned null MessageContext.")] + public static partial void MessageSourceReturnedNullMessageContext(ILogger logger, string messageSource); + + [LogMethod(2, LogLevel.Warning, "Handling message procesing completion failed.")] + public static partial void ExceptionOccuredDuringHandlingMessageProcessingCompletion(ILogger logger, Exception handlerException); + + [LogMethod(3, LogLevel.Warning, "Handling message procesing failure failed with {handlerException}.")] + public static partial void ExceptionOccuredDuringHandlingMessageProcessingFailure(ILogger logger, + Exception processingException, + Exception handlerException); + + [LogMethod(4, LogLevel.Warning, "{messageprocessingStateHandler} failed during releasing context.")] + public static partial void MessageSourceFailedDuringReleasingContext(ILogger logger, + string messageprocessingStateHandler, + Exception releaseException); +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Utilities/UTF8ConverterUtils.cs b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Utilities/UTF8ConverterUtils.cs new file mode 100644 index 0000000000..929255793e --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/Internal/Utilities/UTF8ConverterUtils.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Text; + +namespace System.Cloud.Messaging.Internal; + +internal static class UTF8ConverterUtils +{ + /// + /// Converts the of to . + /// + /// + /// Implementation copied from public override unsafe string ToString() method of BinaryData. + /// No special treatment is given to the contents of the data, it is merely decoded as a UTF-8 string. + /// For a JPEG or other binary file format the string will largely be nonsense with many embedded NUL characters, + /// and UTF-8 JSON values will look like their file/network representation, + /// including starting and stopping quotes on a string. + /// + /// of . + /// value. + public static unsafe string ConvertToUTF8StringUnsafe(ReadOnlyMemory payload) + { + ReadOnlySpan payloadSpan = payload.Span; + if (payloadSpan.IsEmpty) + { + return string.Empty; + } + + fixed (byte* ptr = payloadSpan) + { + return Encoding.UTF8.GetString(ptr, payloadSpan.Length); + } + } +} diff --git a/src/Libraries/System.Cloud.Messaging.Abstractions/System.Cloud.Messaging.Abstractions.csproj b/src/Libraries/System.Cloud.Messaging.Abstractions/System.Cloud.Messaging.Abstractions.csproj new file mode 100644 index 0000000000..e3344bbc4f --- /dev/null +++ b/src/Libraries/System.Cloud.Messaging.Abstractions/System.Cloud.Messaging.Abstractions.csproj @@ -0,0 +1,43 @@ + + + System.Cloud.Messaging + Abstractions for async messaging. + $(PackageTags);Cloud + Fundamentals + + + + true + true + true + true + true + + + + dev + 0 + 10 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Packages/Directory.Build.props b/src/Packages/Directory.Build.props new file mode 100644 index 0000000000..ed9aebbf6e --- /dev/null +++ b/src/Packages/Directory.Build.props @@ -0,0 +1,12 @@ + + + + netstandard2.0 + n/a + true + true + true + + + + diff --git a/src/Packages/Directory.Build.targets b/src/Packages/Directory.Build.targets new file mode 100644 index 0000000000..1443711870 --- /dev/null +++ b/src/Packages/Directory.Build.targets @@ -0,0 +1,3 @@ + + + diff --git a/src/Packages/Microsoft.Extensions.AuditReports/EmptyInternalClass.cs b/src/Packages/Microsoft.Extensions.AuditReports/EmptyInternalClass.cs new file mode 100644 index 0000000000..d8febc6138 --- /dev/null +++ b/src/Packages/Microsoft.Extensions.AuditReports/EmptyInternalClass.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Gen.AuditReports; + +internal sealed class EmptyInternalClass +{ + // This class is a place + // holder so we can build a package without additional warnings. +} diff --git a/src/Packages/Microsoft.Extensions.AuditReports/Microsoft.Extensions.AuditReports.csproj b/src/Packages/Microsoft.Extensions.AuditReports/Microsoft.Extensions.AuditReports.csproj new file mode 100644 index 0000000000..2dba09d14f --- /dev/null +++ b/src/Packages/Microsoft.Extensions.AuditReports/Microsoft.Extensions.AuditReports.csproj @@ -0,0 +1,28 @@ + + + Produces reports about the code being compiled which are useful during privacy and telemetry audits. + Fundamentals + + + + true + + + + normal + n/a + n/a + + + + + + + + + + + + + + diff --git a/src/Packages/Microsoft.Extensions.AuditReports/buildTransitive/Microsoft.Extensions.AuditReports.props b/src/Packages/Microsoft.Extensions.AuditReports/buildTransitive/Microsoft.Extensions.AuditReports.props new file mode 100644 index 0000000000..953632a74b --- /dev/null +++ b/src/Packages/Microsoft.Extensions.AuditReports/buildTransitive/Microsoft.Extensions.AuditReports.props @@ -0,0 +1,18 @@ + + + false + true + + + + false + true + + + + + + + + + diff --git a/src/Packages/Microsoft.Extensions.AuditReports/buildTransitive/Microsoft.Extensions.AuditReports.targets b/src/Packages/Microsoft.Extensions.AuditReports/buildTransitive/Microsoft.Extensions.AuditReports.targets new file mode 100644 index 0000000000..46b977341e --- /dev/null +++ b/src/Packages/Microsoft.Extensions.AuditReports/buildTransitive/Microsoft.Extensions.AuditReports.targets @@ -0,0 +1,11 @@ + + + $(OutputPath) + + + + $(OutputPath) + + + + diff --git a/src/Packages/Microsoft.Extensions.ExtraAnalyzers/EmptyInternalClass.cs b/src/Packages/Microsoft.Extensions.ExtraAnalyzers/EmptyInternalClass.cs new file mode 100644 index 0000000000..9f2a954fc6 --- /dev/null +++ b/src/Packages/Microsoft.Extensions.ExtraAnalyzers/EmptyInternalClass.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Analyzers; + +internal sealed class EmptyInternalClass +{ + // This class is a place + // holder so we can build a package without additional warnings. +} diff --git a/src/Packages/Microsoft.Extensions.ExtraAnalyzers/Microsoft.Extensions.ExtraAnalyzers.csproj b/src/Packages/Microsoft.Extensions.ExtraAnalyzers/Microsoft.Extensions.ExtraAnalyzers.csproj new file mode 100644 index 0000000000..319085c9d0 --- /dev/null +++ b/src/Packages/Microsoft.Extensions.ExtraAnalyzers/Microsoft.Extensions.ExtraAnalyzers.csproj @@ -0,0 +1,30 @@ + + + Code analyzers and fixers + Fundamentals + Static Analysis + + + + n/a + true + true + false + true + + + + normal + n/a + n/a + + + + + + + + + + + diff --git a/src/Packages/Microsoft.Extensions.ExtraAnalyzers/buildTransitive/Microsoft.Extensions.ExtraAnalyzers.props b/src/Packages/Microsoft.Extensions.ExtraAnalyzers/buildTransitive/Microsoft.Extensions.ExtraAnalyzers.props new file mode 100644 index 0000000000..0782155be3 --- /dev/null +++ b/src/Packages/Microsoft.Extensions.ExtraAnalyzers/buildTransitive/Microsoft.Extensions.ExtraAnalyzers.props @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/Packages/Microsoft.Extensions.ExtraAnalyzers/buildTransitive/Microsoft.Extensions.ExtraAnalyzers.targets b/src/Packages/Microsoft.Extensions.ExtraAnalyzers/buildTransitive/Microsoft.Extensions.ExtraAnalyzers.targets new file mode 100644 index 0000000000..ceadfacb28 --- /dev/null +++ b/src/Packages/Microsoft.Extensions.ExtraAnalyzers/buildTransitive/Microsoft.Extensions.ExtraAnalyzers.targets @@ -0,0 +1,3 @@ + + + diff --git a/src/Packages/Microsoft.Extensions.StaticAnalysis/Microsoft.Extensions.StaticAnalysis.csproj b/src/Packages/Microsoft.Extensions.StaticAnalysis/Microsoft.Extensions.StaticAnalysis.csproj new file mode 100644 index 0000000000..f6e1daf13c --- /dev/null +++ b/src/Packages/Microsoft.Extensions.StaticAnalysis/Microsoft.Extensions.StaticAnalysis.csproj @@ -0,0 +1,41 @@ + + + Microsoft.Extensions.StaticAnalysis + Curated set of code analyzers and code analyzer settings. + Fundamentals + Static Analysis + + + + true + false + false + false + + $(NoWarn);NU5128 + + + + normal + n/a + n/a + + + + + + + + + + + + + + + + + + + + diff --git a/src/Shared/BufferWriterPool/BufferWriter.cs b/src/Shared/BufferWriterPool/BufferWriter.cs new file mode 100644 index 0000000000..18269616f4 --- /dev/null +++ b/src/Shared/BufferWriterPool/BufferWriter.cs @@ -0,0 +1,192 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using Microsoft.Shared.Diagnostics; +#if NETCOREAPP3_1_OR_GREATER +using System.Runtime.CompilerServices; +#endif + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Pools; +#pragma warning restore CA1716 + +/// +/// Represents an output sink into which data can be written. +/// +/// +/// This class is similar to System.Buffers.ArrayBufferWriter<T>, with the exception that the +/// ArrayBufferWriter<T>.Clear method has been replaced with a +/// method. When used with value types, doesn't clear the underlying memory +/// buffer to default(T), which makes it considerably faster. Additionally, this class +/// lets you explicitly set the capacity of the underlying buffer. +/// +/// The type of value that will be written into the writer. + +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif + +internal sealed class BufferWriter : IBufferWriter +{ + internal const int MaxArrayLength = 0X7FEF_FFFF; // Copy of the internal Array.MaxArrayLength const + private const int DefaultCapacity = 256; + + private T[] _buffer = Array.Empty(); + + /// + /// Gets the data written to the underlying buffer so far. + /// + /// + /// The returned value can become stale whenever the buffer is written to again. As such, you should + /// always call this property in order to get a fresh value before reading from the buffer. + /// + public ReadOnlyMemory WrittenMemory => _buffer.AsMemory(0, WrittenCount); + + /// + /// Gets the data written to the underlying buffer so far. + /// + /// + /// The returned span can become stale whenever the buffer is written to again. As such, you should + /// always call this property in order to get a fresh span before reading from the buffer. + /// + public ReadOnlySpan WrittenSpan => _buffer.AsSpan(0, WrittenCount); + + /// + /// Gets the amount of data written to the underlying buffer so far. + /// + public int WrittenCount { get; private set; } + + /// + /// Gets or sets the total amount of space within the underlying buffer. + /// + /// + /// When reducing the capacity, the value of is clamped to the + /// new capacity. + /// + public int Capacity + { + get => _buffer.Length; + + set + { + _ = Throw.IfLessThan(value, 0); + + Array.Resize(ref _buffer, value); + if (WrittenCount > value) + { + WrittenCount = value; + } + } + } + + /// + /// Clears the data written to the underlying buffer. + /// + /// + /// You must reset the before trying to re-use it. + /// If is or contains reference types, than this method + /// will clear the underlying memory buffers to default(T) in order to ensure the + /// GC is able to reclaim references correctly. If + /// is a value type and only contains value types, then this method doesn't + /// clear memory and so completes considerably faster. + /// + public void Reset() + { +#if NETCOREAPP3_1_OR_GREATER + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + _buffer.AsSpan(0, WrittenCount).Clear(); + } +#else + _buffer.AsSpan(0, WrittenCount).Clear(); +#endif + + WrittenCount = 0; + } + + /// + /// Notifies that amount of data was written to the output /. + /// + /// The amount of data that has been consumed. + /// + /// Thrown when is negative or when attempting to advance past the end of the underlying buffer. + /// + /// + /// You must request a new buffer after calling to continue writing more data and cannot write to a previously acquired buffer. + /// + public void Advance(int count) + { + _ = Throw.IfOutOfRange(count, 0, _buffer.Length - WrittenCount); + + WrittenCount += count; + } + + /// + /// Returns a to write to that is at least the requested length (specified by ). + /// + /// + /// Thrown when is negative. + /// + /// + /// This will never return an empty . + /// There is no guarantee that successive calls will return the same buffer or the same-sized buffer. + /// You must request a new buffer after calling to continue writing more data and cannot write to a previously acquired buffer. + /// + /// THe minimum size of the returned buffer. If this is 0, returns a buffer with at least 1 byte available. + /// A block of memory that can be written to. + public Memory GetMemory(int sizeHint = 0) + { + EnsureCapacity(sizeHint); + return _buffer.AsMemory(WrittenCount); + } + + /// + /// Returns a to write to that is at least the requested length (specified by ). + /// + /// + /// Thrown when is negative. + /// + /// + /// This will never return an empty . + /// There is no guarantee that successive calls will return the same buffer or the same-sized buffer. + /// You must request a new buffer after calling to continue writing more data and cannot write to a previously acquired buffer. + /// + /// THe minimum size of the returned buffer. If this is 0, returns a buffer with at least 1 byte available. + /// A block of memory that can be written to. + public Span GetSpan(int sizeHint = 0) + { + EnsureCapacity(sizeHint); + return _buffer.AsSpan(WrittenCount); + } + + private void EnsureCapacity(int sizeHint) + { + if (sizeHint == 0) + { + sizeHint = 1; + } + + var avail = _buffer.Length - WrittenCount; + if (sizeHint > avail) + { + var targetCapacity = _buffer.Length == 0 ? DefaultCapacity : _buffer.Length * 2; + if (targetCapacity - WrittenCount < sizeHint) + { + targetCapacity = WrittenCount + sizeHint; + } + + if ((uint)targetCapacity > MaxArrayLength) + { + Throw.InvalidOperationException("Exceeded array capacity"); + } + + Array.Resize(ref _buffer, targetCapacity); + } + else + { + _ = Throw.IfLessThan(sizeHint, 0); + } + } +} diff --git a/src/Shared/BufferWriterPool/BufferWriterPool.cs b/src/Shared/BufferWriterPool/BufferWriterPool.cs new file mode 100644 index 0000000000..5ace68bb7b --- /dev/null +++ b/src/Shared/BufferWriterPool/BufferWriterPool.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Pools; +#pragma warning restore CA1716 + +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif +internal static class BufferWriterPool +{ + internal const int DefaultCapacity = 1024; + private const int DefaultMaxBufferWriterCapacity = 256 * 1024; + + /// + /// Creates an object pool of instances. + /// + /// The type of object managed by the buffer writers. + /// The maximum number of items to keep in the pool. This defaults to 1024. This value is a recommendation, the pool may keep more objects than this. + /// The maximum capacity of the buffer writers to keep in the pool. This defaults to 256K. + /// The pool. + public static ObjectPool> CreateBufferWriterPool(int maxCapacity = DefaultCapacity, int maxBufferWriterCapacity = DefaultMaxBufferWriterCapacity) + { + _ = Throw.IfLessThan(maxCapacity, 1); + _ = Throw.IfLessThan(maxBufferWriterCapacity, 1); + + return PoolFactory.CreatePool>(new BufferWriterPooledObjectPolicy(maxBufferWriterCapacity), maxCapacity); + } + + /// + /// Gets the shared pool of instances. + /// + public static ObjectPool> SharedBufferWriterPool { get; } = CreateBufferWriterPool(); +} diff --git a/src/Shared/BufferWriterPool/BufferWriterPooledObjectPolicy.cs b/src/Shared/BufferWriterPool/BufferWriterPooledObjectPolicy.cs new file mode 100644 index 0000000000..50533f74b2 --- /dev/null +++ b/src/Shared/BufferWriterPool/BufferWriterPooledObjectPolicy.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Pools; +#pragma warning restore CA1716 + +/// +/// An object pooling policy designed for . +/// +/// The type of objects to hold in the buffer writer. + +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif + +internal sealed class BufferWriterPooledObjectPolicy : PooledObjectPolicy> +{ + /// + /// Default maximum retained capacity of buffer writer instances in the pool. + /// + private const int DefaultMaximumRetainedCapacity = 256 * 1024; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The maximum capacity of to keep in the pool. + /// If an object is returned to the pool whose capacity exceeds this number, the object + /// instance is not added to the pool, and thus becomes eligible for garbage collection. + /// + public BufferWriterPooledObjectPolicy(int maximumRetainedCapacity = DefaultMaximumRetainedCapacity) + { + MaximumRetainedCapacity = Throw.IfLessThan(maximumRetainedCapacity, 1); + } + + /// + /// Gets the maximum capacity of to keep in the pool. + /// + /// + /// If an object is returned to the pool whose capacity exceeds this number, the object + /// instance is not added to the pool, and thus becomes eligible for garbage collection. + /// Default maximum retained capacity is 256 * 1024 bytes. + /// + public int MaximumRetainedCapacity { get; } + + /// + /// Creates an instance of . + /// + /// The newly created instance. + public override BufferWriter Create() => new(); + + /// + /// Performs any work needed before returning an object to a pool. + /// + /// The object to return to a pool. + /// true if the object should be returned to the pool, false if it shouldn't. + public override bool Return(BufferWriter obj) + { + _ = Throw.IfNull(obj); + + if (obj.Capacity > MaximumRetainedCapacity) + { + // Too big. Discard this one. + return false; + } + + obj.Reset(); + return true; + } +} diff --git a/src/Shared/BufferWriterPool/README.md b/src/Shared/BufferWriterPool/README.md new file mode 100644 index 0000000000..d6413b4c08 --- /dev/null +++ b/src/Shared/BufferWriterPool/README.md @@ -0,0 +1,11 @@ +# Buffer Writer Pool + +Gives access to an object pool of buffer writer instances. + +To use this in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/Shared/Data.Validation/ExclusiveRangeAttribute.cs b/src/Shared/Data.Validation/ExclusiveRangeAttribute.cs new file mode 100644 index 0000000000..45d854f7c1 --- /dev/null +++ b/src/Shared/Data.Validation/ExclusiveRangeAttribute.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Data.Validation; +#pragma warning restore CA1716 + +/// +/// Provides exclusive boundary validation for or values. +/// +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] +internal sealed class ExclusiveRangeAttribute : ValidationAttribute +{ + /// + /// Gets the minimum value for the range. + /// + public object Minimum { get; } + + /// + /// Gets the maximum value for the range. + /// + public object Maximum { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The minimum value, exclusive. + /// The maximum value, exclusive. + public ExclusiveRangeAttribute(int minimum, int maximum) + { + Minimum = minimum; + Maximum = maximum; + } + + /// + /// Initializes a new instance of the class. + /// + /// The minimum value, exclusive. + /// The maximum value, exclusive. + public ExclusiveRangeAttribute(double minimum, double maximum) + { + Minimum = minimum; + Maximum = maximum; + } + + /// + /// Validates that a given value is in range. + /// + /// The value to validate. + /// Additional context for this validation. + /// A value indicating success or failure. + protected override ValidationResult IsValid(object? value, ValidationContext? validationContext) + { + var comparableMin = Minimum as IComparable; + var comparableMax = Maximum as IComparable; + + // Minimun and Maximum are either of type int or double, so there is no need for + // nullability check here (or later) as both types are IComparable already. + if (comparableMin!.CompareTo(Maximum) >= 0) + { + Throw.InvalidOperationException($"{nameof(ExclusiveRangeAttribute)} requires the minimum to be less than the maximum (see field {validationContext.GetDisplayName()})"); + } + + if (value == null) + { + // use the [Required] attribute to force presence + return ValidationResult.Success!; + } + + var result = comparableMin!.CompareTo(value) < 0 && comparableMax!.CompareTo(value) > 0; + + if (!result) + { + return new ValidationResult($"The field {validationContext.GetDisplayName()} must be > {Minimum} and < {Maximum}.", validationContext.GetMemberName()); + } + + return ValidationResult.Success!; + } +} diff --git a/src/Shared/Data.Validation/LengthAttribute.cs b/src/Shared/Data.Validation/LengthAttribute.cs new file mode 100644 index 0000000000..7d598a44e2 --- /dev/null +++ b/src/Shared/Data.Validation/LengthAttribute.cs @@ -0,0 +1,189 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Data.Validation; +#pragma warning restore CA1716 + +/// +/// Specifies the minimum length of any or objects. +/// +/// +/// The standard supports only non generic or typed objects +/// on .NET Framework, while type is supported only on .NET Core. +/// See issue here . +/// This attribute aims to allow validation of all these objects in a consistent manner across target frameworks. +/// +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] + +internal sealed class LengthAttribute : ValidationAttribute +{ + /// + /// Gets the minimum allowed length of the collection or string. + /// + public int MinimumLength { get; } + + /// + /// Gets the maximum allowed length of the collection or string. + /// + public int? MaximumLength { get; } + + /// + /// Gets or sets a value indicating whether the length validation should exclude the and values. + /// + /// + /// By default the property is set to false. + /// + public bool Exclusive { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The minimum allowable length of array/string data. + /// Value must be greater than or equal to zero. + /// + [RequiresUnreferencedCode("Uses reflection to get the 'Count' property on types that don't implement ICollection. This 'Count' property may be trimmed. Ensure it is preserved.")] + public LengthAttribute(int minimumLength) + { + MinimumLength = minimumLength; + MaximumLength = null; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The minimum allowable length of array/string data. + /// Value must be greater than or equal to zero. + /// + /// + /// The maximum allowable length of array/string data. + /// Value must be greater than or equal to zero. + /// + [RequiresUnreferencedCode("Uses reflection to get the 'Count' property on types that don't implement ICollection. This 'Count' property may be trimmed. Ensure it is preserved.")] + public LengthAttribute(int minimumLength, int maximumLength) + { + MinimumLength = minimumLength; + MaximumLength = maximumLength; + } + + /// + /// Validates that a given value is in range. + /// + /// + /// This method returns true if the is null. + /// It is assumed the is used if the value may not be null. + /// + /// The value to validate. + /// Additional context for this validation. + /// A value indicating success or failure. + /// if is less than zero or if it is greater than . + /// if the validated type is not supported. + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "The ctor is marked with RequiresUnreferencedCode.")] + protected override ValidationResult IsValid(object? value, ValidationContext? validationContext) + { + if (MinimumLength < 0) + { + throw new InvalidOperationException($"{nameof(LengthAttribute)} requires a minimum length >= 0 (see field {validationContext.GetDisplayName()})"); + } + + if (MaximumLength.HasValue && MinimumLength >= MaximumLength) + { + throw new InvalidOperationException($"{nameof(LengthAttribute)} requires the minimum length to be less than maximum length (see field {validationContext.GetDisplayName()})"); + } + + // Automatically pass if value is null. RequiredAttribute should be used to assert a value is not null. + if (value == null) + { + return ValidationResult.Success!; + } + + int count; + switch (value) + { + case string s: + count = s.Length; + break; + + case ICollection c: + count = c.Count; + break; + + case IEnumerable e: + count = 0; + foreach (var item in e) + { + count++; + } + + break; + + default: + var property = GetCountProperty(value); + if (property != null && property.CanRead && property.PropertyType == typeof(int)) + { + count = (int)property.GetValue(value)!; + } + else + { + throw new InvalidOperationException($"{nameof(LengthAttribute)} is not supported for fields of type {value.GetType()} (see field {validationContext.GetDisplayName()})"); + } + + break; + } + + return Validate(count, validationContext); + } + + [RequiresUnreferencedCode("Uses reflection to get the 'Count' property on types that don't implement ICollection. This 'Count' property may be trimmed. Ensure it is preserved.")] + private static PropertyInfo? GetCountProperty(object value) => value.GetType().GetRuntimeProperty("Count"); + + private ValidationResult Validate(int count, ValidationContext? validationContext) + { + bool result; + + if (MaximumLength.HasValue) + { + // Minimum and maximum length validation. + result = Exclusive + ? count > MinimumLength && count < MaximumLength + : count >= MinimumLength && count <= MaximumLength; + } + else + { + // Minimum length validation only. + result = Exclusive + ? count > MinimumLength + : count >= MinimumLength; + } + + if (!result) + { + if (!string.IsNullOrEmpty(ErrorMessage) || !string.IsNullOrEmpty(ErrorMessageResourceName)) + { + return new ValidationResult(FormatErrorMessage(validationContext.GetDisplayName()), validationContext.GetMemberName()); + } + + var exclusiveString = Exclusive ? "exclusive " : string.Empty; + var orEqualString = Exclusive ? string.Empty : "or equal "; + var validationMessage = MaximumLength.HasValue + ? $"The field {validationContext.GetDisplayName()} length must be in the {exclusiveString}range [{MinimumLength}..{MaximumLength}]." + : $"The field {validationContext.GetDisplayName()} length must be greater {orEqualString}than {MinimumLength}."; + + return new ValidationResult(validationMessage, validationContext.GetMemberName()); + } + + return ValidationResult.Success!; + } +} diff --git a/src/Shared/Data.Validation/README.md b/src/Shared/Data.Validation/README.md new file mode 100644 index 0000000000..d72619f813 --- /dev/null +++ b/src/Shared/Data.Validation/README.md @@ -0,0 +1,11 @@ +# Data Validation Attributes + +Provides a number of data validation attributes to help validate models. + +To use this in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/Shared/Data.Validation/TimeSpanAttribute.cs b/src/Shared/Data.Validation/TimeSpanAttribute.cs new file mode 100644 index 0000000000..d3926b9db8 --- /dev/null +++ b/src/Shared/Data.Validation/TimeSpanAttribute.cs @@ -0,0 +1,160 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Data.Validation; +#pragma warning restore CA1716 + +/// +/// Provides boundary validation for . +/// +#if !SHARED_PROJECT +[ExcludeFromCodeCoverage] +#endif + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] +[SuppressMessage("Design", "CA1019:Define accessors for attribute arguments", Justification = "Indirectly we are.")] +internal sealed class TimeSpanAttribute : ValidationAttribute +{ + /// + /// Gets the lower bound for time span. + /// + public TimeSpan Minimum => _minMs.HasValue ? TimeSpan.FromMilliseconds((double)_minMs) : TimeSpan.Parse(_min!, CultureInfo.InvariantCulture); + + /// + /// Gets the upper bound for time span. + /// + public TimeSpan? Maximum + { + get + { + if (_maxMs.HasValue) + { + return TimeSpan.FromMilliseconds((double)_maxMs); + } + else + { + return _max == null ? null : TimeSpan.Parse(_max, CultureInfo.InvariantCulture); + } + } + } + + /// + /// Gets or sets a value indicating whether the time span validation should exclude the minimum and maximum values. + /// + /// + /// By default the property is set to false. + /// + public bool Exclusive { get; set; } + + private readonly int? _minMs; + private readonly int? _maxMs; + private readonly string? _min; + private readonly string? _max; + + /// + /// Initializes a new instance of the class. + /// + /// Minimum in milliseconds. + public TimeSpanAttribute(int minMs) + { + _minMs = minMs; + _maxMs = null; + } + + /// + /// Initializes a new instance of the class. + /// + /// Minimum in milliseconds. + /// Maximum in milliseconds. + public TimeSpanAttribute(int minMs, int maxMs) + { + _minMs = minMs; + _maxMs = maxMs; + } + + /// + /// Initializes a new instance of the class. + /// + /// Minimum represented as time span string. + public TimeSpanAttribute(string min) + { + _ = Throw.IfNullOrWhitespace(min); + + _min = min; + _max = null; + } + + /// + /// Initializes a new instance of the class. + /// + /// Minimum represented as time span string. + /// Maximum represented as time span string. + public TimeSpanAttribute(string min, string max) + { + _ = Throw.IfNullOrWhitespace(min); + _ = Throw.IfNullOrWhitespace(max); + + _min = min; + _max = max; + } + + /// + /// Validates that a given value represents an in-range TimeSpan value. + /// + /// The value to validate. + /// Additional context for this validation. + /// A value indicating success or failure. + protected override ValidationResult IsValid(object? value, ValidationContext? validationContext) + { + var min = Minimum; + var max = Maximum; + + if (min >= max) + { + throw new InvalidOperationException($"{nameof(TimeSpanAttribute)} requires that the minimum value be less than the maximum value (see field {validationContext.GetDisplayName()})"); + } + + if (value == null) + { + // use the [Required] attribute to force presence + return ValidationResult.Success!; + } + + if (value is TimeSpan ts) + { + if (Exclusive && ts <= min) + { + return new ValidationResult($"The field {validationContext.GetDisplayName()} must be > to {min}.", validationContext.GetMemberName()); + } + + if (ts < min) + { + return new ValidationResult($"The field {validationContext.GetDisplayName()} must be >= to {min}.", validationContext.GetMemberName()); + } + + if (max.HasValue) + { + if (Exclusive && ts >= max.Value) + { + return new ValidationResult($"The field {validationContext.GetDisplayName()} must be < to {max}.", validationContext.GetMemberName()); + } + + if (ts > max.Value) + { + return new ValidationResult($"The field {validationContext.GetDisplayName()} must be <= to {max}.", validationContext.GetMemberName()); + } + } + + return ValidationResult.Success!; + } + + throw new InvalidOperationException($"{nameof(TimeSpanAttribute)} can only be used with fields of type TimeSpan (see field {validationContext.GetDisplayName()})"); + } +} diff --git a/src/Shared/Data.Validation/ValidationContextExtensions.cs b/src/Shared/Data.Validation/ValidationContextExtensions.cs new file mode 100644 index 0000000000..45d3365b20 --- /dev/null +++ b/src/Shared/Data.Validation/ValidationContextExtensions.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Data.Validation; +#pragma warning restore CA1716 + +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif + +internal static class ValidationContextExtensions +{ + public static string[]? GetMemberName(this ValidationContext? validationContext) + { +#pragma warning disable S1168 // Empty arrays and collections should be returned instead of null + return validationContext?.MemberName is { } memberName + ? new[] { memberName } + : null; +#pragma warning restore S1168 // Empty arrays and collections should be returned instead of null + } + + public static string GetDisplayName(this ValidationContext? validationContext) + { + return validationContext?.DisplayName ?? string.Empty; + } +} diff --git a/src/Shared/Debugger/AttachedDebugger.cs b/src/Shared/Debugger/AttachedDebugger.cs new file mode 100644 index 0000000000..e2af1806eb --- /dev/null +++ b/src/Shared/Debugger/AttachedDebugger.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Diagnostics; +#pragma warning restore CA1716 + +/// +/// Always attached debugger. +/// +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif +internal sealed class AttachedDebugger : IDebuggerState +{ + private AttachedDebugger() + { + // Intentionally left empty. + } + + /// + /// Gets cached instance of . + /// + public static AttachedDebugger Instance { get; } = new(); + + /// + public bool IsAttached => true; +} diff --git a/src/Shared/Debugger/DebuggerExtensions.cs b/src/Shared/Debugger/DebuggerExtensions.cs new file mode 100644 index 0000000000..cc197be12c --- /dev/null +++ b/src/Shared/Debugger/DebuggerExtensions.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Throw = Microsoft.Shared.Diagnostics.Throw; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Diagnostics; +#pragma warning restore CA1716 + +/// +/// Adds debugger to DI container. +/// +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif + +internal static class DebuggerExtensions +{ + /// + /// Registers system debugger as interface. + /// + /// Service collection to register system debugger in. + /// Passed instance of service collection for further configuration. + public static IServiceCollection AddSystemDebuggerState(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + services.TryAddSingleton(DebuggerState.System); + + return services; + } + + /// + /// Registers system debugger as interface. + /// + /// Service collection to register system debugger in. + /// Passed instance of service collection for further configuration. + public static IServiceCollection AddAttachedDebuggerState(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + services.TryAddSingleton(DebuggerState.Attached); + + return services; + } + + /// + /// Registers system debugger as interface. + /// + /// Service collection to register system debugger in. + /// Passed instance of service collection for further configuration. + public static IServiceCollection AddDetachedDebuggerState(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + services.TryAddSingleton(DebuggerState.Detached); + + return services; + } +} diff --git a/src/Shared/Debugger/DebuggerState.cs b/src/Shared/Debugger/DebuggerState.cs new file mode 100644 index 0000000000..0384a9dedb --- /dev/null +++ b/src/Shared/Debugger/DebuggerState.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Diagnostics; +#pragma warning restore CA1716 + +/// +/// Holds all debugger states useful for test writing. +/// +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif + +internal static class DebuggerState +{ + /// + /// Gets a debugger dynamically changing its state depending on environment. + /// + public static IDebuggerState System => SystemDebugger.Instance; + + /// + /// Gets always attached debugger. + /// + public static IDebuggerState Attached => AttachedDebugger.Instance; + + /// + /// Gets always detached debugger. + /// + public static IDebuggerState Detached => DetachedDebugger.Instance; +} diff --git a/src/Shared/Debugger/DetachedDebugger.cs b/src/Shared/Debugger/DetachedDebugger.cs new file mode 100644 index 0000000000..d9fe7efb6c --- /dev/null +++ b/src/Shared/Debugger/DetachedDebugger.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Diagnostics; +#pragma warning restore CA1716 + +/// +/// Always detached debugger. +/// +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif +internal sealed class DetachedDebugger : IDebuggerState +{ + private DetachedDebugger() + { + // Intentionally left empty. + } + + /// + /// Gets cached instance of . + /// + public static DetachedDebugger Instance { get; } = new(); + + /// + public bool IsAttached => false; +} diff --git a/src/Shared/Debugger/IDebuggerState.cs b/src/Shared/Debugger/IDebuggerState.cs new file mode 100644 index 0000000000..9e5eb7ac0b --- /dev/null +++ b/src/Shared/Debugger/IDebuggerState.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Diagnostics; +#pragma warning restore CA1716 + +/// +/// Abstracts debugger presence to increase testability. +/// +internal interface IDebuggerState +{ + /// + /// Gets a value indicating whether a debugger is attached or not. + /// + public bool IsAttached { get; } +} diff --git a/src/Shared/Debugger/README.md b/src/Shared/Debugger/README.md new file mode 100644 index 0000000000..69ef893210 --- /dev/null +++ b/src/Shared/Debugger/README.md @@ -0,0 +1,11 @@ +# Debugger Support + +Enables debugger control to facilitate testing. + +To use this in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/Shared/Debugger/SystemDebugger.cs b/src/Shared/Debugger/SystemDebugger.cs new file mode 100644 index 0000000000..d7eef4b187 --- /dev/null +++ b/src/Shared/Debugger/SystemDebugger.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Diagnostics; +#pragma warning restore CA1716 + +/// +/// Debugger with environment dependent state. +/// +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif +internal sealed class SystemDebugger : IDebuggerState +{ + private SystemDebugger() + { + // Intentionally left empty. + } + + /// + /// Gets cached instance of . + /// + public static SystemDebugger Instance { get; } = new(); + + /// + public bool IsAttached => Debugger.IsAttached; +} diff --git a/src/Shared/EmptyCollections/Empty.cs b/src/Shared/EmptyCollections/Empty.cs new file mode 100644 index 0000000000..bab1d3df9a --- /dev/null +++ b/src/Shared/EmptyCollections/Empty.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Collections; +#pragma warning restore CA1716 + +/// +/// Defines static methods used to optimize the use of empty collections. +/// +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif + +internal static class Empty +{ + /// + /// Returns an optimized empty collection. + /// + /// The type of the collection. + /// Returns an efficient static instance of an empty collection. + public static IReadOnlyCollection ReadOnlyCollection() => EmptyReadOnlyList.Instance; + + /// + /// Returns an optimized empty collection. + /// + /// The type of the collection. + /// Returns an efficient static instance of an empty collection. + public static IEnumerable Enumerable() => EmptyReadOnlyList.Instance; + + /// + /// Returns an optimized empty collection. + /// + /// The type of the collection. + /// Returns an efficient static instance of an empty list. + public static IReadOnlyList ReadOnlyList() => EmptyReadOnlyList.Instance; + + /// + /// Returns an optimized empty dictionary. + /// + /// The key type of the dictionary. + /// The value type of the dictionary. + /// Returns an efficient static instance of an empty dictionary. + public static IReadOnlyDictionary ReadOnlyDictionary() + where TKey : notnull + => EmptyReadOnlyDictionary.Instance; +} diff --git a/src/Shared/EmptyCollections/EmptyCollectionExtensions.cs b/src/Shared/EmptyCollections/EmptyCollectionExtensions.cs new file mode 100644 index 0000000000..cd40b6887d --- /dev/null +++ b/src/Shared/EmptyCollections/EmptyCollectionExtensions.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Collections; +#pragma warning restore CA1716 + +/// +/// Defines static methods used to optimize the use of empty collections. +/// +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif + +internal static class EmptyCollectionExtensions +{ + /// + /// Returns an optimized empty collection if the input is null or empty, otherwise returns the input. + /// + /// The type of the collection. + /// The collection to check for null or empty. + /// Returns a static instance of an empty type if the input collection is null or empty, otherwise the collection. + /// + /// Substituting a static collection whenever an empty collection is needed helps in two ways. First, + /// it allows the original empty collection to be garbage collected, freeing memory. Second, the + /// empty collection that is returned is optimized to not allocated memory whenever the collection is + /// enumerated. + /// + public static IReadOnlyCollection EmptyIfNull(this IReadOnlyCollection? collection) + => collection == null || collection.Count == 0 ? EmptyReadOnlyList.Instance : collection; + + /// + /// Returns an optimized empty collection if the input is null or empty, otherwise returns the input. + /// + /// The type of the collection. + /// The collection to check for null or empty. + /// Returns a static instance of an empty type if the input collection is null or empty, otherwise the collection. + /// + /// Substituting a static collection whenever an empty collection is needed helps in two ways. First, + /// it allows the original empty collection to be garbage collected, freeing memory. Second, the + /// empty collection that is returned is optimized to not allocated memory whenever the collection is + /// enumerated. + /// + public static IEnumerable EmptyIfNull(this ICollection? collection) + => collection == null || collection.Count == 0 ? EmptyReadOnlyList.Instance : collection; + + /// + /// Returns an optimized empty collection if the input is null or empty, otherwise returns the input. + /// + /// The type of the collection. + /// The collection to check for null or empty. + /// Returns a static instance of an empty type if the input collection is null or empty, otherwise the collection. + /// + /// Substituting a static collection whenever an empty collection is needed helps in two ways. First, + /// it allows the original empty collection to be garbage collected, freeing memory. Second, the + /// empty collection that is returned is optimized to not allocated memory whenever the collection is + /// enumerated. + /// + public static IReadOnlyList EmptyIfNull(this IReadOnlyList? list) + => list == null || list.Count == 0 ? EmptyReadOnlyList.Instance : list; + + /// + /// Returns an optimized empty list if the input is null or empty, otherwise returns the input. + /// + /// The type of the collection. + /// The list to check for null or empty. + /// Returns a static instance of an empty type if the input collection is null or empty, otherwise the collection. + /// + /// Substituting a static list whenever an empty collection is needed helps in two ways. First, + /// it allows the original empty collection to be garbage collected, freeing memory. Second, the + /// empty collection that is returned is optimized to not allocated memory whenever the collection is + /// enumerated. + /// + public static IEnumerable EmptyIfNull(this IList? list) + => list == null || list.Count == 0 ? EmptyReadOnlyList.Instance : list; + + /// + /// Returns an optimized empty array if the input is null or empty, otherwise returns the input. + /// + /// The type of the array. + /// The array to check for null or empty. + /// Returns a static instance of an empty array if the input array is null or empty, otherwise the array. + public static T[] EmptyIfNull(this T[]? array) + => array == null || array.Length == 0 ? Array.Empty() : array; + + /// + /// Returns an optimized empty collection if the input is null or can be determined to be empty, otherwise returns the input. + /// + /// The type of the collection. + /// The collection to check for null or empty. + /// Returns a static instance of an empty type if the input collection is null or empty, otherwise the collection. + /// + /// Note that this method does not enumerate the colleciton. + /// + public static IEnumerable EmptyIfNull(this IEnumerable? enumerable) + { + if (enumerable == null) + { + return EmptyReadOnlyList.Instance; + } + + // note this takes care of the IReadOnlyList case too + if (enumerable is IReadOnlyCollection rc && rc.Count == 0) + { + return EmptyReadOnlyList.Instance; + } + + // note this takes care of the IList case too + if (enumerable is ICollection c && c.Count == 0) + { + return EmptyReadOnlyList.Instance; + } + + return enumerable; + } + + /// + /// Returns an optimized empty dictionary if the input is null or can be determined to be empty, otherwise returns the input. + /// + /// The key type of the dictionary. + /// The value type of the dictionary. + /// The dictionary to check for null or empty. + /// Returns a static instance of an empty type if the input dictionary is null or empty, otherwise the dictionary. + /// + /// Note that this method does not enumerate the dictionary. + /// + public static IReadOnlyDictionary EmptyIfNull(this IReadOnlyDictionary? dictionary) + where TKey : notnull + { + if (dictionary == null || dictionary.Count == 0) + { + return EmptyReadOnlyDictionary.Instance; + } + + return dictionary; + } +} diff --git a/src/Shared/EmptyCollections/EmptyReadOnlyList.cs b/src/Shared/EmptyCollections/EmptyReadOnlyList.cs new file mode 100644 index 0000000000..cf0d931284 --- /dev/null +++ b/src/Shared/EmptyCollections/EmptyReadOnlyList.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Collections; +#pragma warning restore CA1716 + +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "Static field, lifetime matches the process")] +internal sealed class EmptyReadOnlyList : IReadOnlyList, ICollection +{ + public static readonly EmptyReadOnlyList Instance = new(); + private readonly Enumerator _enumerator = new(); + + public IEnumerator GetEnumerator() => _enumerator; + IEnumerator IEnumerable.GetEnumerator() => _enumerator; + public int Count => 0; + public T this[int index] => throw new ArgumentOutOfRangeException(nameof(index)); + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + // nop + } + + bool ICollection.Contains(T item) => false; + bool ICollection.IsReadOnly => true; + void ICollection.Add(T item) => throw new NotSupportedException(); + bool ICollection.Remove(T item) => false; + + void ICollection.Clear() + { + // nop + } + + internal sealed class Enumerator : IEnumerator + { + public void Dispose() + { + // nop + } + + public void Reset() + { + // nop + } + + public bool MoveNext() => false; + public T Current => throw new InvalidOperationException(); + object IEnumerator.Current => throw new InvalidOperationException(); + } +} diff --git a/src/Shared/EmptyCollections/EmptyReadonlyDictionary.cs b/src/Shared/EmptyCollections/EmptyReadonlyDictionary.cs new file mode 100644 index 0000000000..99ef907611 --- /dev/null +++ b/src/Shared/EmptyCollections/EmptyReadonlyDictionary.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Collections; +#pragma warning restore CA1716 + +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif + +internal sealed class EmptyReadOnlyDictionary : IReadOnlyDictionary, IDictionary + where TKey : notnull +{ + public static readonly EmptyReadOnlyDictionary Instance = new(); + + public int Count => 0; + public TValue this[TKey key] => throw new KeyNotFoundException(); + public bool ContainsKey(TKey key) => false; + public IEnumerable Keys => EmptyReadOnlyList.Instance; + public IEnumerable Values => EmptyReadOnlyList.Instance; + + public IEnumerator> GetEnumerator() => EmptyReadOnlyList>.Instance.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + ICollection IDictionary.Keys => Array.Empty(); + ICollection IDictionary.Values => Array.Empty(); + bool ICollection>.IsReadOnly => true; + TValue IDictionary.this[TKey key] + { + get => throw new KeyNotFoundException(); + set => throw new NotSupportedException(); + } + + public bool TryGetValue(TKey key, out TValue value) + { +#pragma warning disable CS8601 // The recommended implementation: https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trygetvalue + value = default; +#pragma warning restore + + return false; + } + + void ICollection>.Clear() + { + // nop + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + // nop + } + + void IDictionary.Add(TKey key, TValue value) => throw new NotSupportedException(); + bool IDictionary.Remove(TKey key) => false; + void ICollection>.Add(KeyValuePair item) => throw new NotSupportedException(); + bool ICollection>.Contains(KeyValuePair item) => false; + bool ICollection>.Remove(KeyValuePair item) => false; +} diff --git a/src/Shared/EmptyCollections/README.md b/src/Shared/EmptyCollections/README.md new file mode 100644 index 0000000000..744af69e27 --- /dev/null +++ b/src/Shared/EmptyCollections/README.md @@ -0,0 +1,11 @@ +# Empty collections + +Utility functions to create efficient read-only empty collections. + +To use this in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/Shared/Memoization/Memoize.cs b/src/Shared/Memoization/Memoize.cs new file mode 100644 index 0000000000..1b6c669100 --- /dev/null +++ b/src/Shared/Memoization/Memoize.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Memoization; +#pragma warning restore CA1716 + +/// +/// Given a function of arity N (1 <= N <= 3), return a function that behaves identically, except that +/// repeated invocations with the same parameters return a cached value rather than redoing the computation. +/// +/// +/// Memoize is like a , but for functions instead of values. +/// Memoize will use the equality of the types of the input parameters. This means that arbitrary objects +/// will use reference equality, unless those types define their own equality semantics. This implies that +/// callers should take care to use types with Memoize that would be safe to put in a dictionary: if the +/// type is mutable, and its Equals/GetHashCode depends on those mutable parts, unexpected behaviour may +/// result. +/// +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif + +internal static class Memoize +{ + /// + /// Returns a function that remembers the results of previous invocations of the given function. + /// + /// The function input type. + /// The function output type. + /// The function that needs to be memoized. + /// A function that appears identical to the original function, but duplicate invocations are nearly instant. + /// + /// Computed values consume memory. Garbage collection will free up that memory when the returned + /// Func is freed. If you're computing values for large numbers of inputs, bear this in mind: if + /// the Func lives for a long time, memory usage can increase without bound. + /// + public static Func Function(Func f) + => new MemoizedFunction(f).Function; + + /// + /// Returns a function that remembers the results of previous invocations of the given function. + /// + /// The type of the function's first parameter. + /// The type of the function's second parameter. + /// The function output type. + /// The function that needs to be memoized. + /// A function that appears identical to the original function, but duplicate invocations are nearly instant. + /// + /// Computed values consume memory. Garbage collection will free up that memory when the returned + /// Func is freed. If you're computing values for large numbers of inputs, bear this in mind: if + /// the Func lives for a long time, memory usage can increase without bound. + /// + public static Func Function(Func f) + => new MemoizedFunction(f).Function; + + /// + /// Returns a function that remembers the results of previous invocations of the given function. + /// + /// The type of the function's first parameter. + /// The type of the function's second parameter. + /// The type of the function's third parameter. + /// The function output type. + /// The function that needs to be memoized. + /// A function that appears identical to the original function, but duplicate invocations are nearly instant. + /// + /// Computed values consume memory. Garbage collection will free up that memory when the returned + /// Func is freed. If you're computing values for large numbers of inputs, bear this in mind: if + /// the Func lives for a long time, memory usage can increase without bound. + /// + [SuppressMessage( + "Major Code Smell", + "S2436:Types and methods should not have too many generic parameters", + Justification = "We're using many generic types for the same reason Func<>, Func<,>, Func<,,>, ... exist.")] + public static Func Function(Func f) + => new MemoizedFunction(f).Function; +} diff --git a/src/Shared/Memoization/MemoizedFunction.cs b/src/Shared/Memoization/MemoizedFunction.cs new file mode 100644 index 0000000000..2fcbfb3d4d --- /dev/null +++ b/src/Shared/Memoization/MemoizedFunction.cs @@ -0,0 +1,237 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Memoization; +#pragma warning restore CA1716 + +#pragma warning disable SA1402 // File may only contain a single type + +/// +/// Memoizer for functions of arity 1. +/// +/// +/// We don't use weak references because those can only wrap reference types, and we wish to support functions that return other kinds of values. +/// +/// Input parameter type for the memoized function. +/// Return type for the memoized function. + +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif + +[DebuggerDisplay("{_values.Count} memoized values")] +internal sealed class MemoizedFunction +{ + private const int Concurrency = 10; + private const int Capacity = 100; + + // Using a readonly struct means that we can assert in the ConcurrentDictionary + // declaration that all keys are non-null. Otherwise we need to say so in the + // type declaration ("where TParameter : notnull"), which forces users to care. + internal readonly struct Arg : IEquatable.Arg> + { + private readonly int _hash; + + public Arg(TParameter arg1) + { + Arg1 = arg1; + _hash = Arg1?.GetHashCode() ?? 0; + } + + public readonly TParameter Arg1; + + public override bool Equals(object? obj) => obj is MemoizedFunction.Arg arg && Equals(arg); + + public bool Equals(MemoizedFunction.Arg other) => EqualityComparer.Default.Equals(Arg1, other.Arg1); + + public override int GetHashCode() => _hash; + } + + private readonly ConcurrentDictionary> _values; + + private readonly Func _function; + + /// + /// Initializes a new instance of the class. + /// + /// The function whose results will be memoized. + public MemoizedFunction(Func function) + { + _function = Throw.IfNull(function); + _values = new(Concurrency, Capacity); + } + + internal TResult Function(TParameter arg1) + { + var arg = new Arg(arg1); + + // Stryker disable once all + if (_values.TryGetValue(arg, out var result)) + { + return result.Value; + } + + return _values.GetOrAdd(arg, new Lazy(() => _function(arg1))).Value; + } +} + +/// +/// Memoizer for functions of arity 2. +/// +/// First input parameter type for the memoized function. +/// Second input parameter type for the memoized function. +/// Return type for the memoized function. + +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif + +[SuppressMessage( + "Major Code Smell", + "S2436:Types and methods should not have too many generic parameters", + Justification = "We're using many generic types for the same reason Func<>, Func<,>, Func<,,>, ... exist.")] +[DebuggerDisplay("{_values.Count} memoized values")] +internal sealed class MemoizedFunction +{ + private const int Concurrency = 10; + private const int Capacity = 100; + + internal readonly struct Args : IEquatable.Args> + { + private readonly int _hash; + + public Args(TParameter1 arg1, TParameter2 arg2) + { + Arg1 = arg1; + Arg2 = arg2; + _hash = HashCode.Combine(Arg1, Arg2); + } + + public readonly TParameter1 Arg1; + + public readonly TParameter2 Arg2; + + public override bool Equals(object? obj) => obj is MemoizedFunction.Args args && Equals(args); + + public bool Equals(MemoizedFunction.Args other) => + EqualityComparer.Default.Equals(Arg1, other.Arg1) + && EqualityComparer.Default.Equals(Arg2, other.Arg2); + + public override int GetHashCode() => _hash; + } + + private readonly ConcurrentDictionary> _values; + + private readonly Func _function; + + /// + /// Initializes a new instance of the class. + /// + /// The function whose results will be memoized. + public MemoizedFunction(Func function) + { + _function = Throw.IfNull(function); + _values = new(Concurrency, Capacity); + } + + internal TResult Function(TParameter1 arg1, TParameter2 arg2) + { + var args = new Args(arg1, arg2); + + // Stryker disable once all + if (_values.TryGetValue(args, out var result)) + { + return result.Value; + } + + return _values.GetOrAdd(args, new Lazy(() => _function(arg1, arg2))).Value; + } +} + +/// +/// Memoizer for functions of arity 3. +/// +/// First input parameter type for the memoized function. +/// Second input parameter type for the memoized function. +/// Third input parameter type for the memoized function. +/// Return type for the memoized function. + +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif + +[SuppressMessage( + "Major Code Smell", + "S2436:Types and methods should not have too many generic parameters", + Justification = "We're using many generic types for the same reason Func<>, Func<,>, Func<,,>, ... exist.")] +[DebuggerDisplay("{_values.Count} memoized values")] +internal sealed class MemoizedFunction +{ + private const int Concurrency = 10; + private const int Capacity = 100; + + internal readonly struct Args : IEquatable.Args> + { + private readonly int _hash; + + public Args(TParameter1 arg1, TParameter2 arg2, TParameter3 arg3) + { + Arg1 = arg1; + Arg2 = arg2; + Arg3 = arg3; + + _hash = HashCode.Combine(Arg1, Arg2, Arg3); + } + + public readonly TParameter1 Arg1; + + public readonly TParameter2 Arg2; + + public readonly TParameter3 Arg3; + + public override bool Equals(object? obj) => + obj is MemoizedFunction.Args args && Equals(args); + + public bool Equals(MemoizedFunction.Args other) => + EqualityComparer.Default.Equals(Arg1, other.Arg1) + && EqualityComparer.Default.Equals(Arg2, other.Arg2) + && EqualityComparer.Default.Equals(Arg3, other.Arg3); + + public override int GetHashCode() => _hash; + } + + private readonly ConcurrentDictionary> _values; + + private readonly Func _function; + + /// + /// Initializes a new instance of the class. + /// + /// The function whose results will be memoized. + public MemoizedFunction(Func function) + { + _function = Throw.IfNull(function); + _values = new(Concurrency, Capacity); + } + + internal TResult Function(TParameter1 arg1, TParameter2 arg2, TParameter3 arg3) + { + var args = new Args(arg1, arg2, arg3); + + // Stryker disable once all + if (_values.TryGetValue(args, out var result)) + { + return result.Value; + } + + return _values.GetOrAdd(args, new Lazy(() => _function(arg1, arg2, arg3))).Value; + } +} diff --git a/src/Shared/Memoization/README.md b/src/Shared/Memoization/README.md new file mode 100644 index 0000000000..ab989e7f7f --- /dev/null +++ b/src/Shared/Memoization/README.md @@ -0,0 +1,53 @@ +# Memoization + +Utility to provide simple and efficient function caching. + +To use this in your project, add the following to your `.csproj` file: + +```xml + + true + +``` + +## Example + +For example, + +```csharp +async Task Delay(int seconds) +{ + // Simulate a long-running/expensive function. + await Task.Delay(TimeSpan.FromSeconds(seconds)); + return seconds; +} + +Console.WriteLine($"t1 = {DateTimeOffset.UtcNow:o}"); +await Delay(1); +await Delay(1); +Console.WriteLine($"t2 = {DateTimeOffset.UtcNow:o}"); + +var memoizedDelay = Memoize.Function>(Delay); + +Console.WriteLine($"t3 = {DateTimeOffset.UtcNow:o}"); +await memoizedDelay(1); +await memoizedDelay(1); +Console.WriteLine($"t4 = {DateTimeOffset.UtcNow:o}") +``` + +prints out + +```text +t1 = 2021-10-22T21:57:12.3753431+00:00 +t2 = 2021-10-22T21:57:14.3973958+00:00 +t3 = 2021-10-22T21:57:14.4034649+00:00 +t4 = 2021-10-22T21:57:15.4163199+00:00 +``` + +## Notes + +Memoize will use the equality of the types of the input parameters. This means that arbitrary objects +will use reference equality, unless those types define their own equality semantics. This implies that +callers should take care to use types with Memoize that would be safe to put in a Dictionary: if the +type is mutable, and its `Equals`/`GetHashCode` depends on those mutable parts, unexpected behavior may +result. diff --git a/src/Shared/NumericExtensions/NumericExtensions.cs b/src/Shared/NumericExtensions/NumericExtensions.cs new file mode 100644 index 0000000000..3fff74b193 --- /dev/null +++ b/src/Shared/NumericExtensions/NumericExtensions.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Text; +#pragma warning restore CA1716 + +#pragma warning disable R9A036 // this is the implementation of ToInvariantString, so this warning doesn't make sense here + +/// +/// Utilities to augment the basic numeric types. +/// +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif + +#if NET8_0_OR_GREATER + +internal static class NumericExtensions +{ + /// + /// Formats an integer as an invariant string. + /// + /// The value to format as a string. + /// The string representation of the integer value. + /// + /// This works identically to value.ToString(CultureInfo.InvariantCulture) except that it is faster as it maintains + /// preformatted strings for common integer values. + /// + public static string ToInvariantString(this int value) => value.ToString(CultureInfo.InvariantCulture); + + /// + /// Formats a 64-bit integer as an invariant string. + /// + /// The value to format as a string. + /// The string representation of the integer value. + /// + /// This works identically to value.ToString(CultureInfo.InvariantCulture) except that it is faster as it maintains + /// preformatted strings for common integer values. + /// + public static string ToInvariantString(this long value) => value.ToString(CultureInfo.InvariantCulture); +} + +#else + +internal static class NumericExtensions +{ + private const int MinCachedValue = -1; + private const int MaxCachedValue = 1024; + private const int NumCachedValues = MaxCachedValue - MinCachedValue + 1; + + private static readonly string[] _cachedValues = MakeCachedValues(); + + /// + /// Formats an integer as an invariant string. + /// + /// The value to format as a string. + /// The string representation of the integer value. + /// + /// This works identically to value.ToString(CultureInfo.InvariantCulture) except that it is faster as it maintains + /// preformatted strings for common integer values. + /// + public static string ToInvariantString(this int value) + { + if (value >= MinCachedValue && value <= MaxCachedValue) + { + return _cachedValues[value - MinCachedValue]; + } + + return value.ToString(CultureInfo.InvariantCulture); + } + + /// + /// Formats a 64-bit integer as an invariant string. + /// + /// The value to format as a string. + /// The string representation of the integer value. + /// + /// This works identically to value.ToString(CultureInfo.InvariantCulture) except that it is faster as it maintains + /// preformatted strings for common integer values. + /// + public static string ToInvariantString(this long value) + { + if (value >= MinCachedValue && value <= MaxCachedValue) + { + return _cachedValues[value - MinCachedValue]; + } + + return value.ToString(CultureInfo.InvariantCulture); + } + + private static string[] MakeCachedValues() + { + var values = new string[NumCachedValues]; + + int index = 0; + for (int i = MinCachedValue; i <= MaxCachedValue; i++) + { + values[index++] = i.ToString(CultureInfo.InvariantCulture); + } + + return values; + } +} + +#endif diff --git a/src/Shared/NumericExtensions/README.md b/src/Shared/NumericExtensions/README.md new file mode 100644 index 0000000000..bcb2d9a7cb --- /dev/null +++ b/src/Shared/NumericExtensions/README.md @@ -0,0 +1,11 @@ +# Numeric Extensions + +`ToInvariantString` function to get fast ordinal numeric conversion. + +To use this in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/Shared/Pools/NoopPooledObjectPolicy.cs b/src/Shared/Pools/NoopPooledObjectPolicy.cs new file mode 100644 index 0000000000..6ac38ad21e --- /dev/null +++ b/src/Shared/Pools/NoopPooledObjectPolicy.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.ObjectPool; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Pools; + +/// +/// An object pool policy that does nothing. +/// +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +internal sealed class NoopPooledObjectPolicy : PooledObjectPolicy + where T : notnull, new() +{ + public static NoopPooledObjectPolicy Instance { get; } = new(); + + private NoopPooledObjectPolicy() + { + } + + public override T Create() => new(); + public override bool Return(T obj) => true; +} diff --git a/src/Shared/Pools/PoolFactory.cs b/src/Shared/Pools/PoolFactory.cs new file mode 100644 index 0000000000..a742ddfffa --- /dev/null +++ b/src/Shared/Pools/PoolFactory.cs @@ -0,0 +1,192 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text; +using System.Threading; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Pools; + +#pragma warning disable R9A038 + +/// +/// A factory of object pools. +/// +/// +/// This class makes it easy to create efficient object pools used to improve performance by reducing +/// strain on the garbage collector. +/// +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +internal static class PoolFactory +{ + internal const int DefaultCapacity = 1024; + private const int DefaultMaxStringBuilderCapacity = 64 * 1024; + private const int InitialStringBuilderCapacity = 128; + + private static readonly IPooledObjectPolicy _defaultStringBuilderPolicy = new StringBuilderPooledObjectPolicy + { + InitialCapacity = InitialStringBuilderCapacity, + MaximumRetainedCapacity = DefaultCapacity + }; + + /// + /// Creates an object pool. + /// + /// The type of object to keep in the pool. + /// The maximum number of items to keep in the pool. This defaults to 1024. This value is a recommendation, the pool may keep more objects than this. + /// The pool. + public static ObjectPool CreatePool(int maxCapacity = DefaultCapacity) + where T : class, new() + { + _ = Throw.IfLessThan(maxCapacity, 1); + + return MakePool(NoopPooledObjectPolicy.Instance, maxCapacity); + } + + /// + /// Creates an object pool with a custom policy. + /// + /// The type of object to keep in the pool. + /// The custom policy that is responsible for creating new objects and preparing objects to be added to the pool. + /// The maximum number of items to keep in the pool. This defaults to 1024. This value is a recommendation, the pool may keep more objects than this. + /// The pool. + public static ObjectPool CreatePool(IPooledObjectPolicy policy, int maxCapacity = DefaultCapacity) + where T : class + { + _ = Throw.IfNull(policy); + _ = Throw.IfLessThan(maxCapacity, 1); + + return MakePool(policy, maxCapacity); + } + + /// + /// Creates an object pool for resettable objects. + /// + /// The type of object to keep in the pool. + /// The maximum number of items to keep in the pool. This defaults to 1024. This value is a recommendation, the pool may keep more objects than this. + /// The pool. + /// + /// Objects are systematically reset before being added to the pool. + /// + public static ObjectPool CreateResettingPool(int maxCapacity = DefaultCapacity) + where T : class, IResettable, new() + { + _ = Throw.IfLessThan(maxCapacity, 1); + + return MakePool(new DefaultPooledObjectPolicy(), maxCapacity); + } + + /// + /// Creates a pool of instances. + /// + /// The maximum number of items to keep in the pool. This defaults to 1024. This value is a recommendation, the pool may keep more objects than this. + /// The maximum capacity of the string builders to keep in the pool. This defaults to 64K. + /// The pool. + public static ObjectPool CreateStringBuilderPool(int maxCapacity = DefaultCapacity, int maxStringBuilderCapacity = DefaultMaxStringBuilderCapacity) + { + _ = Throw.IfLessThan(maxCapacity, 1); + _ = Throw.IfLessThan(maxStringBuilderCapacity, 1); + + if (maxStringBuilderCapacity == DefaultMaxStringBuilderCapacity) + { + return MakePool(_defaultStringBuilderPolicy, maxCapacity); + } + + return MakePool( + new StringBuilderPooledObjectPolicy + { + InitialCapacity = InitialStringBuilderCapacity, + MaximumRetainedCapacity = maxStringBuilderCapacity + }, maxCapacity); + } + + /// + /// Creates an object pool of instances. + /// + /// The type of object held by the lists. + /// + /// The maximum number of items to keep in the pool. + /// This defaults to 1024. + /// This value is a recommendation, the pool may keep more objects than this. + /// + /// The pool. + public static ObjectPool> CreateListPool(int maxCapacity = DefaultCapacity) + { + _ = Throw.IfLessThan(maxCapacity, 1); + + return MakePool(PooledListPolicy.Instance, maxCapacity); + } + + /// + /// Creates an object pool of instances. + /// + /// The type of the dictionary keys. + /// The type of the dictionary values. + /// Optional key comparer used by the dictionaries. + /// + /// The maximum number of items to keep in the pool. + /// This defaults to 1024. + /// This value is a recommendation, the pool may keep more objects than this. + /// + /// The pool. + public static ObjectPool> CreateDictionaryPool(IEqualityComparer? comparer = null, int maxCapacity = DefaultCapacity) + where TKey : notnull + { + _ = Throw.IfLessThan(maxCapacity, 1); + + return MakePool(new PooledDictionaryPolicy(comparer), maxCapacity); + } + + /// + /// Creates an object pool of instances. + /// + /// The type of objects held in the sets. + /// Optional key comparer used by the sets. + /// + /// The maximum number of items to keep in the pool. + /// This defaults to 1024. + /// This value is a recommendation, the pool may keep more objects than this. + /// + /// The pool. + public static ObjectPool> CreateHashSetPool(IEqualityComparer? comparer = null, int maxCapacity = DefaultCapacity) + where T : notnull + { + _ = Throw.IfLessThan(maxCapacity, 1); + + return MakePool(new PooledSetPolicy(comparer), maxCapacity); + } + + /// + /// Creates an object pool of instances. + /// + /// + /// The maximum number of items to keep in the pool. + /// This defaults to 1024. + /// This value is a recommendation, the pool may keep more objects than this. + /// + /// The pool. + /// + /// On .NET 6 and above, cancellation token sources are reusable and this pool leverages this feature. + /// When running on older frameworks, this pool is actually a no-op, every time a source is fetched + /// from the pool, it is always a new instance. In that case, returning an object to the pool merely + /// disposes it. + /// + public static ObjectPool CreateCancellationTokenSourcePool(int maxCapacity = DefaultCapacity) + { + _ = Throw.IfLessThan(maxCapacity, 1); + + return MakePool(PooledCancellationTokenSourcePolicy.Instance, maxCapacity); + } + + /// + /// Gets the shared pool of instances. + /// + public static ObjectPool SharedStringBuilderPool { get; } = CreateStringBuilderPool(); + + private static DefaultObjectPool MakePool(IPooledObjectPolicy policy, int maxRetained) + where T : class + => new(policy, maxRetained); +} diff --git a/src/Shared/Pools/PooledCancellationTokenSourcePolicy.cs b/src/Shared/Pools/PooledCancellationTokenSourcePolicy.cs new file mode 100644 index 0000000000..c738478708 --- /dev/null +++ b/src/Shared/Pools/PooledCancellationTokenSourcePolicy.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Microsoft.Extensions.ObjectPool; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Pools; + +/// +/// An object pool policy for cancellation token sources. +/// +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +internal sealed class PooledCancellationTokenSourcePolicy : PooledObjectPolicy +{ + public static PooledCancellationTokenSourcePolicy Instance { get; } = new(); + + private PooledCancellationTokenSourcePolicy() + { + } + + public override CancellationTokenSource Create() => new(); + + public override bool Return(CancellationTokenSource obj) + { +#if NET6_0_OR_GREATER + if (obj.TryReset()) + { + return true; + } +#endif + + obj.Dispose(); + return false; + } +} diff --git a/src/Shared/Pools/PooledDictionaryPolicy.cs b/src/Shared/Pools/PooledDictionaryPolicy.cs new file mode 100644 index 0000000000..1d85d84e4d --- /dev/null +++ b/src/Shared/Pools/PooledDictionaryPolicy.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.ObjectPool; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Pools; + +/// +/// An object pool policy for dictionaries. +/// +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +internal sealed class PooledDictionaryPolicy : PooledObjectPolicy> + where TKey : notnull +{ + private readonly IEqualityComparer? _comparer; + + public PooledDictionaryPolicy(IEqualityComparer? comparer = null) + { + _comparer = comparer; + } + + public override Dictionary Create() => new(_comparer); + + public override bool Return(Dictionary obj) + { + obj.Clear(); + return true; + } +} diff --git a/src/Shared/Pools/PooledListPolicy.cs b/src/Shared/Pools/PooledListPolicy.cs new file mode 100644 index 0000000000..b74469a0ab --- /dev/null +++ b/src/Shared/Pools/PooledListPolicy.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.ObjectPool; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Pools; + +/// +/// An object pool policy for lists. +/// +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +internal sealed class PooledListPolicy : PooledObjectPolicy> +{ + public static PooledListPolicy Instance { get; } = new(); + + private PooledListPolicy() + { + } + + public override List Create() => new(); + + public override bool Return(List obj) + { + obj.Clear(); + return true; + } +} diff --git a/src/Shared/Pools/PooledSetPolicy.cs b/src/Shared/Pools/PooledSetPolicy.cs new file mode 100644 index 0000000000..4a7b3876a4 --- /dev/null +++ b/src/Shared/Pools/PooledSetPolicy.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.ObjectPool; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Pools; + +/// +/// An object pool policy for sets. +/// +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +internal sealed class PooledSetPolicy : PooledObjectPolicy> + where T : notnull +{ + private readonly IEqualityComparer? _comparer; + + public PooledSetPolicy(IEqualityComparer? comparer = null) + { + _comparer = comparer; + } + + public override HashSet Create() => new(_comparer); + + public override bool Return(HashSet obj) + { + obj.Clear(); + return true; + } +} diff --git a/src/Shared/Pools/README.md b/src/Shared/Pools/README.md new file mode 100644 index 0000000000..09344ba0fc --- /dev/null +++ b/src/Shared/Pools/README.md @@ -0,0 +1,11 @@ +# Pools + +Provides an efficient object pool implementation, along with a few supporting types. + +To use this in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/Shared/RentedSpan/README.md b/src/Shared/RentedSpan/README.md new file mode 100644 index 0000000000..6ed49cb1b1 --- /dev/null +++ b/src/Shared/RentedSpan/README.md @@ -0,0 +1,11 @@ +# Rented Span + +Utility to make it simpler to deal with stack allocated buffers. + +To use this in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/Shared/RentedSpan/RentedSpan.cs b/src/Shared/RentedSpan/RentedSpan.cs new file mode 100644 index 0000000000..b0fd6d9ed4 --- /dev/null +++ b/src/Shared/RentedSpan/RentedSpan.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Pools; +#pragma warning restore CA1716 + +/// +/// Represents a span that's potentially created over a rented array. +/// +/// The type of objects held by the span. +/// +/// This type is used to implement a common pattern to improve performance +/// when using temporary buffers. The pattern encourages the use of stack-based +/// buffers when possible, which eliminate the overhead of garbage collection of +/// buffers. +/// +/// With this type, you make a speculative call to allocate a buffer. If the buffer +/// is too large to fix on the stack, it is allocated from . +/// If the buffer could be allocated on the stack, then no buffer is acquired from the +/// array pool, and instead the caller is expected to use +/// to get the buffer. +/// +/// +/// +/// using var rental = new RentedSpan<char>(length); +/// var span = rental.Rented ? rental.Span : stackalloc char[length]; +/// +/// +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif + +internal readonly ref struct RentedSpan +{ + /// + /// The minimum size in bytes that triggers buffer rental. + /// + internal const int MinimumRentalSpace = 256; + + private readonly int _length; + private readonly T[]? _rentedBuffer; + + /// + /// Initializes a new instance of the struct. + /// + /// The desired length of the span. if this value is %lt;= 0, no buffer is allocated. + public RentedSpan(int length) + { + var size = Unsafe.SizeOf() * length; + if (size >= MinimumRentalSpace) + { + _rentedBuffer = ArrayPool.Shared.Rent(length); + } + else + { + _rentedBuffer = null; + } + + _length = length; + } + + /// + /// Returns a rented array back to the array pool. + /// + /// + /// If no array was actually allocated from the array pool + /// (when is ), + /// then this method has no effect. + /// + /// Calling this method multiple times on the same object is not supported, don't do it. + /// + public void Dispose() + { + if (_rentedBuffer != null) + { + ArrayPool.Shared.Return(_rentedBuffer); + } + } + + /// + /// Gets a span over the rented buffer. + /// + /// + /// If no buffer was rented (because the buffer was deemed too small), then this returns an empty span. + /// When a buffer isn't rented by this type, it's a cue to you to allocate buffer from the stack instead + /// using stackalloc. + /// + public Span Span => _rentedBuffer != null ? _rentedBuffer.AsSpan(0, _length) : Array.Empty().AsSpan(); + + /// + /// Gets a value indicating whether a buffer has been rented. + /// + public bool Rented => _rentedBuffer != null; +} diff --git a/src/Shared/Shared.csproj b/src/Shared/Shared.csproj new file mode 100644 index 0000000000..1d8c3d2a6f --- /dev/null +++ b/src/Shared/Shared.csproj @@ -0,0 +1,42 @@ + + + Microsoft.Shared + Reusable shared code. + Fundamentals + + + + $(NetCoreTargetFrameworks)$(ConditionalNet462) + false + $(DefineConstants);SHARED_PROJECT + true + true + true + true + true + true + true + true + + + + normal + 89 + 100 + 85 + + + + + + + + + + + + + + + + diff --git a/src/Shared/Text.Formatting/CompositeFormat.cs b/src/Shared/Text.Formatting/CompositeFormat.cs new file mode 100644 index 0000000000..aa40d6da28 --- /dev/null +++ b/src/Shared/Text.Formatting/CompositeFormat.cs @@ -0,0 +1,915 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Text; +#pragma warning restore CA1716 + +/// +/// Provides highly efficient string formatting functionality. +/// +/// +/// This type lets you optimize string formatting operations common with the +/// method. This is useful for any situation where you need to repeatedly format the same string with +/// different arguments. +/// +/// This type works faster than string.Format because it parses the composite format string only once when +/// the instance is created, rather than doing it for every formatting operation. +/// +/// You first create an instance of this type, passing the composite format string that you intend to use. +/// Once the instance is created, you call the method with arguments to use in the +/// format operation. +/// +/// You should only use this type if you need to repeatedly reuse the same composite format strings over time. If you only ever +/// use a composite format string once, you're better off using the original string.Format call. +/// +#if !SHARED_PROJECT +[ExcludeFromCodeCoverage] +#endif + +[SuppressMessage("Major Code Smell", "S109:Magic numbers should not be used", Justification = "Acceptable use of magic numbers")] +[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Comparing instances is not an expected scenario")] +internal readonly struct CompositeFormat +{ + internal const int MaxStackAlloc = 128; // = 256 bytes + + private readonly Segment[] _segments; // info on the different chunks to process + + /// + /// Initializes a new instance of the struct. + /// + /// A classic .NET format string as used with . + /// + /// Parses a composite format string into an efficient form for later use. + /// + /// When the format string is malformed. + public static CompositeFormat Parse([StringSyntaxAttribute(StringSyntaxAttribute.CompositeFormat)] ReadOnlySpan format) + { + if (!TryParse(format, out var cf, out var error)) + { + Throw.ArgumentException(nameof(format), error); + } + + return cf; + } + + /// + /// Initializes a new instance of the struct. + /// + /// A classic .NET format string as used with . + /// + /// Parses a composite format string into an efficient form for later use. + /// + /// When the format string is malformed. + public static CompositeFormat Parse([StringSyntaxAttribute(StringSyntaxAttribute.CompositeFormat)] string format) + => Parse(format.AsSpan()); + + /// + /// Initializes a new instance of the struct. + /// + /// A template-based .NET format string as used with LogMethod.Define. + /// Holds the named templates discovered in the format string. + /// + /// Parses a composite format string into an efficient form for later use. + /// + /// When the format string is malformed. + public static CompositeFormat Parse([StringSyntaxAttribute(StringSyntaxAttribute.CompositeFormat)] ReadOnlySpan format, out IList templates) + { + var l = new List(); + + if (!TryParse(format, l, out var cf, out var error)) + { + Throw.ArgumentException(nameof(format), error); + } + + templates = l; + + return cf; + } + + private CompositeFormat(Segment[] segments, int numArgumentsNeeded, string literalString) + { + _segments = segments; + NumArgumentsNeeded = numArgumentsNeeded; + LiteralString = literalString; + } + + /// + /// Initializes a new instance of the struct. + /// + /// A classic .NET format string as used with . + /// Upon successful return, an initialized instance. + /// Upon a failed return, a string providing details about the parsing error. + /// + /// Parses a composite format string into an efficient form for later use. + /// + /// if the string parsed correctly, otherwise. + public static bool TryParse([StringSyntaxAttribute(StringSyntaxAttribute.CompositeFormat)] ReadOnlySpan format, out CompositeFormat result, [NotNullWhen(false)] out string? error) + { + return TryParse(format, null, out result, out error); + } + + /// + /// Initializes a new instance of the struct. + /// + /// A classic .NET format string as used with . + /// Upon successful return, an initialized instance. + /// Upon a failed return, a string providing details about the parsing error. + /// + /// Parses a composite format string into an efficient form for later use. + /// + /// if the string parsed correctly, otherwise. + public static bool TryParse([StringSyntaxAttribute(StringSyntaxAttribute.CompositeFormat)] string format, out CompositeFormat result, [NotNullWhen(false)] out string? error) + { + return TryParse(format.AsSpan(), out result, out error); + } + + /// + /// Initializes a new instance of the struct. + /// + /// A template-based .NET format string as used with LogMethod.Define. + /// Holds the named templates discovered in the format string. + /// Upon successful return, an initialized instance. + /// Upon a failed return, a string providing details about the parsing error. + /// + /// Parses a composite format string into an efficient form for later use. + /// + /// if the string parsed correctly, otherwise. + public static bool TryParse([StringSyntaxAttribute( + StringSyntaxAttribute.CompositeFormat)] ReadOnlySpan format, + IList? templates, + out CompositeFormat result, + [NotNullWhen(false)] out string? error) + { + var pos = 0; + var len = format.Length; + var ch = '\0'; + var segments = new List(); + var numArgs = 0; + using var literal = (format.Length >= MaxStackAlloc) ? new StringMaker(format.Length) : new StringMaker(stackalloc char[MaxStackAlloc]); + + result = default; + error = null; + + while (true) + { + var segStart = literal.Length; + while (pos < len) + { + ch = format[pos]; + + pos++; + if (ch == '}') + { + if (pos < len && format[pos] == '}') + { + // double }, treat as escape sequence + pos++; + } + else + { + // dangling }, fail + error = $"Dangling }} in format string at position {pos}"; + return false; + } + } + else if (ch == '{') + { + if (pos < len && format[pos] == '{') + { + // double {, treat as escape sequence + pos++; + } + else + { + // start of a format specification + pos--; + break; + } + } + + literal.Append(ch); + } + + if (pos == len) + { + var totalLit = literal.Length - segStart; + while (totalLit > 0) + { + var num = Math.Min(totalLit, short.MaxValue); + segments.Add(new Segment((short)num, -1, 0, string.Empty)); + totalLit -= num; + } + + result = new CompositeFormat(segments.ToArray(), numArgs, literal.ExtractString()); + return true; + } + + // extract the argument index + var argIndex = 0; + if (templates == null) + { + // classic composite format string + + pos++; + if (pos == len || (ch = format[pos]) < '0' || ch > '9') + { + // we need an argument index + error = $"Missing argument index in format string at position {pos}"; + return false; + } + + var start = pos; + do + { + argIndex = (argIndex * 10) + (ch - '0'); + + if (argIndex > short.MaxValue) + { + error = $"Argument index in format string at position {start} must be less than 32768"; + return false; + } + + pos++; + + // make sure we get a suitable end to the argument index + if (pos == len) + { + error = $"Invalid character in format string argument index at position {pos}"; + return false; + } + + ch = format[pos]; + } + while (ch >= '0' && ch <= '9'); + } + else + { + // template-based format string + + pos++; + if (pos == len) + { + // we need a template name + error = $"Missing template name in format string at position {pos}"; + return false; + } + + ch = format[pos]; + if (!ValidTemplateNameChar(ch, true)) + { + // we need a template name + error = $"Missing template name in format string at position {pos}"; + return false; + } + + // extract the template name + var start = pos; + do + { + pos++; + + // make sure we get a suitable end + if (pos == len) + { + error = $"Invalid template name in format string at position {pos}"; + return false; + } + + ch = format[pos]; + } + while (ValidTemplateNameChar(ch, false)); + + // get an argument index for the given template + var template = format.Slice(start, pos - start).ToString(); + argIndex = templates.IndexOf(template); + if (argIndex < 0) + { + templates.Add(template); + argIndex = numArgs; + } + } + + if (argIndex >= numArgs) + { + // new max arg count + numArgs = argIndex + 1; + } + + // skip whitespace + while (pos < len && (ch = format[pos]) == ' ') + { + pos++; + } + + // parse the optional field width + var leftAligned = false; + var argWidth = 0; + if (ch == ',') + { + pos++; + + // skip whitespace + while (pos < len && format[pos] == ' ') + { + pos++; + } + + // did we run out of steam + if (pos == len) + { + error = $"No field width found format string at position {pos}"; + return false; + } + + ch = format[pos]; + if (ch == '-') + { + leftAligned = true; + pos++; + + // did we run out of steam? + if (pos == len) + { + error = $"Invalid field width in format string at position {pos}"; + return false; + } + + ch = format[pos]; + } + + if (ch < '0' || ch > '9') + { + error = $"Invalid character in field width in format string at position {pos}"; + return false; + } + + var val = 0; + var start = pos; + do + { + val = (val * 10) + (ch - '0'); + pos++; + + // did we run out of steam? + if (pos == len) + { + error = $"Incomplete field width in format string at position {pos}"; + return false; + } + + // did we get a number that's too big? + if (val > short.MaxValue) + { + error = $"Field width in format string at position {start} must be less than 32768"; + return false; + } + + ch = format[pos]; + } + while (ch >= '0' && ch <= '9'); + + argWidth = val; + } + + if (leftAligned) + { + argWidth = -argWidth; + } + + // skip whitespace + while (pos < len && (ch = format[pos]) == ' ') + { + pos++; + } + + // parse the optional argument format string + + var argFormat = string.Empty; + if (ch == ':') + { + pos++; + var argFormatStart = pos; + + while (true) + { + if (pos == len) + { + error = $"Unterminated format specification in format string at position {pos}"; + return false; + } + + ch = format[pos]; + pos++; + if (ch == '{') + { + error = $"Nested {{ in format string at position {pos}"; + return false; + } + else if (ch == '}') + { + // end of format specification + pos--; + break; + } + } + + if (pos != argFormatStart) + { + argFormat = format.Slice(argFormatStart, pos - argFormatStart).ToString(); + } + } + + if (ch != '}') + { + error = "Unterminated format specification in format string at position {pos}"; + return false; + } + + // skip over the closing brace + pos++; + + var total = literal.Length - segStart; + while (total > short.MaxValue) + { + segments.Add(new Segment(short.MaxValue, -1, 0, string.Empty)); + total -= short.MaxValue; + } + + segments.Add(new Segment((short)total, (short)argIndex, (short)argWidth, argFormat)); + } + } + + /// + /// Formats a string with a single argument. + /// + /// Type of the single argument. + /// An optional format provider that provides formatting functionality for individual arguments. + /// An argument to use in the formatting operation. + /// The formatted string. + public string Format(IFormatProvider? provider, T arg) + { + CheckNumArgs(1, null); + return Fmt(provider, arg, null, null, null, EstimateArgSize(arg)); + } + + /// + /// Formats a string with two arguments. + /// + /// Type of the first argument. + /// Type of the second argument. + /// An optional format provider that provides formatting functionality for individual arguments. + /// First argument to use in the formatting operation. + /// Second argument to use in the formatting operation. + /// The formatted string. + public string Format(IFormatProvider? provider, T0 arg0, T1 arg1) + { + CheckNumArgs(2, null); + return Fmt(provider, arg0, arg1, null, null, EstimateArgSize(arg0) + EstimateArgSize(arg1)); + } + + /// + /// Formats a string with three arguments. + /// + /// Type of the first argument. + /// Type of the second argument. + /// Type of the third argument. + /// An optional format provider that provides formatting functionality for individual arguments. + /// First argument to use in the formatting operation. + /// Second argument to use in the formatting operation. + /// Third argument to use in the formatting operation. + /// The formatted string. + public string Format(IFormatProvider? provider, T0 arg0, T1 arg1, T2 arg2) + { + CheckNumArgs(3, null); + return Fmt(provider, arg0, arg1, arg2, null, EstimateArgSize(arg0) + EstimateArgSize(arg1) + EstimateArgSize(arg2)); + } + + /// + /// Formats a string with arguments. + /// + /// An optional format provider that provides formatting functionality for individual arguments. + /// Type of the first argument. + /// Type of the second argument. + /// Type of the third argument. + /// First argument to use in the formatting operation. + /// Second argument to use in the formatting operation. + /// Third argument to use in the formatting operation. + /// Additional arguments to use in the formatting operation. + /// The formatted string. + public string Format(IFormatProvider? provider, T0 arg0, T1 arg1, T2 arg2, params object?[]? args) + { + CheckNumArgs(3, args); + return Fmt(provider, arg0, arg1, arg2, args, EstimateArgSize(arg0) + EstimateArgSize(arg1) + EstimateArgSize(arg2) + EstimateArgSize(args)); + } + + /// + /// Formats a string with arguments. + /// + /// An optional format provider that provides formatting functionality for individual arguments. + /// Arguments to use in the formatting operation. + /// The formatted string. + public string Format(IFormatProvider? provider, params object?[]? args) + { + CheckNumArgs(0, args); + + if (NumArgumentsNeeded == 0) + { + return LiteralString; + } + + var estimatedSize = EstimateArgSize(args); + +#pragma warning disable CA1062 // Validate arguments of public methods - already handled by CheckNumArgs above + return args!.Length switch +#pragma warning restore CA1062 // Validate arguments of public methods - already handled by CheckNumArgs above + { + 1 => Fmt(provider, args[0], null, null, null, estimatedSize), + 2 => Fmt(provider, args[0], args[1], null, null, estimatedSize), + 3 => Fmt(provider, args[0], args[1], args[2], null, estimatedSize), + _ => Fmt(provider, args[0], args[1], args[2], args.AsSpan(3), estimatedSize), + }; + } + + /// + /// Formats a string with one argument. + /// + /// Type of the single argument. + /// Where to write the resulting string. + /// The number of characters actually written to the destination span. + /// An optional format provider that provides formatting functionality for individual arguments. + /// An argument to use in the formatting operation. + /// True if there was enough room in the destination span for the resulting string. + public bool TryFormat(Span destination, out int charsWritten, IFormatProvider? provider, T arg) + { + CheckNumArgs(1, null); + return TryFmt(destination, out charsWritten, provider, arg, null, null, null); + } + + /// + /// Formats a string with two arguments. + /// + /// Type of the first argument. + /// Type of the second argument. + /// Where to write the resulting string. + /// The number of characters actually written to the destination span. + /// An optional format provider that provides formatting functionality for individual arguments. + /// First argument to use in the formatting operation. + /// Second argument to use in the formatting operation. + /// True if there was enough room in the destination span for the resulting string. + public bool TryFormat(Span destination, out int charsWritten, IFormatProvider? provider, T0 arg0, T1 arg1) + { + CheckNumArgs(2, null); + return TryFmt(destination, out charsWritten, provider, arg0, arg1, null, null); + } + + /// + /// Formats a string with three arguments. + /// + /// Type of the first argument. + /// Type of the second argument. + /// Type of the third argument. + /// Where to write the resulting string. + /// The number of characters actually written to the destination span. + /// An optional format provider that provides formatting functionality for individual arguments. + /// First argument to use in the formatting operation. + /// Second argument to use in the formatting operation. + /// Third argument to use in the formatting operation. + /// True if there was enough room in the destination span for the resulting string. + public bool TryFormat(Span destination, out int charsWritten, IFormatProvider? provider, T0 arg0, T1 arg1, T2 arg2) + { + CheckNumArgs(3, null); + return TryFmt(destination, out charsWritten, provider, arg0, arg1, arg2, null); + } + + /// + /// Formats a string with arguments. + /// + /// Type of the first argument. + /// Type of the second argument. + /// Type of the third argument. + /// Where to write the resulting string. + /// The number of characters actually written to the destination span. + /// An optional format provider that provides formatting functionality for individual arguments. + /// First argument to use in the formatting operation. + /// Second argument to use in the formatting operation. + /// Third argument to use in the formatting operation. + /// Additional arguments to use in the formatting operation. + /// True if there was enough room in the destination span for the resulting string. + public bool TryFormat(Span destination, out int charsWritten, IFormatProvider? provider, T0 arg0, T1 arg1, T2 arg2, params object?[]? args) + { + CheckNumArgs(3, args); + return TryFmt(destination, out charsWritten, provider, arg0, arg1, arg2, args); + } + + /// + /// Formats a string with arguments. + /// + /// Where to write the resulting string. + /// The number of characters actually written to the destination span. + /// An optional format provider that provides formatting functionality for individual arguments. + /// Arguments to use in the formatting operation. + /// True if there was enough room in the destination span for the resulting string. + public bool TryFormat(Span destination, out int charsWritten, IFormatProvider? provider, params object?[]? args) + { + CheckNumArgs(0, args); + + if (NumArgumentsNeeded == 0) + { + if (destination.Length < LiteralString.Length) + { + charsWritten = 0; + return false; + } + + LiteralString.AsSpan().CopyTo(destination); + charsWritten = LiteralString.Length; + return true; + } + +#pragma warning disable CA1062 // Validate arguments of public methods - already handled by CheckNumArgs above + return args!.Length switch +#pragma warning restore CA1062 // Validate arguments of public methods + { + 1 => TryFmt(destination, out charsWritten, provider, args[0], null, null, null), + 2 => TryFmt(destination, out charsWritten, provider, args[0], args[1], null, null), + 3 => TryFmt(destination, out charsWritten, provider, args[0], args[1], args[2], null), + _ => TryFmt(destination, out charsWritten, provider, args[0], args[1], args[2], args.AsSpan(3)), + }; + } + + internal StringBuilder AppendFormat(StringBuilder sb, IFormatProvider? provider, T arg) + { + CheckNumArgs(1, null); + return AppendFmt(sb, provider, arg, null, null, null, EstimateArgSize(arg)); + } + + internal StringBuilder AppendFormat(StringBuilder sb, IFormatProvider? provider, T0 arg0, T1 arg1) + { + CheckNumArgs(2, null); + return AppendFmt(sb, provider, arg0, arg1, null, null, EstimateArgSize(arg0) + EstimateArgSize(arg1)); + } + + internal StringBuilder AppendFormat(StringBuilder sb, IFormatProvider? provider, T0 arg0, T1 arg1, T2 arg2) + { + CheckNumArgs(3, null); + return AppendFmt(sb, provider, arg0, arg1, arg2, null, EstimateArgSize(arg0) + EstimateArgSize(arg1) + EstimateArgSize(arg2)); + } + + internal StringBuilder AppendFormat(StringBuilder sb, IFormatProvider? provider, T0 arg0, T1 arg1, T2 arg2, params object?[]? args) + { + CheckNumArgs(3, args); + return AppendFmt(sb, provider, arg0, arg1, arg2, args, EstimateArgSize(arg0) + EstimateArgSize(arg1) + EstimateArgSize(arg2) + EstimateArgSize(args)); + } + + internal StringBuilder AppendFormat(StringBuilder sb, IFormatProvider? provider, params object?[]? args) + { + CheckNumArgs(0, args); + + if (NumArgumentsNeeded == 0) + { + return sb.Append(LiteralString); + } + + var estimatedSize = EstimateArgSize(args); + + return args!.Length switch + { + 1 => AppendFmt(sb, provider, args[0], null, null, null, estimatedSize), + 2 => AppendFmt(sb, provider, args[0], args[1], null, null, estimatedSize), + 3 => AppendFmt(sb, provider, args[0], args[1], args[2], null, estimatedSize), + _ => AppendFmt(sb, provider, args[0], args[1], args[2], args.AsSpan(3), estimatedSize), + }; + } + + private static void AppendArg(ref StringMaker sm, T arg, string argFormat, IFormatProvider? provider, int argWidth) + { + switch (arg) + { + case int a: + sm.Append(a, argFormat, provider, argWidth); + break; + + case long a: + sm.Append(a, argFormat, provider, argWidth); + break; + + case string a: + sm.Append(a, argWidth); + break; + + case double a: + sm.Append(a, argFormat, provider, argWidth); + break; + + case float a: + sm.Append(a, argFormat, provider, argWidth); + break; + + case uint a: + sm.Append(a, argFormat, provider, argWidth); + break; + + case ulong a: + sm.Append(a, argFormat, provider, argWidth); + break; + + case short a: + sm.Append(a, argFormat, provider, argWidth); + break; + + case ushort a: + sm.Append(a, argFormat, provider, argWidth); + break; + + case byte a: + sm.Append(a, argFormat, provider, argWidth); + break; + + case sbyte a: + sm.Append(a, argFormat, provider, argWidth); + break; + + case bool a: + sm.Append(a, argWidth); + break; + + case char a: + sm.Append(a, argWidth); + break; + + case decimal a: + sm.Append(a, argFormat, provider, argWidth); + break; + + case DateTime a: + sm.Append(a, argFormat, provider, argWidth); + break; + + case TimeSpan a: + sm.Append(a, argFormat, provider, argWidth); + break; + +#if NET6_0_OR_GREATER + case System.ISpanFormattable a: + sm.Append(a, argFormat, provider, argWidth); + break; +#endif + + case IFormattable a: + sm.Append(a, argFormat, provider, argWidth); + break; + + case object a: + sm.Append(a, argWidth); + break; + + default: + // when arg == null + sm.Append(string.Empty, argWidth); + break; + } + } + + private static bool ValidTemplateNameChar(char ch, bool first) + { + if (first) + { + return char.IsLetter(ch) || ch == '_'; + } + + return char.IsLetterOrDigit(ch) || ch == '_'; + } + + private static int EstimateArgSize(T arg) + { + var str = arg as string; + if (str != null) + { + return str.Length; + } + + return 8; + } + + private static int EstimateArgSize(object?[]? args) + { + int total = 0; + + if (args != null) + { + foreach (var arg in args) + { + if (arg is string str) + { + total += str.Length; + } + } + } + + return total; + } + + [SkipLocalsInit] + private string Fmt(IFormatProvider? provider, T0 arg0, T1 arg1, T2 arg2, ReadOnlySpan args, int estimatedSize) + { + estimatedSize += LiteralString.Length; + var sm = (estimatedSize >= MaxStackAlloc) ? new StringMaker(estimatedSize) : new StringMaker(stackalloc char[MaxStackAlloc]); + CoreFmt(ref sm, provider, arg0, arg1, arg2, args); + return sm.ExtractString(); + } + + private bool TryFmt(Span destination, out int charsWritten, IFormatProvider? provider, T0 arg0, T1 arg1, T2 arg2, ReadOnlySpan args) + { + var sm = new StringMaker(destination, true); + CoreFmt(ref sm, provider, arg0, arg1, arg2, args); + charsWritten = sm.Length; + var overflowed = sm.Overflowed; + return !overflowed; + } + + [SkipLocalsInit] + private StringBuilder AppendFmt(StringBuilder sb, IFormatProvider? provider, T0 arg0, T1 arg1, T2 arg2, ReadOnlySpan args, int estimatedSize) + { + estimatedSize += LiteralString.Length; + var sm = (estimatedSize >= MaxStackAlloc) ? new StringMaker(estimatedSize) : new StringMaker(stackalloc char[MaxStackAlloc]); + CoreFmt(ref sm, provider, arg0, arg1, arg2, args); + sm.AppendTo(sb); + sm.Dispose(); + return sb; + } + + private void CheckNumArgs(int explicitCount, object?[]? args) + { + var total = explicitCount; + if (args != null) + { + total += args.Length; + } + + if (NumArgumentsNeeded > total) + { + Throw.ArgumentException(nameof(args), $"Expected {NumArgumentsNeeded} arguments, but got {total}"); + } + } + + private void CoreFmt(ref StringMaker sm, IFormatProvider? provider, T0 arg0, T1 arg1, T2 arg2, ReadOnlySpan args) + { + var literalIndex = 0; + foreach (var segment in _segments) + { + int literalCount = segment.LiteralCount; + if (literalCount > 0) + { + // the segment has some literal text + sm.Append(LiteralString.AsSpan(literalIndex, literalCount)); + literalIndex += literalCount; + } + + var argIndex = segment.ArgIndex; + if (argIndex >= 0) + { + // the segment has an arg to format + switch (argIndex) + { + case 0: + AppendArg(ref sm, arg0, segment.ArgFormat, provider, segment.ArgWidth); + break; + + case 1: + AppendArg(ref sm, arg1, segment.ArgFormat, provider, segment.ArgWidth); + break; + + case 2: + AppendArg(ref sm, arg2, segment.ArgFormat, provider, segment.ArgWidth); + break; + + default: + AppendArg(ref sm, args[argIndex - 3], segment.ArgFormat, provider, segment.ArgWidth); + break; + } + } + } + } + + /// + /// Gets the number of arguments required in order to produce a string with this instance. + /// + public int NumArgumentsNeeded { get; } + + /// + /// Gets all literal text to be inserted into the output. + /// + /// + /// In the case where the format string doesn't contain any formatting + /// sequence, this literal is the string to produce when formatting. + /// + private readonly string LiteralString { get; } +} diff --git a/src/Shared/Text.Formatting/FormatExtensions.cs b/src/Shared/Text.Formatting/FormatExtensions.cs new file mode 100644 index 0000000000..892644d5c7 --- /dev/null +++ b/src/Shared/Text.Formatting/FormatExtensions.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !NETCOREAPP3_1_OR_GREATER +using System; +using System.Globalization; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Text; +#pragma warning restore CA1716 + +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif +internal static class FormatExtensions +{ + public static bool TryFormat(this DateTime value, Span target, out int charsWritten, string? format, IFormatProvider? provider) + { + var s = value.ToString(format, provider); + if (s.Length > target.Length) + { + charsWritten = 0; + return false; + } + + charsWritten = s.Length; + for (int i = 0; i < s.Length; i++) + { + target[i] = s[i]; + } + + return true; + } + + public static bool TryFormat(this TimeSpan value, Span target, out int charsWritten, string? format, IFormatProvider? provider) + { + var s = value.ToString(format, provider); + if (s.Length > target.Length) + { + charsWritten = 0; + return false; + } + + charsWritten = s.Length; + for (int i = 0; i < s.Length; i++) + { + target[i] = s[i]; + } + + return true; + } + + public static bool TryFormat(this long value, Span target, out int charsWritten, string? format, IFormatProvider? provider) + { + var s = value.ToString(format, provider); + if (s.Length > target.Length) + { + charsWritten = 0; + return false; + } + + charsWritten = s.Length; + for (int i = 0; i < s.Length; i++) + { + target[i] = s[i]; + } + + return true; + } + + public static bool TryFormat(this double value, Span target, out int charsWritten, string? format, IFormatProvider? provider) + { + var s = value.ToString(format, provider); + if (s.Length > target.Length) + { + charsWritten = 0; + return false; + } + + charsWritten = s.Length; + for (int i = 0; i < s.Length; i++) + { + target[i] = s[i]; + } + + return true; + } + + public static bool TryFormat(this decimal value, Span target, out int charsWritten, string? format, IFormatProvider? provider) + { + var s = value.ToString(format, provider); + if (s.Length > target.Length) + { + charsWritten = 0; + return false; + } + + charsWritten = s.Length; + for (int i = 0; i < s.Length; i++) + { + target[i] = s[i]; + } + + return true; + } + + public static bool TryFormat(this ulong value, Span target, out int charsWritten, string? format, IFormatProvider? provider) + { + var s = value.ToString(format, provider); + if (s.Length > target.Length) + { + charsWritten = 0; + return false; + } + + charsWritten = s.Length; + for (int i = 0; i < s.Length; i++) + { + target[i] = s[i]; + } + + return true; + } + + public static bool TryFormat(this bool value, Span target, out int charsWritten) + { + var s = value.ToString(CultureInfo.InvariantCulture); + if (s.Length > target.Length) + { + charsWritten = 0; + return false; + } + + charsWritten = s.Length; + for (int i = 0; i < s.Length; i++) + { + target[i] = s[i]; + } + + return true; + } +} +#endif diff --git a/src/Shared/Text.Formatting/README.md b/src/Shared/Text.Formatting/README.md new file mode 100644 index 0000000000..d2f3003292 --- /dev/null +++ b/src/Shared/Text.Formatting/README.md @@ -0,0 +1,11 @@ +# Text Formatting + +Fast text formatting code. + +To use this in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/Shared/Text.Formatting/Segment.cs b/src/Shared/Text.Formatting/Segment.cs new file mode 100644 index 0000000000..3de129db2b --- /dev/null +++ b/src/Shared/Text.Formatting/Segment.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Text; +#pragma warning restore CA1716 + +/// +/// A chunk of formatting information. +/// +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif +internal readonly struct Segment +{ + public Segment(short literalCount, short argIndex, short argWidth, string argFormat) + { + LiteralCount = literalCount; + ArgIndex = argIndex; + ArgWidth = argWidth; + ArgFormat = argFormat; + } + + /// + /// Gets the number of chars of literal text consumed by this segment. + /// + public short LiteralCount { get; } + + /// + /// Gets the index of the argument to be formatted, -1 to skip argument formatting. + /// + public short ArgIndex { get; } + + /// + /// Gets the width of the formatted value in characters. If this is negative, it indicates to left-justify + /// and the field width is then the absolute value. + /// + public short ArgWidth { get; } + + /// + /// Gets the custom format string to use when formatting the argument. + /// + public string ArgFormat { get; } +} diff --git a/src/Shared/Text.Formatting/StringBuilderExtensions.cs b/src/Shared/Text.Formatting/StringBuilderExtensions.cs new file mode 100644 index 0000000000..b390cc2abd --- /dev/null +++ b/src/Shared/Text.Formatting/StringBuilderExtensions.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +#if NETCOREAPP3_1_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +using System.Text; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Text; +#pragma warning restore CA1716 + +/// +/// Extensions for accelerated formatting on . +/// +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif + +#if NETCOREAPP3_1_OR_GREATER +[SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Handled downstream")] +#endif + +internal static class StringBuilderExtensions +{ + /// + /// Formats a string with a single argument. + /// + /// Type of the single argument. + /// The string builder to append to. + /// An optional format provider that provides formatting functionality for individual arguments. + /// The composite format to apply. + /// An argument to use in the formatting operation. + /// The input string builder for call chaining. + public static StringBuilder AppendFormat(this StringBuilder sb, IFormatProvider? provider, CompositeFormat format, T arg) + => format.AppendFormat(sb, provider, arg); + + /// + /// Formats a string with two arguments. + /// + /// Type of the first argument. + /// Type of the second argument. + /// The string builder to append to. + /// The composite format to apply. + /// An optional format provider that provides formatting functionality for individual arguments. + /// First argument to use in the formatting operation. + /// Second argument to use in the formatting operation. + /// The input string builder for call chaining. + public static StringBuilder AppendFormat(this StringBuilder sb, CompositeFormat format, IFormatProvider? provider, T0 arg0, T1 arg1) + => format.AppendFormat(sb, provider, arg0, arg1); + + /// + /// Formats a string with three arguments. + /// + /// Type of the first argument. + /// Type of the second argument. + /// Type of the third argument. + /// The string builder to append to. + /// The composite format to apply. + /// An optional format provider that provides formatting functionality for individual arguments. + /// First argument to use in the formatting operation. + /// Second argument to use in the formatting operation. + /// Third argument to use in the formatting operation. + /// The input string builder for call chaining. + public static StringBuilder AppendFormat(this StringBuilder sb, CompositeFormat format, IFormatProvider? provider, T0 arg0, T1 arg1, T2 arg2) + => format.AppendFormat(sb, provider, arg0, arg1, arg2); + + /// + /// Formats a string with arguments. + /// + /// Type of the first argument. + /// Type of the second argument. + /// Type of the third argument. + /// The string builder to append to. + /// The composite format to apply. + /// An optional format provider that provides formatting functionality for individual arguments. + /// First argument to use in the formatting operation. + /// Second argument to use in the formatting operation. + /// Third argument to use in the formatting operation. + /// Additional arguments to use in the formatting operation. + /// The input string builder for call chaining. + public static StringBuilder AppendFormat(this StringBuilder sb, CompositeFormat format, IFormatProvider? provider, T0 arg0, T1 arg1, T2 arg2, params object?[]? args) + => format.AppendFormat(sb, provider, arg0, arg1, arg2, args); + + /// + /// Formats a string with arguments. + /// + /// The string builder to append to. + /// The composite format to apply. + /// An optional format provider that provides formatting functionality for individual arguments. + /// Arguments to use in the formatting operation. + /// The input string builder for call chaining. +#pragma warning disable CA1062 // Validate arguments of public methods - already handled by CheckNumArgs above + public static StringBuilder AppendFormat(this StringBuilder sb, CompositeFormat format, IFormatProvider? provider, params object?[]? args) + => format.AppendFormat(sb, provider, args); +#pragma warning restore CA1062 // Validate arguments of public methods - already handled by CheckNumArgs above +} diff --git a/src/Shared/Text.Formatting/StringMaker.cs b/src/Shared/Text.Formatting/StringMaker.cs new file mode 100644 index 0000000000..f2f142cd50 --- /dev/null +++ b/src/Shared/Text.Formatting/StringMaker.cs @@ -0,0 +1,436 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Text; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Text; +#pragma warning restore CA1716 + +#pragma warning disable IDE0064 + +#if !SHARED_PROJECT +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +#endif +internal ref struct StringMaker +{ + public const int DefaultCapacity = 128; + + private readonly bool _fixedCapacity; + private char[]? _rentedBuffer; + private Span _chars; + + public StringMaker(Span initialBuffer, bool fixedCapacity = false) + { + _rentedBuffer = null; + _chars = initialBuffer; + _fixedCapacity = fixedCapacity; + Length = 0; + Overflowed = false; + } + + public StringMaker(int initialCapacity) + { + _rentedBuffer = ArrayPool.Shared.Rent(initialCapacity); + _chars = _rentedBuffer; + _fixedCapacity = false; + Length = 0; + Overflowed = false; + } + + public void Dispose() + { + if (_rentedBuffer != null) + { + ArrayPool.Shared.Return(_rentedBuffer); + } + + // clear out everything to prevent accidental reuse + this = default; + } + + public string ExtractString() => _chars.Slice(0, Length).ToString(); + public ReadOnlySpan ExtractSpan() => _chars.Slice(0, Length); + +#if NETCOREAPP3_1_OR_GREATER + internal void AppendTo(StringBuilder sb) => _ = sb.Append(_chars.Slice(0, Length)); +#else + internal void AppendTo(StringBuilder sb) => _ = sb.Append(_chars.Slice(0, Length).ToString()); +#endif + public int Length { get; private set; } + public bool Overflowed { get; private set; } + + public void Fill(char value, int count) + { + if (!Ensure(count)) + { + return; + } + + _chars.Slice(Length, count).Fill(value); + Length += count; + } + + public void Append(string? value, int width) + { + if (value == null) + { + Fill(' ', width); + } + else if (width == 0) + { + if (!Ensure(value.Length)) + { + return; + } + + value.AsSpan().CopyTo(_chars.Slice(Length)); + Length += value.Length; + } + else if (width > value.Length) + { + Fill(' ', width - value.Length); + FinishAppend(value, 0); + } + else + { + FinishAppend(value, width); + } + } + + public void Append(ReadOnlySpan value) + { + if (!Ensure(value.Length)) + { + return; + } + + value.CopyTo(_chars.Slice(Length)); + Length += value.Length; + } + + public void Append(ReadOnlySpan value, int width) + { + if (width == 0) + { + if (!Ensure(value.Length)) + { + return; + } + + value.CopyTo(_chars.Slice(Length)); + Length += value.Length; + } + else if (width > value.Length) + { + Fill(' ', width - value.Length); + FinishAppend(value, 0); + } + else + { + FinishAppend(value, width); + } + } + + public void Append(char value) + { + if (!Ensure(1)) + { + return; + } + + _chars[Length++] = value; + } + + public void Append(char value, int width) + { + if (width >= -1 && width <= 1) + { + if (!Ensure(1)) + { + return; + } + + _chars[Length++] = value; + } + else if (width > 1) + { + if (!Ensure(width)) + { + return; + } + + _chars.Slice(Length, width - 1).Fill(' '); + Length += width; + _chars[Length - 1] = value; + } + else + { + width = -width; + if (!Ensure(width)) + { + return; + } + + _chars[Length++] = value; + _chars.Slice(Length, width - 1).Fill(' '); + Length += width - 1; + } + } + +#if !NET6_0_OR_GREATER + public void Append(long value, string? format, IFormatProvider? provider, int width) + { + int charsWritten; + while (!value.TryFormat(_chars.Slice(Length), out charsWritten, format, provider)) + { + if (!Expand()) + { + return; + } + } + + FinishAppend(charsWritten, width); + } + + public void Append(ulong value, string? format, IFormatProvider? provider, int width) + { + int charsWritten; + while (!value.TryFormat(_chars.Slice(Length), out charsWritten, format, provider)) + { + if (!Expand()) + { + return; + } + } + + FinishAppend(charsWritten, width); + } + + public void Append(double value, string? format, IFormatProvider? provider, int width) + { + int charsWritten; + while (!value.TryFormat(_chars.Slice(Length), out charsWritten, format, provider)) + { + if (!Expand()) + { + return; + } + } + + FinishAppend(charsWritten, width); + } + + public void Append(bool value, int width) + { + int charsWritten; + while (!value.TryFormat(_chars.Slice(Length), out charsWritten)) + { + if (!Expand()) + { + return; + } + } + + FinishAppend(charsWritten, width); + } + + public void Append(decimal value, string? format, IFormatProvider? provider, int width) + { + int charsWritten; + while (!value.TryFormat(_chars.Slice(Length), out charsWritten, format, provider)) + { + if (!Expand()) + { + return; + } + } + + FinishAppend(charsWritten, width); + } + + public void Append(DateTime value, string? format, IFormatProvider? provider, int width) + { + int charsWritten; + while (!value.TryFormat(_chars.Slice(Length), out charsWritten, format, provider)) + { + if (!Expand()) + { + return; + } + } + + FinishAppend(charsWritten, width); + } + + public void Append(TimeSpan value, string? format, IFormatProvider? provider, int width) + { + int charsWritten; + while (!value.TryFormat(_chars.Slice(Length), out charsWritten, format, provider)) + { + if (!Expand()) + { + return; + } + } + + FinishAppend(charsWritten, width); + } +#endif + +#if NET6_0_OR_GREATER + public void Append(T value, string? format, IFormatProvider? provider, int width) + where T : System.ISpanFormattable + { + int charsWritten; + while (!value.TryFormat(_chars.Slice(Length), out charsWritten, format.AsSpan(), provider)) + { + if (!Expand()) + { + return; + } + } + + FinishAppend(charsWritten, width); + } +#endif + + public void Append(IFormattable value, string? format, IFormatProvider? provider, int width) + { + FinishAppend(value.ToString(format, provider), width); + } + + public void Append(object? value, int width) + { + if (value == null) + { + FinishAppend(string.Empty, width); + return; + } + + FinishAppend(value.ToString(), width); + } + + private void FinishAppend(int charsWritten, int width) + { + Length += charsWritten; + + var leftAlign = false; + if (width < 0) + { + width = -width; + leftAlign = true; + } + + int padding = width - charsWritten; + if (padding > 0) + { + if (!Ensure(padding)) + { + return; + } + + if (leftAlign) + { + _chars.Slice(Length, padding).Fill(' '); + } + else + { + int start = Length - charsWritten; + _chars.Slice(start, charsWritten).CopyTo(_chars.Slice(start + padding)); + _chars.Slice(start, padding).Fill(' '); + } + + Length += padding; + } + } + +#if !NETCOREAPP3_1_OR_GREATER + private void FinishAppend(string result, int width) => FinishAppend(result.AsSpan(), width); +#endif + + private void FinishAppend(ReadOnlySpan result, int width) + { + var leftAlign = false; + if (width < 0) + { + width = -width; + leftAlign = true; + } + + int padding = width - result.Length; + int extra = result.Length; + if (padding > 0) + { + extra += padding; + } + + if (!Ensure(extra)) + { + return; + } + + if (padding > 0) + { + if (leftAlign) + { + result.CopyTo(_chars.Slice(Length)); + _chars.Slice(Length + result.Length, padding).Fill(' '); + } + else + { + _chars.Slice(Length, padding).Fill(' '); + result.CopyTo(_chars.Slice(Length + padding)); + } + } + else + { + result.CopyTo(_chars.Slice(Length)); + } + + Length += extra; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool Ensure(int neededCapacity) + { + if (Length <= _chars.Length - neededCapacity) + { + return true; + } + + return Expand(neededCapacity); + } + + private bool Expand(int neededCapacity = 0) + { + if (_fixedCapacity) + { + Overflowed = true; + return false; + } + + if (neededCapacity == 0) + { + neededCapacity = DefaultCapacity; + } + + int newCapacity = _chars.Length + neededCapacity; + + // allocate a new array and copy the existing data to it + var a = ArrayPool.Shared.Rent(newCapacity); + _chars.Slice(0, Length).CopyTo(a); + + if (_rentedBuffer != null) + { + ArrayPool.Shared.Return(_rentedBuffer); + } + + _rentedBuffer = a; + _chars = a; + return true; + } +} diff --git a/src/Shared/Throw/README.md b/src/Shared/Throw/README.md new file mode 100644 index 0000000000..2c9399864e --- /dev/null +++ b/src/Shared/Throw/README.md @@ -0,0 +1,11 @@ +# Throw + +Efficient exception throwing utilities. + +To use this in your project, add the following to your `.csproj` file: + +```xml + + true + +``` diff --git a/src/Shared/Throw/Throw.cs b/src/Shared/Throw/Throw.cs new file mode 100644 index 0000000000..083cf5ae2a --- /dev/null +++ b/src/Shared/Throw/Throw.cs @@ -0,0 +1,974 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +#pragma warning disable CA1716 +namespace Microsoft.Shared.Diagnostics; +#pragma warning restore CA1716 + +/// +/// Defines static methods used to throw exceptions. +/// +/// +/// The main purpose is to reduce code size, improve performance, and standardize exception +/// messages. +/// +[SuppressMessage("Minor Code Smell", "S4136:Method overloads should be grouped together", Justification = "Doesn't work with the region layout")] +[SuppressMessage("Design", "CA1716", Justification = "Not part of an API")] + +#if !SHARED_PROJECT +[ExcludeFromCodeCoverage] +#endif + +internal static class Throw +{ + #region For Object + + /// + /// Throws an if the specified argument is . + /// + /// Argument type to be checked for . + /// Object to be checked for . + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNull] + public static T IfNull([NotNull] T argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument is null) + { + ArgumentNullException(paramName); + } + + return argument; + } + + /// + /// Throws an if the specified argument is , + /// or if the specified member is . + /// + /// Argument type to be checked for . + /// Member type to be checked for . + /// Argument to be checked for . + /// Object member to be checked for . + /// The name of the parameter being checked. + /// The name of the member. + /// The original value of . + /// + /// + /// Throws.IfNullOrMemberNull(myObject, myObject?.MyProperty) + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNull] + public static TMember IfNullOrMemberNull( + [NotNull] TParameter argument, + [NotNull] TMember member, + [CallerArgumentExpression(nameof(argument))] string paramName = "", + [CallerArgumentExpression(nameof(member))] string memberName = "") + { + if (argument is null) + { + ArgumentNullException(paramName); + } + + if (member is null) + { + ArgumentException(paramName, $"Member {memberName} of {paramName} is null"); + } + + return member; + } + + /// + /// Throws an if the specified member is . + /// + /// Argument type. + /// Member type to be checked for . + /// Argument to which member belongs. + /// Object member to be checked for . + /// The name of the parameter being checked. + /// The name of the member. + /// The original value of . + /// + /// + /// Throws.IfMemberNull(myObject, myObject.MyProperty) + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNull] + [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Analyzer isn't seeing the reference to 'argument' in the attribute")] + public static TMember IfMemberNull( + TParameter argument, + [NotNull] TMember member, + [CallerArgumentExpression(nameof(argument))] string paramName = "", + [CallerArgumentExpression(nameof(member))] string memberName = "") + where TParameter : notnull + { + if (member is null) + { + ArgumentException(paramName, $"Member {memberName} of {paramName} is null"); + } + + return member; + } + + #endregion + + #region For String + + /// + /// Throws either an or an + /// if the specified string is or whitespace respectively. + /// + /// String to be checked for or whitespace. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNull] + public static string IfNullOrWhitespace([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { +#if !NETCOREAPP3_1_OR_GREATER + if (argument == null) + { + ArgumentNullException(paramName); + } +#endif + + if (string.IsNullOrWhiteSpace(argument)) + { + if (argument == null) + { + ArgumentNullException(paramName); + } + else + { + ArgumentException(paramName, "Argument is whitespace"); + } + } + + return argument; + } + + /// + /// Throws an if the string is , + /// or if it is empty. + /// + /// String to be checked for or empty. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNull] + public static string IfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { +#if !NETCOREAPP3_1_OR_GREATER + if (argument == null) + { + ArgumentNullException(paramName); + } +#endif + + if (string.IsNullOrEmpty(argument)) + { + if (argument == null) + { + ArgumentNullException(paramName); + } + else + { + ArgumentException(paramName, "Argument is an empty string"); + } + } + + return argument; + } + + #endregion + + #region For Buffer + + /// + /// Throws an if the argument's buffer size is less than the required buffer size. + /// + /// The actual buffer size. + /// The required buffer size. + /// The name of the parameter to be checked. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void IfBufferTooSmall(int bufferSize, int requiredSize, string paramName = "") + { + if (bufferSize < requiredSize) + { + ArgumentException(paramName, $"Buffer too small, needed a size of {requiredSize} but got {bufferSize}"); + } + } + + #endregion + + #region For Enums + + /// + /// Throws an if the enum value is not valid. + /// + /// The argument to evaluate. + /// The name of the parameter being checked. + /// The type of the enumeration. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T IfOutOfRange(T argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + where T : struct, Enum + { +#if NET5_0_OR_GREATER + if (!Enum.IsDefined(argument)) +#else + if (!Enum.IsDefined(typeof(T), argument)) +#endif + { + ArgumentOutOfRangeException(paramName, $"{argument} is an invalid value for enum type {typeof(T)}"); + } + + return argument; + } + + #endregion + + #region For Collections + + /// + /// Throws an if the collection is , + /// or if it is empty. + /// + /// The collection to evaluate. + /// The name of the parameter being checked. + /// The type of objects in the collection. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNull] + + // The method has actually 100% coverage, but due to a bug in the code coverage tool, + // a lower number is reported. Therefore, we temporarily exclude this method + // from the coverage measurements. Once the bug in the code coverage tool is fixed, + // the exclusion attribute can be removed. + [ExcludeFromCodeCoverage] + public static IEnumerable IfNullOrEmpty([NotNull] IEnumerable? argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument == null) + { + ArgumentNullException(paramName); + } + else + { + switch (argument) + { + case ICollection collection: + if (collection.Count == 0) + { + ArgumentException(paramName, "Collection is empty"); + } + + break; + case IReadOnlyCollection readOnlyCollection: + if (readOnlyCollection.Count == 0) + { + ArgumentException(paramName, "Collection is empty"); + } + + break; + default: + using (var enumerator = argument.GetEnumerator()) + { + if (!enumerator.MoveNext()) + { + ArgumentException(paramName, "Collection is empty"); + } + } + + break; + } + } + + return argument; + } + + #endregion + + #region Exceptions + + /// + /// Throws an . + /// + /// The name of the parameter that caused the exception. +#if !NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + [DoesNotReturn] + public static void ArgumentNullException(string paramName) + => throw new ArgumentNullException(paramName); + + /// + /// Throws an . + /// + /// The name of the parameter that caused the exception. + /// A message that describes the error. +#if !NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + [DoesNotReturn] + public static void ArgumentNullException(string paramName, string? message) + => throw new ArgumentNullException(paramName, message); + + /// + /// Throws an . + /// + /// The name of the parameter that caused the exception. +#if !NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + [DoesNotReturn] + public static void ArgumentOutOfRangeException(string paramName) + => throw new ArgumentOutOfRangeException(paramName); + + /// + /// Throws an . + /// + /// The name of the parameter that caused the exception. + /// A message that describes the error. +#if !NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + [DoesNotReturn] + public static void ArgumentOutOfRangeException(string paramName, string? message) + => throw new ArgumentOutOfRangeException(paramName, message); + + /// + /// Throws an . + /// + /// The name of the parameter that caused the exception. + /// The value of the argument that caused this exception. + /// A message that describes the error. +#if !NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + [DoesNotReturn] + public static void ArgumentOutOfRangeException(string paramName, object? actualValue, string? message) + => throw new ArgumentOutOfRangeException(paramName, actualValue, message); + + /// + /// Throws an . + /// + /// The name of the parameter that caused the exception. + /// A message that describes the error. +#if !NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + [DoesNotReturn] + public static void ArgumentException(string paramName, string? message) + => throw new ArgumentException(message, paramName); + + /// + /// Throws an . + /// + /// The name of the parameter that caused the exception. + /// A message that describes the error. + /// The exception that is the cause of the current exception. + /// + /// If the is not a , the current exception is raised in a catch + /// block that handles the inner exception. + /// +#if !NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + [DoesNotReturn] + public static void ArgumentException(string paramName, string? message, Exception? innerException) + => throw new ArgumentException(message, paramName, innerException); + + /// + /// Throws an . + /// + /// A message that describes the error. +#if !NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + [DoesNotReturn] + public static void InvalidOperationException(string message) + => throw new InvalidOperationException(message); + + /// + /// Throws an . + /// + /// A message that describes the error. + /// The exception that is the cause of the current exception. +#if !NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.NoInlining)] +#endif + [DoesNotReturn] + public static void InvalidOperationException(string message, Exception? innerException) + => throw new InvalidOperationException(message, innerException); + + #endregion + + #region For Integer + + /// + /// Throws an if the specified number is less than min. + /// + /// Number to be expected being less than min. + /// The number that must be less than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IfLessThan(int argument, int min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument < min) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater than max. + /// + /// Number to be expected being greater than max. + /// The number that must be greater than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IfGreaterThan(int argument, int max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument > max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is less or equal than min. + /// + /// Number to be expected being less or equal than min. + /// The number that must be less or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IfLessThanOrEqual(int argument, int min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument <= min) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less or equal than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater or equal than max. + /// + /// Number to be expected being greater or equal than max. + /// The number that must be greater or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IfGreaterThanOrEqual(int argument, int max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument >= max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater or equal than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is not in the specified range. + /// + /// Number to be expected being greater or equal than max. + /// The lower bound of the allowed range of argument values. + /// The upper bound of the allowed range of argument values. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IfOutOfRange(int argument, int min, int max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument < min || argument > max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument not in the range [{min}..{max}]"); + } + + return argument; + } + + /// + /// Throws an if the specified number is equal to 0. + /// + /// Number to be expected being not equal to zero. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IfZero(int argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument == 0) + { + ArgumentOutOfRangeException(paramName, "Argument is zero"); + } + + return argument; + } + + #endregion + + #region For Unsigned Integer + + /// + /// Throws an if the specified number is less than min. + /// + /// Number to be expected being less than min. + /// The number that must be less than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint IfLessThan(uint argument, uint min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument < min) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater than max. + /// + /// Number to be expected being greater than max. + /// The number that must be greater than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint IfGreaterThan(uint argument, uint max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument > max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is less or equal than min. + /// + /// Number to be expected being less or equal than min. + /// The number that must be less or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint IfLessThanOrEqual(uint argument, uint min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument <= min) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less or equal than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater or equal than max. + /// + /// Number to be expected being greater or equal than max. + /// The number that must be greater or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint IfGreaterThanOrEqual(uint argument, uint max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument >= max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater or equal than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is not in the specified range. + /// + /// Number to be expected being greater or equal than max. + /// The lower bound of the allowed range of argument values. + /// The upper bound of the allowed range of argument values. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint IfOutOfRange(uint argument, uint min, uint max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument < min || argument > max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument not in the range [{min}..{max}]"); + } + + return argument; + } + + /// + /// Throws an if the specified number is equal to 0. + /// + /// Number to be expected being not equal to zero. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint IfZero(uint argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument == 0U) + { + ArgumentOutOfRangeException(paramName, "Argument is zero"); + } + + return argument; + } + + #endregion + + #region For Long + + /// + /// Throws an if the specified number is less than min. + /// + /// Number to be expected being less than min. + /// The number that must be less than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long IfLessThan(long argument, long min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument < min) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater than max. + /// + /// Number to be expected being greater than max. + /// The number that must be greater than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long IfGreaterThan(long argument, long max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument > max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is less or equal than min. + /// + /// Number to be expected being less or equal than min. + /// The number that must be less or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long IfLessThanOrEqual(long argument, long min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument <= min) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less or equal than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater or equal than max. + /// + /// Number to be expected being greater or equal than max. + /// The number that must be greater or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long IfGreaterThanOrEqual(long argument, long max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument >= max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater or equal than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is not in the specified range. + /// + /// Number to be expected being greater or equal than max. + /// The lower bound of the allowed range of argument values. + /// The upper bound of the allowed range of argument values. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long IfOutOfRange(long argument, long min, long max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument < min || argument > max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument not in the range [{min}..{max}]"); + } + + return argument; + } + + /// + /// Throws an if the specified number is equal to 0. + /// + /// Number to be expected being not equal to zero. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long IfZero(long argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument == 0L) + { + ArgumentOutOfRangeException(paramName, "Argument is zero"); + } + + return argument; + } + + #endregion + + #region For Unsigned Long + + /// + /// Throws an if the specified number is less than min. + /// + /// Number to be expected being less than min. + /// The number that must be less than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong IfLessThan(ulong argument, ulong min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument < min) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater than max. + /// + /// Number to be expected being greater than max. + /// The number that must be greater than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong IfGreaterThan(ulong argument, ulong max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument > max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is less or equal than min. + /// + /// Number to be expected being less or equal than min. + /// The number that must be less or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong IfLessThanOrEqual(ulong argument, ulong min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument <= min) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less or equal than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater or equal than max. + /// + /// Number to be expected being greater or equal than max. + /// The number that must be greater or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong IfGreaterThanOrEqual(ulong argument, ulong max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument >= max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater or equal than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is not in the specified range. + /// + /// Number to be expected being greater or equal than max. + /// The lower bound of the allowed range of argument values. + /// The upper bound of the allowed range of argument values. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong IfOutOfRange(ulong argument, ulong min, ulong max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument < min || argument > max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument not in the range [{min}..{max}]"); + } + + return argument; + } + + /// + /// Throws an if the specified number is equal to 0. + /// + /// Number to be expected being not equal to zero. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong IfZero(ulong argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument == 0UL) + { + ArgumentOutOfRangeException(paramName, "Argument is zero"); + } + + return argument; + } + + #endregion + + #region For Double + + /// + /// Throws an if the specified number is less than min. + /// + /// Number to be expected being less than min. + /// The number that must be less than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double IfLessThan(double argument, double min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument < min) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater than max. + /// + /// Number to be expected being greater than max. + /// The number that must be greater than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double IfGreaterThan(double argument, double max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument > max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is less or equal than min. + /// + /// Number to be expected being less or equal than min. + /// The number that must be less or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double IfLessThanOrEqual(double argument, double min, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument <= min) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument less or equal than minimum value {min}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is greater or equal than max. + /// + /// Number to be expected being greater or equal than max. + /// The number that must be greater or equal than the argument. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double IfGreaterThanOrEqual(double argument, double max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument >= max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument greater or equal than maximum value {max}"); + } + + return argument; + } + + /// + /// Throws an if the specified number is not in the specified range. + /// + /// Number to be expected being greater or equal than max. + /// The lower bound of the allowed range of argument values. + /// The upper bound of the allowed range of argument values. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double IfOutOfRange(double argument, double min, double max, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { + if (argument < min || argument > max) + { + ArgumentOutOfRangeException(paramName, argument, $"Argument not in the range [{min}..{max}]"); + } + + return argument; + } + + /// + /// Throws an if the specified number is equal to 0. + /// + /// Number to be expected being not equal to zero. + /// The name of the parameter being checked. + /// The original value of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double IfZero(double argument, [CallerArgumentExpression(nameof(argument))] string paramName = "") + { +#pragma warning disable S1244 // Floating point numbers should not be tested for equality + if (Math.Abs(argument) == 0.0) +#pragma warning restore S1244 // Floating point numbers should not be tested for equality + { + ArgumentOutOfRangeException(paramName, "Argument is zero"); + } + + return argument; + } + + #endregion +} diff --git a/src/ToBeMoved/DependencyInjection.AutoActivation/AutoActivationExtensions.cs b/src/ToBeMoved/DependencyInjection.AutoActivation/AutoActivationExtensions.cs new file mode 100644 index 0000000000..32ee4777a8 --- /dev/null +++ b/src/ToBeMoved/DependencyInjection.AutoActivation/AutoActivationExtensions.cs @@ -0,0 +1,296 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for automatically activating singletons after application starts. +/// +public static class AutoActivationExtensions +{ + /// + /// Enforces singleton activation at startup time rather then at runtime. + /// + /// The type of the service to add. + /// The to add the service to. + /// A reference to this instance after the operation has completed. + public static IServiceCollection Activate(this IServiceCollection services) + where TService : class + { + _ = Throw.IfNull(services); + + return services.Activate(typeof(TService)); + } + + /// + /// Adds an autoactivated singleton service of the type specified in TService with an implementation + /// type specified in TImplementation using the factory specified in implementationFactory + /// to the specified . + /// + /// The to add the service to. + /// The factory that creates the service. + /// The type of the service to add. + /// The type of the implementation to use. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddActivatedSingleton(this IServiceCollection services, Func implementationFactory) + where TService : class + where TImplementation : class, TService + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(implementationFactory); + + return services.AddSingleton(implementationFactory).Activate(typeof(TService)); + } + + /// + /// Adds an autoactivated singleton service of the type specified in TService with a factory specified + /// in implementationFactory to the specified . + /// + /// The to add the service to. + /// The factory that creates the service. + /// The type of the service to add. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddActivatedSingleton(this IServiceCollection services, Func implementationFactory) + where TService : class + { + return services.AddActivatedSingleton(typeof(TService), implementationFactory); + } + + /// + /// Adds an autoactivated singleton service of the type specified in TService to the specified . + /// + /// The to add the service to. + /// The type of the service to add. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddActivatedSingleton(this IServiceCollection services) + where TService : class + { + return services.AddActivatedSingleton(typeof(TService)); + } + + /// + /// Adds an autoactivated singleton service of the type specified in serviceType to the specified + /// . + /// + /// The to add the service to. + /// The type of the service to register and the implementation to use. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddActivatedSingleton(this IServiceCollection services, Type serviceType) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(serviceType); + + return services.AddSingleton(serviceType).Activate(serviceType); + } + + /// + /// Adds an autoactivated singleton service of the type specified in TService with an implementation + /// type specified in TImplementation to the specified . + /// + /// The to add the service to. + /// The type of the service to add. + /// The type of the implementation to use. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddActivatedSingleton(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + return services.AddActivatedSingleton(typeof(TService), typeof(TImplementation)); + } + + /// + /// Adds an autoactivated singleton service of the type specified in serviceType with a factory + /// specified in implementationFactory to the specified . + /// + /// The to add the service to. + /// The type of the service to register. + /// The factory that creates the service. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddActivatedSingleton(this IServiceCollection services, Type serviceType, Func implementationFactory) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(serviceType); + _ = Throw.IfNull(implementationFactory); + + return services.AddSingleton(serviceType, implementationFactory).Activate(serviceType); + } + + /// + /// Adds an autoactivated singleton service of the type specified in serviceType with an implementation + /// of the type specified in implementationType to the specified . + /// + /// The to add the service to. + /// The type of the service to register. + /// The implementation type of the service. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddActivatedSingleton(this IServiceCollection services, Type serviceType, Type implementationType) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(serviceType); + _ = Throw.IfNull(implementationType); + + return services.AddSingleton(serviceType, implementationType).Activate(serviceType); + } + + /// + /// Adds an autoactivated singleton service of the type specified in serviceType to the specified + /// if the service type hasn't already been registered. + /// + /// The to add the service to. + /// The type of the service to register. + public static void TryAddActivatedSingleton(this IServiceCollection services, Type serviceType) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(serviceType); + + services.TryAddAndActivate(ServiceDescriptor.Singleton(serviceType, serviceType)); + } + + /// + /// Adds an autoactivated singleton service of the type specified in serviceType with an implementation + /// of the type specified in implementationType to the specified + /// if the service type hasn't already been registered. + /// + /// The to add the service to. + /// The type of the service to register. + /// The implementation type of the service. + public static void TryAddActivatedSingleton(this IServiceCollection services, Type serviceType, Type implementationType) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(serviceType); + _ = Throw.IfNull(implementationType); + + services.TryAddAndActivate(ServiceDescriptor.Singleton(serviceType, implementationType)); + } + + /// + /// Adds an autoactivated singleton service of the type specified in serviceType with a factory + /// specified in implementationFactory to the specified + /// if the service type hasn't already been registered. + /// + /// The to add the service to. + /// The type of the service to register. + /// The factory that creates the service. + public static void TryAddActivatedSingleton(this IServiceCollection services, Type serviceType, Func implementationFactory) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(serviceType); + _ = Throw.IfNull(implementationFactory); + + services.TryAddAndActivate(ServiceDescriptor.Singleton(serviceType, implementationFactory)); + } + + /// + /// Adds an autoactivated singleton service of the type specified in TService + /// to the specified + /// if the service type hasn't already been registered. + /// + /// The to add the service to. + /// The type of the service to add. + public static void TryAddActivatedSingleton(this IServiceCollection services) + where TService : class + { + _ = Throw.IfNull(services); + + services.TryAddAndActivate(ServiceDescriptor.Singleton(typeof(TService), typeof(TService))); + } + + /// + /// Adds an autoactivated singleton service of the type specified in TService with an implementation + /// type specified in TImplementation using the factory specified in implementationFactory + /// to the specified + /// if the service type hasn't already been registered. + /// + /// The to add the service to. + /// The type of the service to add. + /// The type of the implementation to use. + public static void TryAddActivatedSingleton(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + _ = Throw.IfNull(services); + + services.TryAddAndActivate(ServiceDescriptor.Singleton(typeof(TService), typeof(TImplementation))); + } + + /// + /// Adds an autoactivated singleton service of the type specified in serviceType with a factory + /// specified in implementationFactory to the specified + /// if the service type hasn't already been registered. + /// + /// The to add the service to. + /// The factory that creates the service. + /// The type of the service to add. + public static void TryAddActivatedSingleton(this IServiceCollection services, Func implementationFactory) + where TService : class + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(implementationFactory); + + services.TryAddAndActivate(ServiceDescriptor.Singleton(typeof(TService), implementationFactory)); + } + + private static void TryAddAndActivate(this IServiceCollection services, ServiceDescriptor descriptor) + { + if (services.Any(d => d.ServiceType == descriptor.ServiceType)) + { + return; + } + + services.Add(descriptor); + _ = services.Activate(descriptor.ServiceType); + } + + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed with [DynamicallyAccessedMembers]")] + private static IServiceCollection Activate(this IServiceCollection services, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type serviceType) + { + _ = services.AddHostedServiceIfNotExist() + .AddOptions() + .Configure(ao => + { + var constructed = typeof(IEnumerable<>).MakeGenericType(serviceType); + if (ao.AutoActivators.Contains(constructed)) + { + return; + } + + if (ao.AutoActivators.Remove(serviceType)) + { + _ = ao.AutoActivators.Add(constructed); + return; + } + + _ = ao.AutoActivators.Add(serviceType); + }); + + return services; + } + + private static IServiceCollection AddHostedServiceIfNotExist(this IServiceCollection services) + { +#if NETFRAMEWORK + var autoActivationHostedServiceType = typeof(AutoActivationHostedService); + + // This loop is needed only for older .NET versions where there's no check + // if the service was already added to the IServiceCollection. + foreach (var service in services) + { + if (service.ImplementationType == autoActivationHostedServiceType) + { + return services; + } + } +#endif + return services.AddHostedService(); + } +} diff --git a/src/ToBeMoved/DependencyInjection.AutoActivation/AutoActivationHostedService.cs b/src/ToBeMoved/DependencyInjection.AutoActivation/AutoActivationHostedService.cs new file mode 100644 index 0000000000..b33c748e44 --- /dev/null +++ b/src/ToBeMoved/DependencyInjection.AutoActivation/AutoActivationHostedService.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.DependencyInjection; + +internal sealed class AutoActivationHostedService : IHostedService +{ + private readonly Type[] _autoActivators; + private readonly IServiceProvider _provider; + + public AutoActivationHostedService(IServiceProvider provider, IOptions options) + { + _provider = provider; + var value = Throw.IfMemberNull(options, options.Value); + + _autoActivators = value.AutoActivators.ToArray(); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + foreach (var singleton in _autoActivators) + { + _ = _provider.GetRequiredService(singleton); + } + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/src/ToBeMoved/DependencyInjection.AutoActivation/AutoActivatorOptions.cs b/src/ToBeMoved/DependencyInjection.AutoActivation/AutoActivatorOptions.cs new file mode 100644 index 0000000000..9f1fab139a --- /dev/null +++ b/src/ToBeMoved/DependencyInjection.AutoActivation/AutoActivatorOptions.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.DependencyInjection; + +internal sealed class AutoActivatorOptions +{ + public HashSet AutoActivators { get; } = new HashSet(); +} diff --git a/src/ToBeMoved/DependencyInjection.AutoActivation/DependencyInjection.AutoActivation.csproj b/src/ToBeMoved/DependencyInjection.AutoActivation/DependencyInjection.AutoActivation.csproj new file mode 100644 index 0000000000..843c9994ab --- /dev/null +++ b/src/ToBeMoved/DependencyInjection.AutoActivation/DependencyInjection.AutoActivation.csproj @@ -0,0 +1,27 @@ + + + + Microsoft.Extensions.DependencyInjection.AutoActivation + Microsoft.Extensions.DependencyInjection + Extensions to auto-activate registered singletons in the dependency injection system. + Fundamentals + Dependency Injection + + + + normal + 66 + 100 + 100 + + + + + + + + + + + + diff --git a/src/ToBeMoved/DependencyInjection.NamedService/DependencyInjection.NamedService.csproj b/src/ToBeMoved/DependencyInjection.NamedService/DependencyInjection.NamedService.csproj new file mode 100644 index 0000000000..43369caae1 --- /dev/null +++ b/src/ToBeMoved/DependencyInjection.NamedService/DependencyInjection.NamedService.csproj @@ -0,0 +1,24 @@ + + + Microsoft.Extensions.DependencyInjection.NamedService + Microsoft.Extensions.DependencyInjection + Extensions to register and resolve named services. + Fundamentals + Configuration + true + + + + dev + 0 + 100 + + + + + + + + + + diff --git a/src/ToBeMoved/DependencyInjection.NamedService/INamedServiceProvider.cs b/src/ToBeMoved/DependencyInjection.NamedService/INamedServiceProvider.cs new file mode 100644 index 0000000000..d0b81b6a85 --- /dev/null +++ b/src/ToBeMoved/DependencyInjection.NamedService/INamedServiceProvider.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Provides a mechanism for retrieving named service objects of the specified type. +/// +/// The type of the service objects to retrieve. +public interface INamedServiceProvider + where TService : class +{ + /// + /// Gets the service object with the specified name. + /// + /// The name of the service object. + /// The object. + /// + /// This method returns the latest registered under the name. + /// + public TService? GetService(string name); + + /// + /// Gets all the service objects with the specified name. + /// + /// The name of the service objects. + /// The collection of objects. + /// + /// This method returns all registered under the name in the order they were registered. + /// + public IEnumerable GetServices(string name); +} diff --git a/src/ToBeMoved/DependencyInjection.NamedService/NamedServiceCollectionExtensions.cs b/src/ToBeMoved/DependencyInjection.NamedService/NamedServiceCollectionExtensions.cs new file mode 100644 index 0000000000..91b8094825 --- /dev/null +++ b/src/ToBeMoved/DependencyInjection.NamedService/NamedServiceCollectionExtensions.cs @@ -0,0 +1,117 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extensions for adding named services to . +/// +public static class NamedServiceCollectionExtensions +{ + /// + /// Adds a singleton named service of the type specific in to the + /// specified . + /// + /// The type of the service to add. + /// The to add the service to. + /// The name of the service. + /// The factory that creates the service. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddNamedSingleton(this IServiceCollection serviceCollection, + string name, Func implementationFactory) + where TService : class + { + _ = Throw.IfNull(serviceCollection); + _ = Throw.IfNullOrEmpty(name); + + _ = serviceCollection.AddOptions>(name) + .Configure(options => + options.Services.Add(NamedServiceDescriptor.Describe( + implementationFactory, + ServiceLifetime.Singleton))); + + serviceCollection.TryAdd(ServiceDescriptor.Singleton(typeof(INamedServiceProvider<>), typeof(NamedServiceProvider<>))); + + return serviceCollection; + } + + /// + /// Adds a singleton named service of the type specific in to the + /// specified . + /// + /// The type of the service to add. + /// The type of the implementation to use. + /// The to add the service to. + /// The name of the service. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddNamedSingleton(this IServiceCollection serviceCollection, + string name) + where TService : class + where TImplementation : TService + { + return serviceCollection.AddNamedSingleton(name, + provider => ActivatorUtilities.CreateInstance(provider, Array.Empty())); + } + + /// + /// Adds a singleton named service of the type specific in to the + /// specified . + /// + /// The type of the service to add. + /// The to add the service to. + /// The name of the service. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddNamedSingleton(this IServiceCollection serviceCollection, + string name) + where TService : class + { + return serviceCollection.AddNamedSingleton(name); + } + + /// + /// Adds a transient named service of the type specific in to the + /// specified . + /// + /// The type of the service to add. + /// The to add the service to. + /// The name of the service. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddNamedTransient(this IServiceCollection serviceCollection, string name) + where TService : class + { + return serviceCollection.AddNamedTransient(name, + provider => ActivatorUtilities.CreateInstance(provider, Array.Empty())); + } + + /// + /// Adds a transient named service of the type specific in to the + /// specified . + /// + /// The type of the service to add. + /// The to add the service to. + /// The name of the service. + /// The factory that creates the service. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddNamedTransient(this IServiceCollection serviceCollection, + string name, Func implementationFactory) + where TService : class + { + _ = Throw.IfNull(serviceCollection); + _ = Throw.IfNullOrEmpty(name); + + _ = serviceCollection.AddOptions>(name) + .Configure(options => + options.Services.Add(NamedServiceDescriptor.Describe( + implementationFactory, + ServiceLifetime.Transient))); + + serviceCollection.TryAdd(ServiceDescriptor.Singleton(typeof(INamedServiceProvider<>), typeof(NamedServiceProvider<>))); + + return serviceCollection; + } +} diff --git a/src/ToBeMoved/DependencyInjection.NamedService/NamedServiceDescriptor.cs b/src/ToBeMoved/DependencyInjection.NamedService/NamedServiceDescriptor.cs new file mode 100644 index 0000000000..b1a7b63d00 --- /dev/null +++ b/src/ToBeMoved/DependencyInjection.NamedService/NamedServiceDescriptor.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.DependencyInjection; + +internal sealed class NamedServiceDescriptor + where TService : class +{ + public NamedServiceDescriptor(Func implementationFactory, ServiceLifetime lifetime) + { + ImplementationFactory = implementationFactory; + Lifetime = lifetime; + } + + public ServiceLifetime Lifetime { get; } + + public Func ImplementationFactory { get; } + + public static NamedServiceDescriptor Describe(Func implementationFactory, ServiceLifetime lifetime) + { + return new NamedServiceDescriptor(implementationFactory, lifetime); + } +} diff --git a/src/ToBeMoved/DependencyInjection.NamedService/NamedServiceProvider.cs b/src/ToBeMoved/DependencyInjection.NamedService/NamedServiceProvider.cs new file mode 100644 index 0000000000..113c1cbf7e --- /dev/null +++ b/src/ToBeMoved/DependencyInjection.NamedService/NamedServiceProvider.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection; + +internal sealed class NamedServiceProvider : INamedServiceProvider + where TService : class +{ + private readonly IServiceProvider _serviceProvider; + private readonly IOptionsMonitor> _optionsMonitor; + private readonly ConcurrentDictionary, Lazy> _cache = new(); + + private readonly Func, Lazy> _factory; + + public NamedServiceProvider(IServiceProvider serviceProvider, + IOptionsMonitor> optionsMonitor) + { + _serviceProvider = serviceProvider; + _optionsMonitor = optionsMonitor; + _factory = CreateTService; + } + + public TService? GetService(string name) + { + var options = _optionsMonitor.Get(name); + int count = options.Services.Count; + if (count == 0) + { + return null; + } + + // the last one wins + var serviceDescriptor = options.Services[count - 1]; + return GetOrCreateTService(serviceDescriptor); + } + + public IEnumerable GetServices(string name) + { + var options = _optionsMonitor.Get(name); + int count = options.Services.Count; + if (count == 0) + { + return Enumerable.Empty(); + } + + var collection = new List(count); + foreach (var serviceDescriptor in options.Services) + { + collection.Add(GetOrCreateTService(serviceDescriptor)); + } + + return collection; + } + + private TService GetOrCreateTService(NamedServiceDescriptor serviceDescriptor) + { + if (_cache.TryGetValue(serviceDescriptor, out var lazy)) + { + return lazy.Value; + } + + if (serviceDescriptor.Lifetime == ServiceLifetime.Transient) + { + return serviceDescriptor.ImplementationFactory(_serviceProvider); + } + + return _cache.GetOrAdd(serviceDescriptor, _factory).Value; + } + + private Lazy CreateTService(NamedServiceDescriptor serviceDescriptor) + { + return new Lazy( + () => serviceDescriptor.ImplementationFactory(_serviceProvider), + LazyThreadSafetyMode.ExecutionAndPublication); + } +} diff --git a/src/ToBeMoved/DependencyInjection.NamedService/NamedServiceProviderExtensions.cs b/src/ToBeMoved/DependencyInjection.NamedService/NamedServiceProviderExtensions.cs new file mode 100644 index 0000000000..4ecccddaf0 --- /dev/null +++ b/src/ToBeMoved/DependencyInjection.NamedService/NamedServiceProviderExtensions.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for getting services from an . +/// +public static class NamedServiceProviderExtensions +{ + /// + /// Get service of type from the . + /// + /// The type of service object to get. + /// The to retrieve the service object from. + /// The name of the service. + /// A service object of type . + /// There is no service of type with the name. + /// + /// This method returns the latest registered under the name. + /// + public static TService GetRequiredService(this INamedServiceProvider provider, string name) + where TService : class + { + _ = Throw.IfNull(provider); + _ = Throw.IfNullOrEmpty(name); + + var service = provider.GetService(name); + if (service == null) + { + Throw.InvalidOperationException($"No service for type '${typeof(TService)}' and name '${name}' has been registered."); + } + + return service; + } +} diff --git a/src/ToBeMoved/DependencyInjection.NamedService/NamedServiceProviderOptions.cs b/src/ToBeMoved/DependencyInjection.NamedService/NamedServiceProviderOptions.cs new file mode 100644 index 0000000000..32ebd74f95 --- /dev/null +++ b/src/ToBeMoved/DependencyInjection.NamedService/NamedServiceProviderOptions.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Extensions.DependencyInjection; + +internal sealed class NamedServiceProviderOptions + where TService : class +{ + public List> Services { get; set; } = new(); +} diff --git a/src/ToBeMoved/DependencyInjection.Pools/DependencyInjectedPolicy.cs b/src/ToBeMoved/DependencyInjection.Pools/DependencyInjectedPolicy.cs new file mode 100644 index 0000000000..3f251b84a6 --- /dev/null +++ b/src/ToBeMoved/DependencyInjection.Pools/DependencyInjectedPolicy.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.Extensions.DependencyInjection.Pools; + +internal sealed class DependencyInjectedPolicy : IPooledObjectPolicy + where TDefinition : class + where TImplementation : class, TDefinition +{ + private readonly IServiceProvider _provider; + private readonly ObjectFactory _factory; + private readonly bool _isResettable; + + public DependencyInjectedPolicy(IServiceProvider provider) + { + _provider = provider; + _factory = ActivatorUtilities.CreateFactory(typeof(TImplementation), Type.EmptyTypes); + _isResettable = typeof(IResettable).IsAssignableFrom(typeof(TImplementation)); + } + + public TDefinition Create() => (TDefinition)_factory(_provider, Array.Empty()); + + public bool Return(TDefinition obj) + { + if (_isResettable) + { + return ((IResettable)obj).TryReset(); + } + + return true; + } +} diff --git a/src/ToBeMoved/DependencyInjection.Pools/DependencyInjection.Pools.csproj b/src/ToBeMoved/DependencyInjection.Pools/DependencyInjection.Pools.csproj new file mode 100644 index 0000000000..7118a2f148 --- /dev/null +++ b/src/ToBeMoved/DependencyInjection.Pools/DependencyInjection.Pools.csproj @@ -0,0 +1,27 @@ + + + Microsoft.Extensions.DependencyInjection.Pools + Microsoft.Extensions.DependencyInjection.Pools + Pools integration into DI container. + Fundamentals + Dependency Injection + true + true + + + + normal + 90 + 60 + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ToBeMoved/DependencyInjection.Pools/DependencyInjectionExtensions.cs b/src/ToBeMoved/DependencyInjection.Pools/DependencyInjectionExtensions.cs new file mode 100644 index 0000000000..337dac0bea --- /dev/null +++ b/src/ToBeMoved/DependencyInjection.Pools/DependencyInjectionExtensions.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.DependencyInjection.Pools; + +/// +/// Extension methods for adding to DI container. +/// +[Experimental] +public static class DependencyInjectionExtensions +{ + /// + /// Adds an and lets DI return scoped instances of T. + /// + /// The to add to. + /// The action used to configure the options of the pool. + /// The type of objects to pool. + /// Provided service collection. + /// When is . + /// + /// The default capacity is 1024. + /// The pooled type instances are obtainable the same way like any other type instances from the DI container. + /// + public static IServiceCollection AddPool(this IServiceCollection services, Action? configure = null) + where TDefinition : class + { + return services.AddPoolInternal(configure); + } + + /// + /// Adds an and let DI return scoped instances of T. + /// + /// The to add to. + /// Configuration of the pool. + /// The type of objects to pool. + /// The type of the implementation to use. + /// Provided service collection. + /// When is . + /// + /// The default capacity is 1024. + /// The pooled type instances are obtainable the same way like any other type instances from the DI container. + /// + public static IServiceCollection AddPool(this IServiceCollection services, Action? configure = null) + where TDefinition : class + where TImplementation : class, TDefinition + { + return services.AddPoolInternal(configure); + } + + /// + /// Configures DI pools. + /// + /// The to add to. + /// The configuration section to bind. + /// Provided service collection. + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(PoolOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed by [DynamicDependency]")] + public static IServiceCollection ConfigurePools(this IServiceCollection services, IConfigurationSection section) + { + foreach (var child in Throw.IfNull(section).GetChildren()) + { + if (!int.TryParse(child.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var capacity)) + { + Throw.ArgumentException(nameof(section), $"Can't parse '{child.Key}' value '{child.Value}' to integer."); + } + + _ = services.Configure(child.Key, options => options.Capacity = capacity); + } + + return services; + } + + private static IServiceCollection AddPoolInternal(this IServiceCollection services, Action? configure) + where TService : class + where TImplementation : class, TService + { + return services + .Configure(typeof(TService).FullName, configure ?? (_ => { })) + .AddSingleton>(provider => + { + var options = provider.GetRequiredService>().Get(typeof(TService).FullName); + return PoolFactory.CreatePool(new DependencyInjectedPolicy(provider)); + }); + } +} diff --git a/src/ToBeMoved/DependencyInjection.Pools/PoolOptions.cs b/src/ToBeMoved/DependencyInjection.Pools/PoolOptions.cs new file mode 100644 index 0000000000..f42d7aa94a --- /dev/null +++ b/src/ToBeMoved/DependencyInjection.Pools/PoolOptions.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.DependencyInjection.Pools; + +/// +/// Contains configuration for pools. +/// +[Experimental] +public sealed class PoolOptions +{ + /// + /// Gets or sets the maximal capacity of the pool. + /// + /// The default is 1024. + public int Capacity { get; set; } = 1024; +} diff --git a/src/ToBeMoved/Directory.Build.props b/src/ToBeMoved/Directory.Build.props new file mode 100644 index 0000000000..3d0c4d6723 --- /dev/null +++ b/src/ToBeMoved/Directory.Build.props @@ -0,0 +1,16 @@ + + + + + $(NetCoreTargetFrameworks)$(ConditionalNet462) + true + + true + true + true + true + true + true + true + + diff --git a/src/ToBeMoved/Hosting.StartupInitialization/Hosting.StartupInitialization.csproj b/src/ToBeMoved/Hosting.StartupInitialization/Hosting.StartupInitialization.csproj new file mode 100644 index 0000000000..5897430738 --- /dev/null +++ b/src/ToBeMoved/Hosting.StartupInitialization/Hosting.StartupInitialization.csproj @@ -0,0 +1,39 @@ + + + + Microsoft.Extensions.Hosting.Testing.StartupInitialization + Microsoft.Extensions.Hosting.Testing + Provides infrastructure to execute asynchronous functions on server startups + Fundamentals + Application Bootstrap + true + true + true + true + + + + normal + 100 + 80 + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ToBeMoved/Hosting.StartupInitialization/IStartupInitializationBuilder.cs b/src/ToBeMoved/Hosting.StartupInitialization/IStartupInitializationBuilder.cs new file mode 100644 index 0000000000..def62bee98 --- /dev/null +++ b/src/ToBeMoved/Hosting.StartupInitialization/IStartupInitializationBuilder.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Hosting.Testing; + +/// +/// Configure service startup initialization. +/// +public interface IStartupInitializationBuilder +{ + /// + /// Gets services used add initializers. + /// + public IServiceCollection Services { get; } + + /// + /// Adds initializer of given type to be executed at service startup. + /// + /// + /// The initializers should be pure functions, i.e. they shouldn't hold any state. + /// They are used in transient manner, and the implementation is not guaranteed to be reachable by GC after startup time. + /// + /// Type of the initializer to add. + /// Instance of for further configuration. + public IStartupInitializationBuilder AddInitializer() + where T : class, IStartupInitializer; + + /// + /// Add ad-hoc initializer to be executed at service startup. + /// + /// + /// Note, that there is no indempotency semantics while calling this API. + /// Therefore, this interface is not recommended for library authors. + /// + /// Initializer to execute. + /// Instance of for further configuration. + public IStartupInitializationBuilder AddInitializer(Func initializer); +} diff --git a/src/ToBeMoved/Hosting.StartupInitialization/IStartupInitializer.cs b/src/ToBeMoved/Hosting.StartupInitialization/IStartupInitializer.cs new file mode 100644 index 0000000000..c45bc1a890 --- /dev/null +++ b/src/ToBeMoved/Hosting.StartupInitialization/IStartupInitializer.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Hosting.Testing; + +/// +/// Holds the initialization function, so we can pass it through . +/// +public interface IStartupInitializer +{ + /// + /// Short startup initialization job. + /// + /// Cancellation token. + /// New . + public Task InitializeAsync(CancellationToken token); +} diff --git a/src/ToBeMoved/Hosting.StartupInitialization/Internal/FunctionDerivedInitializer.cs b/src/ToBeMoved/Hosting.StartupInitialization/Internal/FunctionDerivedInitializer.cs new file mode 100644 index 0000000000..1609635cd9 --- /dev/null +++ b/src/ToBeMoved/Hosting.StartupInitialization/Internal/FunctionDerivedInitializer.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Hosting.Testing.Internal; + +internal sealed class FunctionDerivedInitializer : IStartupInitializer +{ + private readonly Func _action; + private readonly IServiceProvider _provider; + + public FunctionDerivedInitializer(IServiceProvider provider, Func action) + { + _provider = provider; + _action = action; + } + + public Task InitializeAsync(CancellationToken token) + => _action(_provider, token); +} diff --git a/src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupHostedService.cs b/src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupHostedService.cs new file mode 100644 index 0000000000..69da75a432 --- /dev/null +++ b/src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupHostedService.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Text; + +namespace Microsoft.Extensions.Hosting.Testing.Internal; + +internal sealed class StartupHostedService : IHostedService +{ + internal readonly TimeSpan Timeout; + + private const string TimeoutMessageTemplate = "Exceeded maximum server initialization time of {0}. Adjust {1} or split your work into smaller chunks."; + private static readonly CompositeFormat _timeoutMessage = CompositeFormat.Parse(TimeoutMessageTemplate); + private readonly TimeProvider _timeProvider; + private IStartupInitializer[] _initializers; + + public StartupHostedService(IOptions options, + IEnumerable initializers, TimeProvider? timeProvider = null, IDebuggerState? debugger = null) + { + Timeout = Throw.IfMemberNull(options, options.Value).Timeout; + _initializers = initializers.ToArray(); + _timeProvider = timeProvider ?? TimeProvider.System; + + if (debugger?.IsAttached ?? DebuggerState.System.IsAttached) + { + Timeout = System.Threading.Timeout.InfiniteTimeSpan; + } + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + using var tcts = _timeProvider.CreateCancellationTokenSource(Timeout); + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, tcts.Token); + cts.Token.ThrowIfCancellationRequested(); + + var tasks = _initializers.Select(initializer => initializer.InitializeAsync(cts.Token)); + + try + { + await Task.WhenAll(tasks).ConfigureAwait(false); + } + catch (TaskCanceledException e) when (!cancellationToken.IsCancellationRequested) + { + throw new TaskCanceledException( + message: _timeoutMessage.Format(CultureInfo.InvariantCulture, Timeout, nameof(StartupInitializationOptions)), + innerException: e); + } + + // StartupHostedService will be in the memory for the lifetime of the process. + // Looking at codebase, startup initializers are often holding many objects, so to allow GC to trace less of them, + // we are allowing to collect the initializers. + _initializers = Array.Empty(); + } + + public Task StopAsync(CancellationToken cancellationToken) + => Task.CompletedTask; +} diff --git a/src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupInitializationBuilder.cs b/src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupInitializationBuilder.cs new file mode 100644 index 0000000000..6c70493a12 --- /dev/null +++ b/src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupInitializationBuilder.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Hosting.Testing.Internal; + +/// +/// Builds server initialization phase. +/// +internal sealed class StartupInitializationBuilder : IStartupInitializationBuilder +{ + /// + /// Gets services used to configure initializers. + /// + public IServiceCollection Services { get; } + + /// + /// Initializes a new instance of the class. + /// + /// Service collection used for configuration. + public StartupInitializationBuilder(IServiceCollection services) + { + Services = services; + + RegisterHostedService(services); + _ = Services.AddValidatedOptions(); + } + + /// + /// Adds initializer of given type to be executed at service startup. + /// + /// Type of the initializer to add. + /// Instance of for further configuration. + public IStartupInitializationBuilder AddInitializer() + where T : class, IStartupInitializer + { + Services.TryAddTransient(); + + return this; + } + + /// + public IStartupInitializationBuilder AddInitializer(Func initializer) + { + _ = Throw.IfNull(initializer); + + _ = Services.AddTransient(provider => new FunctionDerivedInitializer(provider, initializer)); + + return this; + } + + private static void RegisterHostedService(IServiceCollection services) + { + if (services.Count != 0 && services[0].ImplementationType == typeof(StartupHostedService)) + { + return; + } + + services + .RemoveAll() + .Insert(0, ServiceDescriptor.Singleton()); + } +} diff --git a/src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupInitializationOptionsValidator.cs b/src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupInitializationOptionsValidator.cs new file mode 100644 index 0000000000..a15058129e --- /dev/null +++ b/src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupInitializationOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Hosting.Testing.Internal; + +[OptionsValidator] +internal sealed partial class StartupInitializationOptionsValidator : IValidateOptions +{ +} diff --git a/src/ToBeMoved/Hosting.StartupInitialization/StartupInitializationExtensions.cs b/src/ToBeMoved/Hosting.StartupInitialization/StartupInitializationExtensions.cs new file mode 100644 index 0000000000..31276de6c9 --- /dev/null +++ b/src/ToBeMoved/Hosting.StartupInitialization/StartupInitializationExtensions.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting.Testing.Internal; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Hosting.Testing; + +/// +/// Extensions for configuring startup initialization. +/// +public static class StartupInitializationExtensions +{ + /// + /// Adds function that will be executed before application starts. + /// + /// + /// Use it for one time initialization logic. + /// Sequence of execution is not guaranteed. + /// + /// Service collection use to register initialization function. + /// Services passed for further configuration. + public static IStartupInitializationBuilder AddStartupInitialization(this IServiceCollection services) + { + _ = Throw.IfNull(services); + + return new StartupInitializationBuilder(services); + } + + /// + /// Adds function that will be executed before application starts. + /// + /// + /// Use it for one time initialization logic. + /// Sequence of execution is not guaranteed. + /// + /// Service collection use to register initialization function. + /// Configure startup initializers. + /// Services passed for further configuration. + public static IStartupInitializationBuilder AddStartupInitialization(this IServiceCollection services, Action configure) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(configure); + + _ = services.AddValidatedOptions() + .Configure(configure); + + return new StartupInitializationBuilder(services); + } + + /// + /// Adds function that will be executed before application starts. + /// + /// + /// Use it for one time initialization logic. + /// Sequence of execution is not guaranteed. + /// + /// Service collection use to register initialization function. + /// Configure startup initializers with config. + /// Services passed for further configuration. + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(StartupInitializationOptions))] + [UnconditionalSuppressMessage("Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed by [DynamicDependency]")] + public static IStartupInitializationBuilder AddStartupInitialization(this IServiceCollection services, IConfigurationSection section) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(section); + + _ = services.AddValidatedOptions(); + _ = services.Configure(section); + + return new StartupInitializationBuilder(services); + } +} diff --git a/src/ToBeMoved/Hosting.StartupInitialization/StartupInitializationOptions.cs b/src/ToBeMoved/Hosting.StartupInitialization/StartupInitializationOptions.cs new file mode 100644 index 0000000000..5c501f7ae0 --- /dev/null +++ b/src/ToBeMoved/Hosting.StartupInitialization/StartupInitializationOptions.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Data.Validation; + +namespace Microsoft.Extensions.Hosting.Testing; + +/// +/// Configures startup initialization logic. +/// +public class StartupInitializationOptions +{ + /// + /// Gets or sets maximum time allowed for initialization logic. + /// + /// + /// Default set to 30 seconds. + /// + [TimeSpan("00:00:05", "01:00:00")] + public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30); +} diff --git a/src/ToBeMoved/HttpClient.SocketHandling/HttpClient.SocketHandling.csproj b/src/ToBeMoved/HttpClient.SocketHandling/HttpClient.SocketHandling.csproj new file mode 100644 index 0000000000..288becef1e --- /dev/null +++ b/src/ToBeMoved/HttpClient.SocketHandling/HttpClient.SocketHandling.csproj @@ -0,0 +1,33 @@ + + + + Microsoft.Extensions.HttpClient.SocketHandling + Microsoft.Extensions.HttpClient.SocketHandling + HttpClientBuilder extensions adding SocketHttpHandler with sensible defaults. + Fundamentals + HTTP Processing + $(NetCoreTargetFrameworks) + true + true + + + + normal + 100 + 100 + + + + + + + + + + + + + + + + diff --git a/src/ToBeMoved/HttpClient.SocketHandling/HttpClientSocketHandlingExtensions.cs b/src/ToBeMoved/HttpClient.SocketHandling/HttpClientSocketHandlingExtensions.cs new file mode 100644 index 0000000000..45b35ce471 --- /dev/null +++ b/src/ToBeMoved/HttpClient.SocketHandling/HttpClientSocketHandlingExtensions.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.HttpClient.SocketHandling; + +/// +/// Extension methods for configuring an . +/// +public static class HttpClientSocketHandlingExtensions +{ + /// + /// Adds a delegate that will set as the primary + /// for a named . + /// + /// The . + /// The given instance to allow method chaining. + public static IHttpClientBuilder AddSocketsHttpHandler(this IHttpClientBuilder builder) + { + _ = Throw.IfNull(builder); + + var builderName = builder.Name; + _ = builder.ConfigurePrimaryHttpMessageHandler(provider => + { + var optionsMonitor = provider.GetRequiredService>(); + var options = optionsMonitor.Get(builderName); + return new SocketsHttpHandler + { + AutomaticDecompression = options.AutomaticDecompression, + AllowAutoRedirect = options.AllowAutoRedirect, + ConnectTimeout = options.ConnectTimeout, + MaxConnectionsPerServer = options.MaxConnectionsPerServer, + PooledConnectionLifetime = options.PooledConnectionLifetime, + PooledConnectionIdleTimeout = options.PooledConnectionIdleTimeout, +#if NET5_0_OR_GREATER + KeepAlivePingDelay = options.KeepAlivePingDelay, + KeepAlivePingTimeout = options.KeepAlivePingTimeout, +#endif + UseCookies = options.UseCookies + }; + }); + + return builder; + } + + /// + /// Adds a delegate that will set as the primary + /// for a named . + /// + /// The . + /// Configure using a instance. + /// The given instance to allow method chaining. + public static IHttpClientBuilder AddSocketsHttpHandler(this IHttpClientBuilder builder, Action configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNull(configure); + + _ = builder.AddSocketsHttpHandler(); + configure(new SocketsHttpHandlerBuilder(builder)); + + return builder; + } +} diff --git a/src/ToBeMoved/HttpClient.SocketHandling/SocketsHttpHandlerBuilder.cs b/src/ToBeMoved/HttpClient.SocketHandling/SocketsHttpHandlerBuilder.cs new file mode 100644 index 0000000000..027e1f2544 --- /dev/null +++ b/src/ToBeMoved/HttpClient.SocketHandling/SocketsHttpHandlerBuilder.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.HttpClient.SocketHandling; + +/// +/// A builder for configuring named instances. +/// +public class SocketsHttpHandlerBuilder +{ + /// + /// Initializes a new instance of the class. + /// + /// The . + public SocketsHttpHandlerBuilder(IHttpClientBuilder builder) + { + _ = Throw.IfNull(builder); + + Name = builder.Name; + Services = builder.Services; + } + + /// + /// Gets the name of . + /// + public string Name { get; } + + /// + /// Gets services collection. + /// + public IServiceCollection Services { get; } + + /// + /// Adds a delegate that will execute the action on the primary handler. + /// + /// The delegate to execute. + /// The . + public SocketsHttpHandlerBuilder ConfigureHandler(Action configure) + { + _ = Services.Configure(Name, + options => + { + options.HttpMessageHandlerBuilderActions.Add( + item => configure((item.PrimaryHandler as SocketsHttpHandler)!)); + }); + + return this; + } + + /// + /// Adds a delegate that will execute the action on the primary handler. + /// + /// The delegate to execute. + /// The . + public SocketsHttpHandlerBuilder ConfigureHandler(Action configure) + { + _ = Services.Configure( + Name, options => + { + options.HttpMessageHandlerBuilderActions.Add(item => configure( + item.Services, + (item.PrimaryHandler as SocketsHttpHandler)!)); + }); + + return this; + } + + /// + /// Adds a delegate that will set as the primary + /// for a named and will use to configure it. + /// + /// Configuration for . + /// The . + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(SocketsHttpHandlerOptions))] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed by [DynamicDependency]")] + public SocketsHttpHandlerBuilder ConfigureOptions(IConfigurationSection section) + { + _ = Throw.IfNull(section); + + _ = Services + .Configure(Name, section) + .AddValidatedOptions(); + + return this; + } + + /// + /// Adds a delegate that will set as the primary + /// for a named and will use the delegate to configure it. + /// + /// Configuration for . + /// The . + public SocketsHttpHandlerBuilder ConfigureOptions(Action configure) + { + _ = Throw.IfNull(configure); + _ = Services.Configure(Name, configure); + return this; + } + + /// + /// Disable verification of remote certificate on SSL/TLS connections. + /// + /// The . + [SuppressMessage("Security", "CA5359:Do Not Disable Certificate Validation", Justification = "Intentional")] + public SocketsHttpHandlerBuilder DisableRemoteCertificateValidation() + { + return ConfigureHandler((_, handler) => + { + handler.SslOptions.RemoteCertificateValidationCallback = (_, _, _, _) => true; + }); + } + + /// + /// Adds a delegate to set a single client certificate for all remote endpoints. + /// + /// The function to fetch the client certificate instance. + /// The . + public SocketsHttpHandlerBuilder ConfigureClientCertificate(Func clientCertificate) + { + _ = Throw.IfNull(clientCertificate); + + return ConfigureHandler((provider, handler) => + { + var x509Certificate2 = clientCertificate(provider); + + if (x509Certificate2 is null) + { + throw new InvalidDataException( + $"The parameter {nameof(clientCertificate)} returned null when called."); + } + + handler.SslOptions.LocalCertificateSelectionCallback = (_, _, _, _, _) => x509Certificate2; + }); + } +} diff --git a/src/ToBeMoved/HttpClient.SocketHandling/SocketsHttpHandlerOptions.cs b/src/ToBeMoved/HttpClient.SocketHandling/SocketsHttpHandlerOptions.cs new file mode 100644 index 0000000000..84c6d228c1 --- /dev/null +++ b/src/ToBeMoved/HttpClient.SocketHandling/SocketsHttpHandlerOptions.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Net; +using System.Net.Http; +using Microsoft.Shared.Data.Validation; + +namespace Microsoft.Extensions.HttpClient.SocketHandling; + +/// Provides a state bag of settings for configuring . +public class SocketsHttpHandlerOptions +{ + private const int MaxConnectionsPerServerUpperLimit = 100_000; + private static readonly TimeSpan _defaultConnectTimeout = TimeSpan.FromSeconds(10); + private static readonly TimeSpan _defaultConnectionLifetime = TimeSpan.FromMinutes(5); + private static readonly TimeSpan _defaultConnectionIdleTimeout = TimeSpan.FromSeconds(110); +#if NET5_0_OR_GREATER + private static readonly TimeSpan _defaultKeepAlivePingDelay = TimeSpan.FromMinutes(1); + private static readonly TimeSpan _defaultKeepAlivePingTimeout = TimeSpan.FromSeconds(30); +#endif + + /// + /// Gets or sets a value indicating whether to automatically follow redirection responses. + /// + /// + /// Default set to false. + /// + public bool AllowAutoRedirect { get; set; } + + /// + /// Gets or sets a value indicating whether to use cookies when sending requests. + /// + /// + /// Default set to false. + /// + public bool UseCookies { get; set; } + + /// + /// Gets or sets the maximum number of concurrent connections (per server endpoint) allowed when making requests. + /// + /// + /// Default set to `100000`. + /// + [Range(1, MaxConnectionsPerServerUpperLimit)] + public int MaxConnectionsPerServer { get; set; } = MaxConnectionsPerServerUpperLimit; + + /// + /// Gets or sets the type of decompression method used by the handler for automatic decompression of the HTTP content response. + /// + /// + /// Default set to `All`. + /// + public DecompressionMethods AutomaticDecompression { get; set; } = DecompressionMethods.All; + + /// + /// Gets or sets the length of time (in seconds) to wait for a connection to the server before terminating the attempt and generating an error. + /// + /// + /// Default set to 10 seconds. 100 minutes is the max timeout value in Azure SLB. + /// + [TimeSpan("00:00:05", "00:05:00")] + public TimeSpan ConnectTimeout { get; set; } = _defaultConnectTimeout; + + /// + /// Gets or sets how long a connection can be in the pool to be considered reusable. + /// + /// + /// Default set to 5 minutes. + /// + [TimeSpan("00:00:01", "00:15:00")] + public TimeSpan PooledConnectionLifetime { get; set; } = _defaultConnectionLifetime; + + /// + /// Gets or sets how long a connection can be idle in the pool to be considered reusable. + /// + /// + /// Default set to 3 minutes. + /// + [TimeSpan("00:00:01", "01:40:00")] + public TimeSpan PooledConnectionIdleTimeout { get; set; } = _defaultConnectionIdleTimeout; + +#if NET5_0_OR_GREATER + /// + /// Gets or sets the keep alive ping delay. + /// + /// + /// Default set to 1 minute. + /// + [TimeSpan("00:00:01", "01:00:00")] + public TimeSpan KeepAlivePingDelay { get; set; } = _defaultKeepAlivePingDelay; + + /// + /// Gets or sets the keep alive ping timeout. + /// + /// + /// Default set to 30 seconds. + /// + [TimeSpan("00:00:01", "00:05:00")] + public TimeSpan KeepAlivePingTimeout { get; set; } = _defaultKeepAlivePingTimeout; +#endif +} diff --git a/src/ToBeMoved/HttpClient.SocketHandling/SocketsHttpHandlerOptionsValidator.cs b/src/ToBeMoved/HttpClient.SocketHandling/SocketsHttpHandlerOptionsValidator.cs new file mode 100644 index 0000000000..c5c343d8ef --- /dev/null +++ b/src/ToBeMoved/HttpClient.SocketHandling/SocketsHttpHandlerOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.HttpClient.SocketHandling; + +[OptionsValidator] +internal sealed partial class SocketsHttpHandlerOptionsValidator : IValidateOptions +{ +} diff --git a/src/ToBeRemoved/Directory.Build.props b/src/ToBeRemoved/Directory.Build.props new file mode 100644 index 0000000000..c41942ee0a --- /dev/null +++ b/src/ToBeRemoved/Directory.Build.props @@ -0,0 +1,18 @@ + + + + + $(NetCoreTargetFrameworks)$(ConditionalNet462) + true + + true + true + true + true + true + true + true + + true + + diff --git a/src/ToBeRemoved/Options.ValidateOnStart/Options.ValidateOnStart.csproj b/src/ToBeRemoved/Options.ValidateOnStart/Options.ValidateOnStart.csproj new file mode 100644 index 0000000000..a21bb50fed --- /dev/null +++ b/src/ToBeRemoved/Options.ValidateOnStart/Options.ValidateOnStart.csproj @@ -0,0 +1,31 @@ + + + Microsoft.Extensions.Options.ValidateOnStart + Microsoft.Extensions.Options.ValidateOnStart + Support for extended option validation. + Fundamentals + Configuration + true + true + true + + + + normal + 100 + 95 + 92 + + + + + + + + + + + + + + diff --git a/src/ToBeRemoved/Options.ValidateOnStart/OptionsBuilderExtensions.cs b/src/ToBeRemoved/Options.ValidateOnStart/OptionsBuilderExtensions.cs new file mode 100644 index 0000000000..af84fd9be0 --- /dev/null +++ b/src/ToBeRemoved/Options.ValidateOnStart/OptionsBuilderExtensions.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Options.Validation; + +/// +/// Extension methods for adding configuration related options services to the DI container via . +/// +public static class OptionsBuilderExtensions +{ + /// + /// Adds named options that are automatically validated during startup using a built-in validator. + /// + /// Service collection. + /// Name of the options. + /// Options to validate. + /// The so that additional calls can be chained. + /// + /// We recommend using custom generated validator. + /// + public static OptionsBuilder AddValidatedOptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>( + this IServiceCollection services, + string? name = null) + where TOptions : class + { + _ = Throw.IfNull(services); + + _ = services.AddOptions(); + + return new OptionsBuilder(services, name ?? Microsoft.Extensions.Options.Options.DefaultName) + .ValidateOnStart(); + } + + /// + /// Adds named options that are automatically validated during startup using a custom validator. + /// + /// Service collection. + /// Name of the options. + /// Options to validate. + /// Validator to use. + /// The so that additional calls can be chained. + public static OptionsBuilder AddValidatedOptions< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TValidateOptions>( + this IServiceCollection services, + string? name = null) + where TOptions : class + where TValidateOptions : class, IValidateOptions + { + _ = Throw.IfNull(services); + + services + .AddOptions() + .TryAddEnumerable(ServiceDescriptor.Singleton, TValidateOptions>()); + + return new OptionsBuilder(services, name ?? Microsoft.Extensions.Options.Options.DefaultName) + .ValidateOnStart(); + } + +#if !NET6_0_OR_GREATER + /// + /// Enforces options validation check in startup time rather then in runtime. + /// + /// Options to validate. + /// The to configure options instance. + /// The so that additional calls can be chained. + private static OptionsBuilder ValidateOnStart<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>( + this OptionsBuilder optionsBuilder) + where TOptions : class + { + _ = Throw.IfNull(optionsBuilder); + + var validatorSetter = new ValidatorSetter(optionsBuilder); + + // This will only add the hosted service once. + _ = optionsBuilder.Services.AddHostedService() + .AddOptions() +#pragma warning disable R9A034 // Optimize method group use to avoid allocations + .Configure>(validatorSetter.SetValidator); +#pragma warning restore R9A034 // Optimize method group use to avoid allocations + + return optionsBuilder; + } + + // This is a workaround. Originally it was implemented as a lambda expression in the ValidateOnStart + // method above. + // After trim analysis was enabled, compiler was not able to correctly propagate the DynamicallyAccessedMembers + // attribute value of the TOptions generic parameter in ValidateOnStart into the lambda expression, + // which resulted in trim analysis warnings (although there was no reason for them actually). + internal sealed class ValidatorSetter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions> + where TOptions : class + { + private readonly OptionsBuilder _optionsBuilder; + internal ValidatorSetter(OptionsBuilder optionsBuilder) + { + _optionsBuilder = optionsBuilder; + } + + internal void SetValidator(ValidatorOptions vo, IOptionsMonitor options) + { + // This adds an action that resolves the options value to force evaluation + // We don't care about the result as duplicates aren't important + vo.Validators[(typeof(TOptions), _optionsBuilder.Name)] = () => options.Get(_optionsBuilder.Name); + } + } +#endif +} diff --git a/src/ToBeRemoved/Options.ValidateOnStart/ValidateOptionsResultExtensions.cs b/src/ToBeRemoved/Options.ValidateOnStart/ValidateOptionsResultExtensions.cs new file mode 100644 index 0000000000..ba86573af8 --- /dev/null +++ b/src/ToBeRemoved/Options.ValidateOnStart/ValidateOptionsResultExtensions.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Options.Validation; + +/// +/// Extension methods for helping with instances. +/// +public static class ValidateOptionsResultExtensions +{ + /// + /// Throws a if the given result indicates a failure. + /// + /// The result value to inspect. + /// Thrown when the result indicates a failure. + public static void ThrowIfFailed(this ValidateOptionsResult result) + { + _ = Throw.IfNull(result); + + if (result.Failed) + { + throw new ValidationException(result.FailureMessage); + } + } +} diff --git a/src/ToBeRemoved/Options.ValidateOnStart/ValidationHostedService.cs b/src/ToBeRemoved/Options.ValidateOnStart/ValidationHostedService.cs new file mode 100644 index 0000000000..452f7102e9 --- /dev/null +++ b/src/ToBeRemoved/Options.ValidateOnStart/ValidationHostedService.cs @@ -0,0 +1,119 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !NET6_0_OR_GREATER + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Pools; +using Microsoft.Shared.Text; + +namespace Microsoft.Extensions.Options.Validation; + +internal sealed class ValidationHostedService : IHostedService +{ + internal const string FriendlyMessageTemplate = "Option instance of type '{0}' with name '{1}' is invalid because: '{2}'. Failed members: {3}"; + internal const string MemberSeparator = "; "; + internal const string Unknown = "Unknown"; + + private static readonly CompositeFormat _friendlyMessage = CompositeFormat.Parse(FriendlyMessageTemplate); + private readonly FrozenDictionary<(Type, string), Action> _validators; + + public ValidationHostedService(IOptions options) + { + _ = Throw.IfMemberNull(options, options.Value); + + if (options.Value.Validators.Count == 0) + { + Throw.ArgumentException(nameof(options), "No validators specified"); + } + +#if FIXME +// FIXME: this should be set to true, but this currently bafs as of 04/03/2023 +#endif + _validators = options.Value.Validators.ToFrozenDictionary(optimizeForReading: false); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + List? exceptions = null; + + foreach (var pair in _validators) + { + cancellationToken.ThrowIfCancellationRequested(); + + var (optionsType, optionsName) = pair.Key; + var validator = pair.Value; + + try + { + validator(); + } + catch (ValidationException exception) + { + exceptions ??= new(); + var friendlyMessage = GetFriendlyMessage(exception, optionsType, optionsName); + + exceptions.Add(new OptionsValidationException(optionsName, optionsType, new[] { friendlyMessage })); + } + catch (OptionsValidationException exception) + { + exceptions ??= new(); + + exceptions.Add(exception); + } + } + + if (exceptions != null) + { + if (exceptions.Count == 1) + { + // Rethrow if it's a single error + ExceptionDispatchInfo.Capture(exceptions[0]).Throw(); + } + else + { + // Aggregate if we have many errors + throw new AggregateException(exceptions); + } + } + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + private static string GetFriendlyMessage(ValidationException exception, Type optionsType, string optionsName) + { + var invalidMembers = PoolFactory.SharedStringBuilderPool.Get(); + + try + { + foreach (var invalidMember in exception.ValidationResult.MemberNames) + { + _ = invalidMembers + .Append(invalidMember) + .Append(MemberSeparator); + } + + _ = invalidMembers.Length != 0 ? invalidMembers.Remove(invalidMembers.Length - MemberSeparator.Length, MemberSeparator.Length) : invalidMembers.Append(Unknown); + + return _friendlyMessage.Format(CultureInfo.InvariantCulture, optionsType.FullName, optionsName, exception.ValidationResult.ErrorMessage, invalidMembers); + } + finally + { + PoolFactory.SharedStringBuilderPool.Return(invalidMembers); + } + } +} + +#endif diff --git a/src/ToBeRemoved/Options.ValidateOnStart/ValidatorOptions.cs b/src/ToBeRemoved/Options.ValidateOnStart/ValidatorOptions.cs new file mode 100644 index 0000000000..47df0a8024 --- /dev/null +++ b/src/ToBeRemoved/Options.ValidateOnStart/ValidatorOptions.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !NET6_0_OR_GREATER + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Options.Validation; + +internal sealed class ValidatorOptions +{ + /// + /// The key maps to the tuple with (a) type of TOptions in and (b) name of options. + /// The value is a method that accesses the property in order to force evaluation of + /// the options type. + /// Default value is an empty . + /// + public Dictionary<(Type optionsType, string optionsName), Action> Validators { get; } = new(); +} + +#endif diff --git a/start-code.cmd b/start-code.cmd new file mode 100644 index 0000000000..b2f9a14604 --- /dev/null +++ b/start-code.cmd @@ -0,0 +1,34 @@ +@ECHO OFF +SETLOCAL + +:: This command launches a Visual Studio Code with environment variables required to use a local version of the .NET Core SDK. + +FOR /f "delims=" %%a IN ('where.exe code') DO @SET vscode=%%a& GOTO break +:break + +IF ["%vscode%"] == [""] ( + echo [ERROR] Visual Studio Code is not installed or can't be found. + exit /b 1 +) + +:: This tells .NET Core to use the same dotnet.exe that build scripts use +SET DOTNET_ROOT=%~dp0.dotnet +SET DOTNET_ROOT(x86)=%~dp0.dotnet\x86 + +:: This tells .NET Core not to go looking for .NET Core in other places +SET DOTNET_MULTILEVEL_LOOKUP=0 + +:: Put our local dotnet.exe on PATH first so Visual Studio knows which one to use +SET PATH=%DOTNET_ROOT%;%PATH% + +IF NOT EXIST "%DOTNET_ROOT%\dotnet.exe" ( + echo [ERROR] .NET SDK has not yet been installed. Run `%~dp0restore.cmd` to install the toolset. + exit /b 1 +) + +IF ["%~1"] == [""] GOTO noargs +"%vscode%" %* +exit /b 1 + +:noargs +"%vscode%" "." diff --git a/start-code.sh b/start-code.sh new file mode 100755 index 0000000000..aa17600387 --- /dev/null +++ b/start-code.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# This tells .NET Core to use the same dotnet.exe that build scripts use +export DOTNET_ROOT=$DIR/.dotnet + +# This tells .NET Core not to go looking for .NET Core in other places +export DOTNET_MULTILEVEL_LOOKUP=0 + +# Put our local dotnet on PATH first so the SDK knows which one to use +export PATH=$DOTNET_ROOT:$PATH + +if [ ! -e $DOTNET_ROOT/dotnet ]; then + echo "[ERROR] .NET SDK has not yet been installed. Run ./restore.sh to install tools" + exit -1 +fi + +if [[ $# < 1 ]] +then + # Perform restore and build, if no args are supplied. + set -- '.'; +fi + +code "$@" + diff --git a/start-vs.cmd b/start-vs.cmd new file mode 100644 index 0000000000..322db7f134 --- /dev/null +++ b/start-vs.cmd @@ -0,0 +1,42 @@ +@echo off +setlocal enabledelayedexpansion + +:: This command launches a Visual Studio solution with environment variables required to use a local version of the .NET Core SDK. + +:: This tells .NET Core to use the same dotnet.exe that build scripts use +set DOTNET_ROOT=%~dp0.dotnet +set DOTNET_ROOT(x86)=%~dp0.dotnet\x86 + +:: This tells .NET Core not to go looking for .NET Core in other places +set DOTNET_MULTILEVEL_LOOKUP=0 + +:: Put our local dotnet.exe on PATH first so Visual Studio knows which one to use +set PATH=%DOTNET_ROOT%;%PATH% + +call restore.cmd + +if not exist "%DOTNET_ROOT%\dotnet.exe" ( + echo [ERROR] .NET Core has not yet been installed. Run `%~dp0restore.cmd` to install tools + exit /b 1 +) + +:: Prefer the VS in the developer command prompt if we're in one, followed by whatever shows up in the current search path. +set "DEVENV=%DevEnvDir%devenv.exe" + +set SLN=SDK.sln + +if exist "%DEVENV%" ( + :: Fully qualified works + set "COMMAND=start "" /B "%ComSpec%" /S /C ""%DEVENV%" "%~dp0%SLN%""" +) else ( + where devenv.exe /Q + if !errorlevel! equ 0 ( + :: On the PATH, use that. + set "COMMAND=start "" /B "%ComSpec%" /S /C "devenv.exe "%~dp0%SLN%""" + ) else ( + :: Can't find devenv.exe, let file associations take care of it + set "COMMAND=start /B .\%SLN%" + ) +) + +%COMMAND% diff --git a/test/.editorconfig b/test/.editorconfig new file mode 100644 index 0000000000..492f32e2d5 --- /dev/null +++ b/test/.editorconfig @@ -0,0 +1,7450 @@ +# Created by the R9 diagnostic config generator +# Generated : 2023-02-20 04:52:14Z +# Attributes: general, test +# Analyzers : ILLink.RoslynAnalyzer, Internal.Analyzers, Microsoft.AspNetCore.App.Analyzers, Microsoft.AspNetCore.Components.Analyzers, Microsoft.CodeAnalysis.CodeStyle, Microsoft.CodeAnalysis.CSharp.CodeStyle, Microsoft.CodeAnalysis.CSharp.NetAnalyzers, Microsoft.CodeAnalysis.NetAnalyzers, Microsoft.CPR.Standard.Analyzers.Basic.R3, Microsoft.R9.Analyzers.Roslyn4.0, Microsoft.VisualStudio.Threading.Analyzers, Microsoft.VisualStudio.Threading.Analyzers.CSharp, SonarAnalyzer.CSharp, StyleCop.Analyzers, xunit.analyzers + +[*.cs] + +# Title : Do not use model binding attributes with route handlers +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0003.severity = warning + +# Title : Do not use action results with route handlers +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0004.severity = warning + +# Title : Do not place attribute on method called by route handler lambda +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0005.severity = warning + +# Title : Do not use non-literal sequence numbers +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0006.severity = warning + +# Title : Route parameter and argument optionality is mismatched +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0007.severity = warning + +# Title : Do not use ConfigureWebHost with WebApplicationBuilder.Host +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0008.severity = error + +# Title : Do not use Configure with WebApplicationBuilder.WebHost +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0009.severity = error + +# Title : Do not use UseStartup with WebApplicationBuilder.WebHost +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0010.severity = error + +# Title : Suggest using builder.Logging over Host.ConfigureLogging or WebHost.ConfigureLogging +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0011.severity = warning + +# Title : Suggest using builder.Services over Host.ConfigureServices or WebHost.ConfigureServices +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0012.severity = warning + +# Title : Suggest switching from using Configure methods to WebApplicationBuilder.Configuration +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0013.severity = warning + +# Title : Suggest using top level route registrations +# Category : Usage +# Help Link: https://aka.ms/aspnet/analyzers +dotnet_diagnostic.ASP0014.severity = warning + +# Title : Component parameter should have public setters. +# Category : Encapsulation +dotnet_diagnostic.BL0001.severity = error + +# Title : Component has multiple CaptureUnmatchedValues parameters +# Category : Usage +dotnet_diagnostic.BL0002.severity = warning + +# Title : Component parameter with CaptureUnmatchedValues has the wrong type +# Category : Usage +dotnet_diagnostic.BL0003.severity = warning + +# Title : Component parameter should be public. +# Category : Encapsulation +dotnet_diagnostic.BL0004.severity = error + +# Title : Component parameter should not be set outside of its component. +# Category : Usage +dotnet_diagnostic.BL0005.severity = warning + +# Title : Do not use RenderTree types +# Category : Usage +dotnet_diagnostic.BL0006.severity = warning + +# Title : Component parameters should be auto properties +# Category : Usage +dotnet_diagnostic.BL0007.severity = warning + +# Title : Do not declare static members on generic types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1000 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1000.severity = none + +# Title : Types that own disposable fields should be disposable +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1001 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1001.severity = warning + +# Title : Do not expose generic lists +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1002 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1002.severity = none + +# Title : Use generic event handler instances +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1003 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1003.severity = none + +# Title : Avoid excessive parameters on generic types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1005 +# Tags : PortedFromFxCop, Telemetry +dotnet_diagnostic.CA1005.severity = none + +# Title : Enums should have zero value +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1008 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode, RuleNoZero +dotnet_diagnostic.CA1008.severity = none + +# Title : Generic interface should also be implemented +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1010 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1010.severity = none + +# Title : Abstract types should not have public constructors +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1012 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1012.severity = warning +dotnet_code_quality.CA1012.api_surface = all + +# Title : Mark assemblies with CLSCompliant +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1014 +# Tags : PortedFromFxCop, Telemetry, CompilationEnd +dotnet_diagnostic.CA1014.severity = none + +# Title : Mark assemblies with assembly version +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1016 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA1016.severity = warning + +# Title : Mark assemblies with ComVisible +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1017 +# Tags : PortedFromFxCop, Telemetry, CompilationEnd +dotnet_diagnostic.CA1017.severity = none + +# Title : Mark attributes with AttributeUsageAttribute +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1018 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1018.severity = warning + +# Title : Define accessors for attribute arguments +# Category : Design +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1019 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1019.severity = suggestion + +# Title : Define accessors for attribute arguments +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1019 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1019.severity = warning + +# Title : Avoid out parameters +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1021 +# Tags : PortedFromFxCop, Telemetry +dotnet_diagnostic.CA1021.severity = none + +# Title : Use properties where appropriate +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1024 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1024.severity = none + +# Title : Mark enums with FlagsAttribute +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1027 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1027.severity = warning +dotnet_code_quality.CA1027.api_surface = all + +# Title : Enum Storage should be Int32 +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1028 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1028.severity = none + +# Title : Use events where appropriate +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1030 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1030.severity = warning + +# Title : Do not catch general exception types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1031 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1031.severity = warning + +# Title : Implement standard exception constructors +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1032 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1032.severity = none + +# Title : Interface methods should be callable by child types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1033.severity = warning + +# Title : Nested types should not be visible +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1034 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1034.severity = none + +# Title : Override methods on comparable types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1036 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1036.severity = none + +# Title : Avoid empty interfaces +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1040 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +# Comment : Reasonably frequent in modern .NET programming +dotnet_diagnostic.CA1040.severity = none + +# Title : Provide ObsoleteAttribute message +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1041 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1041.severity = warning +dotnet_code_quality.CA1041.api_surface = all + +# Title : Use Integral Or String Argument For Indexers +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1043 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1043.severity = none + +# Title : Properties should not be write only +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1044 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1044.severity = warning + +# Title : Do not pass types by reference +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1045 +# Tags : PortedFromFxCop, Telemetry +dotnet_diagnostic.CA1045.severity = none + +# Title : Do not overload equality operator on reference types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1046 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1046.severity = warning +dotnet_code_quality.CA1046.api_surface = all + +# Title : Do not declare protected member in sealed type +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1047 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1047.severity = warning +dotnet_code_quality.CA1047.api_surface = all + +# Title : Declare types in namespaces +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1050 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1050.severity = none + +# Title : Do not declare visible instance fields +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1051 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1051.severity = none + +# Title : Static holder types should be Static or NotInheritable +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1052 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1052.severity = warning + +# Title : URI-like parameters should not be strings +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1054 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1054.severity = none + +# Title : URI-like return values should not be strings +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1055 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1055.severity = none + +# Title : URI-like properties should not be strings +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1056 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1056.severity = none + +# Title : Types should not extend certain base types +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1058 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1058.severity = warning +dotnet_code_quality.CA1058.api_surface = public + +# Title : Move pinvokes to native methods class +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1060 +# Tags : PortedFromFxCop, Telemetry +dotnet_diagnostic.CA1060.severity = warning + +# Title : Do not hide base class methods +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1061 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1061.severity = warning +dotnet_code_quality.CA1061.api_surface = all + +# Title : Validate arguments of public methods +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1062 +# Tags : PortedFromFxCop, Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1062.severity = none + +# Title : Implement IDisposable Correctly +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1063 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1063.severity = warning + +# Title : Exceptions should be public +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1064 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1064.severity = none + +# Title : Do not raise exceptions in unexpected locations +# Category : Design +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1065 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1065.severity = warning + +# Title : Do not raise exceptions in unexpected locations +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1065 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1065.severity = warning + +# Title : Implement IEquatable when overriding Object.Equals +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1066 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1066.severity = warning + +# Title : Override Object.Equals(object) when implementing IEquatable +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1067 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1067.severity = warning + +# Title : CancellationToken parameters must come last +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1068 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1068.severity = none + +# Title : Enums values should not be duplicated +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1069 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1069.severity = warning + +# Title : Do not declare event fields as virtual +# Category : Design +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1070 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1070.severity = warning +dotnet_code_quality.CA1070.api_surface = all + +# Title : Avoid using cref tags with a prefix +# Category : Documentation +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1200 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1200.severity = warning + +# Title : Do not pass literals as localized parameters +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1303 +# Tags : PortedFromFxCop, Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1303.severity = none + +# Title : Specify CultureInfo +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1304 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1304.severity = none + +# Title : Specify IFormatProvider +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1305.severity = none + +# Title : Specify StringComparison for clarity +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1307 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1307.severity = none + +# Title : Normalize strings to uppercase +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1308 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1308.severity = none + +# Title : Use ordinal string comparison +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1309 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1309.severity = suggestion + +# Title : Specify StringComparison for correctness +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1310 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1310.severity = none + +# Title : Specify a culture or use an invariant version +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1311 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1311.severity = warning + +# Title : P/Invokes should not be visible +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1401 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1401.severity = none + +# Title : Validate platform compatibility +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1416.severity = warning + +# Title : Do not use 'OutAttribute' on string parameters for P/Invokes +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1417 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1417.severity = warning + +# Title : Use valid platform string +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1418 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1418.severity = warning + +# Title : Provide a parameterless constructor that is as visible as the containing type for concrete types derived from 'System.Runtime.InteropServices.SafeHandle' +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1419 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1419.severity = none + +# Title : Property, type, or attribute requires runtime marshalling +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1420 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1420.severity = warning + +# Title : This method uses runtime marshalling even when the 'DisableRuntimeMarshallingAttribute' is applied +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1421 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1421.severity = suggestion + +# Title : Validate platform compatibility +# Category : Interoperability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1422 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1422.severity = warning + +# Title : Avoid excessive inheritance +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1501 +# Tags : PortedFromFxCop, Telemetry, CompilationEnd +dotnet_diagnostic.CA1501.severity = warning +dotnet_code_quality.CA1501.api_surface = public + +# Title : Avoid excessive complexity +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1502 +# Tags : PortedFromFxCop, Telemetry, CompilationEnd +# Comment : Code gets complicated +dotnet_diagnostic.CA1502.severity = none + +# Title : Avoid unmaintainable code +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1505 +# Tags : PortedFromFxCop, Telemetry, CompilationEnd +dotnet_diagnostic.CA1505.severity = warning + +# Title : Avoid excessive class coupling +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1506 +# Tags : PortedFromFxCop, Telemetry, CompilationEnd +# Comment : Code gets complicated +dotnet_diagnostic.CA1506.severity = none + +# Title : Use nameof to express symbol names +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1507 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1507.severity = warning + +# Title : Avoid dead conditional code +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1508 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1508.severity = warning + +# Title : Invalid entry in code metrics rule specification file +# Category : Maintainability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1509 +# Tags : Telemetry, CompilationEnd +dotnet_diagnostic.CA1509.severity = warning + +# Title : Do not name enum values 'Reserved' +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1700 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1700.severity = none + +# Title : Identifiers should not contain underscores +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1707 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +# Comment : StyleCop handles this +dotnet_diagnostic.CA1707.severity = none + +# Title : Identifiers should differ by more than case +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1708 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1708.severity = silent + +# Title : Identifiers should have correct suffix +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1710 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1710.severity = silent + +# Title : Identifiers should not have incorrect suffix +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1711 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1711.severity = silent + +# Title : Do not prefix enum values with type name +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1712 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1712.severity = warning + +# Title : Events should not have 'Before' or 'After' prefix +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1713 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1713.severity = warning + +# Title : Identifiers should have correct prefix +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1715 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1715.severity = none + +# Title : Identifiers should not match keywords +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1716 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1716.severity = warning +dotnet_code_quality.CA1716.api_surface = all +dotnet_code_quality.CA1716.analyzed_symbol_kinds = all + +# Title : Identifier contains type name +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1720 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1720.severity = silent + +# Title : Property names should not match get methods +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1721 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1721.severity = none + +# Title : Type names should not match namespaces +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1724 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA1724.severity = none + +# Title : Parameter names should match base declaration +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1725 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1725.severity = warning + +# Title : Use PascalCase for named placeholders +# Category : Naming +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1727 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1727.severity = silent + +# Title : Review unused parameters +# Category : Usage +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1801 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1801.severity = none + +# Title : Review unused parameters +# Category : Usage +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1801 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1801.severity = none + +# Title : Use literals where appropriate +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1802 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1802.severity = none + +# Title : Use literals where appropriate +# Category : Performance +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1802 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1802.severity = none + +# Title : Do not initialize unnecessarily +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1805 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1805.severity = warning + +# Title : Do not initialize unnecessarily +# Category : Performance +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1805 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1805.severity = warning + +# Title : Do not ignore method results +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1806 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1806.severity = warning + +# Title : Initialize reference type static fields inline +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1810 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1810.severity = none + +# Title : Avoid uninstantiated internal classes +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1812 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +# Comment : S1144 finds more cases and has no false positives +dotnet_diagnostic.CA1812.severity = none + +# Title : Avoid unsealed attributes +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1813 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1813.severity = none + +# Title : Prefer jagged arrays over multidimensional +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1814 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1814.severity = none + +# Title : Override equals and operator equals on value types +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1815 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1815.severity = none + +# Title : Dispose methods should call SuppressFinalize +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1816 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1816.severity = warning + +# Title : Properties should not return arrays +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1819 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1819.severity = none + +# Title : Test for empty strings using string length +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1820 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1820.severity = none + +# Title : Remove empty Finalizers +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1821 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1821.severity = none + +# Title : Mark members as static +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1822.severity = warning + +# Title : Avoid unused private fields +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1823 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1823.severity = none + +# Title : Mark assemblies with NeutralResourcesLanguageAttribute +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1824 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA1824.severity = suggestion + +# Title : Avoid zero-length array allocations +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1825 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1825.severity = none + +# Title : Do not use Enumerable methods on indexable collections +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1826 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1826.severity = none + +# Title : Do not use Count() or LongCount() when Any() can be used +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1827 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1827.severity = none + +# Title : Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1828 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1828.severity = none + +# Title : Use Length/Count property instead of Count() when available +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1829 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1829.severity = none + +# Title : Prefer strongly-typed Append and Insert method overloads on StringBuilder +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1830 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1830.severity = none + +# Title : Use AsSpan or AsMemory instead of Range-based indexers when appropriate +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1831 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1831.severity = none + +# Title : Use AsSpan or AsMemory instead of Range-based indexers when appropriate +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1832 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1832.severity = none + +# Title : Use AsSpan or AsMemory instead of Range-based indexers when appropriate +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1833 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1833.severity = none + +# Title : Consider using 'StringBuilder.Append(char)' when applicable +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1834 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1834.severity = none + +# Title : Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1835 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1835.severity = none + +# Title : Prefer IsEmpty over Count +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1836 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1836.severity = none + +# Title : Use 'Environment.ProcessId' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1837 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1837.severity = none + +# Title : Avoid 'StringBuilder' parameters for P/Invokes +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1838 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1838.severity = none + +# Title : Use 'Environment.ProcessPath' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1839 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1839.severity = none + +# Title : Use 'Environment.CurrentManagedThreadId' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1840 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1840.severity = none + +# Title : Prefer Dictionary.Contains methods +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1841 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1841.severity = none + +# Title : Do not use 'WhenAll' with a single task +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1842 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1842.severity = suggestion + +# Title : Do not use 'WaitAll' with a single task +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1843 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1843.severity = none + +# Title : Provide memory-based overrides of async methods when subclassing 'Stream' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1844 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1844.severity = none + +# Title : Use span-based 'string.Concat' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1845 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1845.severity = none + +# Title : Prefer 'AsSpan' over 'Substring' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1846 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1846.severity = none + +# Title : Use char literal for a single character lookup +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1847 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1847.severity = none + +# Title : Use the LoggerMessage delegates +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848 +# Tags : Telemetry, EnabledRuleInAggressiveMode +# Comment : Use R9 logging model instead +dotnet_diagnostic.CA1848.severity = none + +# Title : Call async methods when in an async method +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1849 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1849.severity = none + +# Title : Prefer static 'HashData' method over 'ComputeHash' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1850 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1850.severity = none + +# Title : Possible multiple enumerations of 'IEnumerable' collection +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1851.severity = suggestion + +# Title : Seal internal types +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852 +# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA1852.severity = none + +# Title : Unnecessary call to 'Dictionary.ContainsKey(key)' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1853 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1853.severity = none + +# Title : Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1854 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA1854.severity = none + +# Title : Prefer 'Clear' over 'Fill' +# Category : Performance +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1855 +# Tags : Telemetry, EnabledRuleInAggressiveMode + +# Title : Dispose objects before losing scope +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000 +# Tags : PortedFromFxCop, Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2000.severity = warning + +# Title : Do not lock on objects with weak identity +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2002 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2002.severity = warning + +# Title : Consider calling ConfigureAwait on the awaited task +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2007 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2007.severity = none + +# Title : Do not create tasks without passing a TaskScheduler +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2008 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2008.severity = warning + +# Title : Do not call ToImmutableCollection on an ImmutableCollection value +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2009 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2009.severity = warning + +# Title : Avoid infinite recursion +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2011 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2011.severity = error + +# Title : Use ValueTasks correctly +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2012 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2012.severity = warning + +# Title : Do not use ReferenceEquals with value types +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2013 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2013.severity = warning + +# Title : Do not use stackalloc in loops +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2014 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2014.severity = warning + +# Title : Do not use stackalloc in loops +# Category : Reliability +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2014 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2014.severity = warning + +# Title : Do not define finalizers for types derived from MemoryManager +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2015 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2015.severity = warning + +# Title : Forward the 'CancellationToken' parameter to methods +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2016 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2016.severity = warning + +# Title : Parameter count mismatch +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2017 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2017.severity = warning + +# Title : 'Buffer.BlockCopy' expects the number of bytes to be copied for the 'count' argument +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2018 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2018.severity = warning + +# Title : Improper 'ThreadStatic' field initialization +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2019 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2019.severity = warning + +# Title : Prevent from behavioral change +# Category : Reliability +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2020 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2020.severity = suggestion + +# Title : Review SQL queries for security vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2100 +# Tags : PortedFromFxCop, Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2100.severity = none + +# Title : Specify marshaling for P/Invoke string arguments +# Category : Globalization +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2101 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2101.severity = warning + +# Title : Review visible event handlers +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2109 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2109.severity = none + +# Title : Seal methods that satisfy private interfaces +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2119 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2119.severity = warning + +# Title : Do Not Catch Corrupted State Exceptions +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2153 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2153.severity = warning + +# Title : Rethrow to preserve stack details +# Category : Usage +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2200 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2200.severity = warning + +# Title : Rethrow to preserve stack details +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2200 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2200.severity = warning + +# Title : Do not raise reserved exception types +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2201 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2201.severity = warning + +# Title : Initialize value type static fields inline +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2207 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2207.severity = none + +# Title : Instantiate argument exceptions correctly +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2208 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2208.severity = warning + +# Title : Non-constant fields should not be visible +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2211 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2211.severity = none + +# Title : Disposable fields should be disposed +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2213 +# Tags : PortedFromFxCop, Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2213.severity = warning + +# Title : Do not call overridable methods in constructors +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2214 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2214.severity = warning + +# Title : Dispose methods should call base class dispose +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2215 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2215.severity = warning + +# Title : Disposable types should declare finalizer +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2216 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2216.severity = warning + +# Title : Do not mark enums with FlagsAttribute +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2217 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2217.severity = warning +dotnet_code_quality.CA2217.api_surface = all + +# Title : Do not raise exceptions in finally clauses +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2219 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2219.severity = warning + +# Title : Operator overloads have named alternates +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2225 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2225.severity = none + +# Title : Operators should have symmetrical overloads +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2226 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2226.severity = none + +# Title : Collection properties should be read only +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2227 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2227.severity = none + +# Title : Implement serialization constructors +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2229 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +# Comment : Obsolete +dotnet_diagnostic.CA2229.severity = none + +# Title : Overload operator equals on overriding value type Equals +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2231 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2231.severity = error + +# Title : Pass system uri objects instead of strings +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2234 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2234.severity = none + +# Title : Mark all non-serializable fields +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2235 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +# Comment : Obsolete +dotnet_diagnostic.CA2235.severity = none + +# Title : Mark ISerializable types with serializable +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2237 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +# Comment : Obsolete +dotnet_diagnostic.CA2237.severity = none + +# Title : Provide correct arguments to formatting methods +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2241 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2241.severity = warning + +# Title : Test for NaN correctly +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2242 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2242.severity = warning + +# Title : Attribute string literals should parse correctly +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2243 +# Tags : PortedFromFxCop, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2243.severity = warning + +# Title : Do not duplicate indexed element initializations +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2244 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2244.severity = warning + +# Title : Do not assign a property to itself +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2245 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2245.severity = warning + +# Title : Assigning symbol and its member in the same statement +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2246 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2246.severity = warning + +# Title : Argument passed to TaskCompletionSource constructor should be TaskCreationOptions enum instead of TaskContinuationOptions enum +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2247 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2247.severity = warning + +# Title : Provide correct 'enum' argument to 'Enum.HasFlag' +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2248 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2248.severity = warning + +# Title : Consider using 'string.Contains' instead of 'string.IndexOf' +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2249 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2249.severity = warning + +# Title : Use 'ThrowIfCancellationRequested' +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2250 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2250.severity = suggestion + +# Title : Use 'string.Equals' +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2251 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2251.severity = warning + +# Title : This API requires opting into preview features +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2252 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2252.severity = error + +# Title : Named placeholders should not be numeric values +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2253 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2253.severity = warning + +# Title : Template should be a static expression +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2254 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2254.severity = none + +# Title : The 'ModuleInitializer' attribute should not be used in libraries +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2255 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2255.severity = warning + +# Title : All members declared in parent interfaces must have an implementation in a DynamicInterfaceCastableImplementation-attributed interface +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2256 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2256.severity = error + +# Title : Members defined on an interface with the 'DynamicInterfaceCastableImplementationAttribute' should be 'static' +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2257 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2257.severity = warning + +# Title : Providing a 'DynamicInterfaceCastableImplementation' interface in Visual Basic is unsupported +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2258 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2258.severity = warning + +# Title : 'ThreadStatic' only affects static fields +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2259 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2259.severity = warning + +# Title : Use correct type parameter +# Category : Usage +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2260 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2260.severity = warning + +# Title : Do not use insecure deserializer BinaryFormatter +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2300 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2300.severity = none + +# Title : Do not call BinaryFormatter.Deserialize without first setting BinaryFormatter.Binder +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2301 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2301.severity = none + +# Title : Ensure BinaryFormatter.Binder is set before calling BinaryFormatter.Deserialize +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2302 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2302.severity = none + +# Title : Do not use insecure deserializer LosFormatter +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2305 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2305.severity = none + +# Title : Do not use insecure deserializer NetDataContractSerializer +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2310 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2310.severity = none + +# Title : Do not deserialize without first setting NetDataContractSerializer.Binder +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2311 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2311.severity = none + +# Title : Ensure NetDataContractSerializer.Binder is set before deserializing +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2312 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2312.severity = none + +# Title : Do not use insecure deserializer ObjectStateFormatter +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2315 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2315.severity = none + +# Title : Do not deserialize with JavaScriptSerializer using a SimpleTypeResolver +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2321 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2321.severity = none + +# Title : Ensure JavaScriptSerializer is not initialized with SimpleTypeResolver before deserializing +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2322 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2322.severity = none + +# Title : Do not use TypeNameHandling values other than None +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2326 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2326.severity = none + +# Title : Do not use insecure JsonSerializerSettings +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2327 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2327.severity = none + +# Title : Ensure that JsonSerializerSettings are secure +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2328 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2328.severity = none + +# Title : Do not deserialize with JsonSerializer using an insecure configuration +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2329 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2329.severity = none + +# Title : Ensure that JsonSerializer has a secure configuration when deserializing +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2330 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA2330.severity = none + +# Title : Do not use DataTable.ReadXml() with untrusted data +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2350 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2350.severity = none + +# Title : Do not use DataSet.ReadXml() with untrusted data +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2351 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2351.severity = none + +# Title : Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2352 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2352.severity = none + +# Title : Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2352 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2352.severity = none + +# Title : Unsafe DataSet or DataTable in serializable type +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2353 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2353.severity = none + +# Title : Unsafe DataSet or DataTable in serializable type +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2353 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2353.severity = none + +# Title : Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attacks +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2354 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2354.severity = none + +# Title : Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attacks +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2354 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2354.severity = none + +# Title : Unsafe DataSet or DataTable type found in deserializable object graph +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2355 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2355.severity = none + +# Title : Unsafe DataSet or DataTable type found in deserializable object graph +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2355 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2355.severity = none + +# Title : Unsafe DataSet or DataTable type in web deserializable object graph +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2356 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2356.severity = none + +# Title : Unsafe DataSet or DataTable type in web deserializable object graph +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2356 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2356.severity = none + +# Title : Ensure auto-generated class containing DataSet.ReadXml() is not used with untrusted data +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2361 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2361.severity = none + +# Title : Unsafe DataSet or DataTable in auto-generated serializable type can be vulnerable to remote code execution attacks +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2362 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2362.severity = none + +# Title : Unsafe DataSet or DataTable in autogenerated serializable type can be vulnerable to remote code execution attacks +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2362 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA2362.severity = none + +# Title : Review code for SQL injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3001 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3001.severity = none + +# Title : Review code for XSS vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3002 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3002.severity = none + +# Title : Review code for file path injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3003 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3003.severity = none + +# Title : Review code for information disclosure vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3004 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3004.severity = none + +# Title : Review code for LDAP injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3005 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3005.severity = none + +# Title : Review code for process command injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3006 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3006.severity = none + +# Title : Review code for open redirect vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3007 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3007.severity = none + +# Title : Review code for XPath injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3008 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3008.severity = none + +# Title : Review code for XML injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3009 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3009.severity = none + +# Title : Review code for XAML injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3010 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3010.severity = none + +# Title : Review code for DLL injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3011 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3011.severity = none + +# Title : Review code for regex injection vulnerabilities +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3012 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3012.severity = none + +# Title : Do Not Add Schema By URL +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3061 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3061.severity = none + +# Title : Insecure DTD processing in XML +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3075 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3075.severity = none + +# Title : Insecure XSLT script processing. +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3076 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3076.severity = none + +# Title : Insecure XSLT script processing +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3076 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3076.severity = none + +# Title : Insecure Processing in API Design, XmlDocument and XmlTextReader +# Category : Security +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3077 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3077.severity = none + +# Title : Insecure Processing in API Design, XmlDocument and XmlTextReader +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3077 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3077.severity = none + +# Title : Mark Verb Handlers With Validate Antiforgery Token +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca3147 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA3147.severity = none + +# Title : Do Not Use Weak Cryptographic Algorithms +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5350 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5350.severity = none + +# Title : Do Not Use Broken Cryptographic Algorithms +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5351 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5351.severity = none + +# Title : Review cipher mode usage with cryptography experts +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5358 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5358.severity = none + +# Title : Do Not Disable Certificate Validation +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5359 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5359.severity = none + +# Title : Do Not Call Dangerous Methods In Deserialization +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5360 +# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5360.severity = none + +# Title : Do Not Disable SChannel Use of Strong Crypto +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5361 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5361.severity = none + +# Title : Potential reference cycle in deserialized object graph +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5362 +# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5362.severity = none + +# Title : Do Not Disable Request Validation +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5363 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5363.severity = none + +# Title : Do Not Use Deprecated Security Protocols +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5364 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5364.severity = none + +# Title : Do Not Disable HTTP Header Checking +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5365 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5365.severity = none + +# Title : Use XmlReader for 'DataSet.ReadXml()' +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5366 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5366.severity = none + +# Title : Do Not Serialize Types With Pointer Fields +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5367 +# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5367.severity = none + +# Title : Set ViewStateUserKey For Classes Derived From Page +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5368 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5368.severity = none + +# Title : Use XmlReader for 'XmlSerializer.Deserialize()' +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5369 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5369.severity = none + +# Title : Use XmlReader for XmlValidatingReader constructor +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5370 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5370.severity = none + +# Title : Use XmlReader for 'XmlSchema.Read()' +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5371 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5371.severity = none + +# Title : Use XmlReader for XPathDocument constructor +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5372 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5372.severity = none + +# Title : Do not use obsolete key derivation function +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5373 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5373.severity = none + +# Title : Do Not Use XslTransform +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5374 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5374.severity = none + +# Title : Do Not Use Account Shared Access Signature +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5375 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5375.severity = none + +# Title : Use SharedAccessProtocol HttpsOnly +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5376 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5376.severity = none + +# Title : Use Container Level Access Policy +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5377 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5377.severity = none + +# Title : Do not disable ServicePointManagerSecurityProtocols +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5378 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5378.severity = none + +# Title : Ensure Key Derivation Function algorithm is sufficiently strong +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5379 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5379.severity = none + +# Title : Do Not Add Certificates To Root Store +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5380 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5380.severity = none + +# Title : Ensure Certificates Are Not Added To Root Store +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5381 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5381.severity = none + +# Title : Use Secure Cookies In ASP.NET Core +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5382 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5382.severity = none + +# Title : Ensure Use Secure Cookies In ASP.NET Core +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5383 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5383.severity = none + +# Title : Do Not Use Digital Signature Algorithm (DSA) +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5384 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5384.severity = none + +# Title : Use Rivest-Shamir-Adleman (RSA) Algorithm With Sufficient Key Size +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5385 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5385.severity = none + +# Title : Avoid hardcoding SecurityProtocolType value +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5386 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5386.severity = none + +# Title : Do Not Use Weak Key Derivation Function With Insufficient Iteration Count +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5387 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5387.severity = none + +# Title : Ensure Sufficient Iteration Count When Using Weak Key Derivation Function +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5388 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5388.severity = none + +# Title : Do Not Add Archive Item's Path To The Target File System Path +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5389 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5389.severity = none + +# Title : Do not hard-code encryption key +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5390 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5390.severity = none + +# Title : Use antiforgery tokens in ASP.NET Core MVC controllers +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5391 +# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5391.severity = none + +# Title : Use DefaultDllImportSearchPaths attribute for P/Invokes +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5392 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5392.severity = none + +# Title : Do not use unsafe DllImportSearchPath value +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5393 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5393.severity = none + +# Title : Do not use insecure randomness +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5394 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5394.severity = none + +# Title : Miss HttpVerb attribute for action methods +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5395 +# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5395.severity = none + +# Title : Set HttpOnly to true for HttpCookie +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5396 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5396.severity = none + +# Title : Do not use deprecated SslProtocols values +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5397 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5397.severity = none + +# Title : Avoid hardcoded SslProtocols values +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5398 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5398.severity = none + +# Title : HttpClients should enable certificate revocation list checks +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5399 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5399.severity = none + +# Title : Ensure HttpClient certificate revocation list check is not disabled +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5400 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5400.severity = none + +# Title : Do not use CreateEncryptor with non-default IV +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5401 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5401.severity = none + +# Title : Use CreateEncryptor with the default IV +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5402 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA5402.severity = none + +# Title : Do not hard-code certificate +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5403 +# Tags : Dataflow, Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5403.severity = none + +# Title : Do not disable token validation checks +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5404 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5404.severity = none + +# Title : Do not always skip token validation in delegates +# Category : Security +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5405 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.CA5405.severity = none + +# Title : Avoid single use string builders in frequently called class members. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR1001.severity = none + +# Title : Use bitwise operations instead of 'Enum.HasFlag' +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : Obsolete +dotnet_diagnostic.CPR101.severity = none + +# Title : Use HashSet.Contains instead of List.Contains +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR102.severity = none + +# Title : Use Ordinal and OrdinalIgnoreCase instead of InvariantCulture when localization is not required. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR103.severity = warning + +# Title : Use DateTime.UtcNow instead of DateTime.Now when time zone conversion is not applicable. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR105.severity = none + +# Title : MemoryStream.ToArray() is memory inefficient and can often be avoided. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR107.severity = none + +# Title : List.AddRange() is memory inefficient. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : Obsolete +dotnet_diagnostic.CPR108.severity = none + +# Title : List.Reverse can cause boxing. Implement your own reverse for better performance. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : Obsolete +dotnet_diagnostic.CPR109.severity = none + +# Title : Specify an initial list size when initializing a list to reduce reallocations. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : Too noisy +dotnet_diagnostic.CPR110.severity = none + +# Title : ImmutableDictionary is memory inefficient. Use IReadOnlyDictionary if readonly collection is needed. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR111.severity = none + +# Title : ImmutableList is memory inefficient. Use IReadOnlyList if a readonly collection is needed. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR112.severity = none + +# Title : Avoid Linq as much as possible since much of the functionality is inefficient. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : Too noisy +dotnet_diagnostic.CPR113.severity = none + +# Title : string.StartWith and string.EndsWith can be implemented more efficiently checking characters with the indexer for short strings. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR114.severity = none + +# Title : string.Contains with string.ToLower() or string.ToUpper() causes allocations which can be avoided with a case insensitive comparison. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR115.severity = none + +# Title : string.Equals with string.ToLower() or string.ToUpper() causes allocations which can be avoided with a case insensitive comparison. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR116.severity = none + +# Title : operator== with string.ToLower() or string.ToUpper() causes allocations which can be avoided with a case insensitive comparison. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR117.severity = none + +# Title : Linq.SequenceEqual is inefficient. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR118.severity = none + +# Title : Use Debug.WriteLine for debugging instead of Console.WriteLine. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : Duplicate +dotnet_diagnostic.CPR119.severity = none + +# Title : File.ReadAllXXX should be replaced by using a StreamReader to avoid adding objects to the large object heap (LOH). +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR120.severity = none + +# Title : Specify 'concurrencyLevel' and 'capacity' in the ConcurrentDictionary ctor. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR121.severity = none + +# Title : ConcurrentDictionary.Keys and ConcurrentDictionary.Values takes a lock defeats the benefits of concurrency. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR122.severity = none + +# Title : ConcurrentDictionary Count, ToArray(), CopyTo() and Clear() take locks and defeats the benefits of the concurrency. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR123.severity = none + +# Title : TraceSource.TraceEvent does a lock on each listener and can cause lock contention. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR124.severity = none + +# Title : String interning can cause lock contention. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR125.severity = none + +# Title : string.Format and StringBuilder.AppendFormat are not efficient for concatenation. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR126.severity = none + +# Title : Use a custom implementation of IComparer rather than Nullable.Compare. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR127.severity = warning + +# Title : Process.GetProcessName/Process.GetMachineName does a lot of processing. Use once per process and save the result. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR128.severity = none + +# Title : string.IndexOf is inefficient when used to check the beginning of string. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR129.severity = none + +# Title : ArrayList is non-generic. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR130.severity = none + +# Title : Hashtable is non-generic. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR131.severity = none + +# Title : Avoid char.ToString. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR134.severity = none + +# Title : Stream.CopyTo should be used with buffer size. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR135.severity = none + +# Title : StreamReader.ReadLine can allocate StringBuilder instances each call if the lines are longer than the buffer size. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR136.severity = none + +# Title : Random class instances should be shared as statics. Random is not thread safe so locks, ThreadLocal class or [ThreadStatic] attribute should be used for synchronization. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR138.severity = none + +# Title : Regular expressions should be reused from static fields or properties. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR139.severity = none + +# Title : TextWriter.WriteLine(string) allocates a char array. Use different overload or make two calls. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR140.severity = none + +# Title : Reduce delegate allocations by storing them in static fields and properties. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +# Comment : This rule flags most lambda uses, which makes it extremely verbose and not particularly useful +dotnet_diagnostic.CPR145.severity = none + +# Title : Extra dictionary access +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR500.severity = none + +# Title : Avoid repeated type checking. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR501.severity = none + +# Title : Do not cast multiple times. +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR502.severity = none + +# Title : Extra HashSet access +# Category : Performance Analysis +# Help Link: http://aka.ms/cprwiki +dotnet_diagnostic.CPR503.severity = none + +# Title : Do not use banned insecure deserialization APIs +# Category : Security +# Help Link: https://aka.ms/ia2989 +dotnet_diagnostic.IA2989.severity = none + +# Title : Do Not Use Banned APIs For Insecure Deserializers +# Category : Security +# Help Link: https://aka.ms/ia2992 +dotnet_diagnostic.IA2992.severity = none + +# Title : Do Not Use Banned Constructors For Insecure Deserializers +# Category : Security +# Help Link: https://aka.ms/ia2993 +dotnet_diagnostic.IA2993.severity = none + +# Title : Do Not Use ResourceSet Without ResourceReader +# Category : Security +# Help Link: https://aka.ms/ia2994 +dotnet_diagnostic.IA2994.severity = none + +# Title : Do Not Use ResourceReader +# Category : Security +# Help Link: https://aka.ms/ia2995 +dotnet_diagnostic.IA2995.severity = none + +# Title : Do Not Use ResXResourceReader Without ITypeResolutionService +# Category : Security +# Help Link: https://aka.ms/ia2996 +dotnet_diagnostic.IA2996.severity = none + +# Title : Do Not Use TypeNameHandling Other Than None +# Category : Security +# Help Link: https://aka.ms/ia2997 +dotnet_diagnostic.IA2997.severity = none + +# Title : Do Not Deserialize With BinaryFormatter Without Binder +# Category : Security +# Help Link: https://aka.ms/ia2998 +dotnet_diagnostic.IA2998.severity = none + +# Title : Do Not Set BinaryFormatter.Binder to null +# Category : Security +# Help Link: https://aka.ms/ia2999 +dotnet_diagnostic.IA2999.severity = none + +# Title : Do Not Use Weak Cryptographic Algorithms +# Category : Security +# Help Link: http://aka.ms/IA5350 +# Tags : Telemetry +dotnet_diagnostic.IA5350.severity = none + +# Title : Do Not Use Broken Cryptographic Algorithms +# Category : Security +# Help Link: http://aka.ms/IA5351 +# Tags : Telemetry +dotnet_diagnostic.IA5351.severity = none + +# Title : Do Not Misuse Cryptographic APIs +# Category : Security +# Help Link: http://aka.ms/IA5352 +# Tags : Telemetry +dotnet_diagnostic.IA5352.severity = none + +# Title : Use approved crypto libraries for the supported platform +# Category : Security +# Help Link: https://aka.ms/ia5359 +# Tags : Telemetry +dotnet_diagnostic.IA5359.severity = none + +# Title : Custom web token handler was found +# Category : Security +# Help Link: https://aka.ms/ia6450 +# Tags : Telemetry +dotnet_diagnostic.IA6450.severity = none + +# Title : Implement required validations for app asserted actor token +# Category : Security +# Help Link: https://aka.ms/ia6451 +# Tags : Telemetry +dotnet_diagnostic.IA6451.severity = none + +# Title : Do not disable {0} +# Category : Security +# Help Link: https://aka.ms/ia6452 +# Tags : Telemetry +dotnet_diagnostic.IA6452.severity = none + +# Title : Do not disable {0} +# Category : Security +# Help Link: https://aka.ms/ia6453 +# Tags : Telemetry +dotnet_diagnostic.IA6453.severity = none + +# Title : Do not disable {0} +# Category : Security +# Help Link: https://aka.ms/ia6454 +# Tags : Telemetry +dotnet_diagnostic.IA6454.severity = none + +# Title : Do Not Use Insecure PowerShell LanguageModes +# Category : Security +# Help Link: https://aka.ms/ia6456 +# Tags : Telemetry +dotnet_diagnostic.IA6456.severity = none + +# Title : Review PowerShell Execution for PowerShell Injection +# Category : Security +# Help Link: https://aka.ms/IA6457 +dotnet_diagnostic.IA6457.severity = none + +# Title : Simplify name +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0001 +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line. This metadata was entered manually, since it is not exposed in the normal way from the analyzer assembly. +dotnet_diagnostic.IDE0001.severity = silent + +# Title : Simplify name +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0002 +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line. This metadata was entered manually, since it is not exposed in the normal way from the analyzer assembly. +dotnet_diagnostic.IDE0002.severity = silent + +# Title : Remove this or Me qualification +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0003-ide0009 +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line. This metadata was entered manually, since it is not exposed in the normal way from the analyzer assembly. +dotnet_diagnostic.IDE0003.severity = silent +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Title : Remove Unnecessary Cast +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0004 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled, Unnecessary +dotnet_diagnostic.IDE0004.severity = warning + +# Title : Using directive is unnecessary. +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended, Unnecessary +dotnet_diagnostic.IDE0005.severity = none + +# Title : Using directive is unnecessary. +# Category : Style +# Tags : Telemetry, EnforceOnBuild_Never, NotConfigurable, Unnecessary +dotnet_diagnostic.IDE0005_gen.severity = none + +# Title : Use implicit type +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0007 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0007.severity = silent +csharp_style_var_elsewhere = true +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = true + +# Title : Use explicit type +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0008 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0008.severity = silent + +# Title : Member access should be qualified. +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0009 +# Tags : Telemetry, EnforceOnBuild_Never +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line +dotnet_diagnostic.IDE0009.severity = none + +# Title : Add missing cases +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0010 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0010.severity = silent + +# Title : Add braces +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0011 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0011.severity = warning +csharp_prefer_braces = true + +# Title : Use 'throw' expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0016 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0016.severity = warning +csharp_style_throw_expression = true + +# Title : Simplify object initialization +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0017 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0017.severity = warning +dotnet_style_object_initializer = true + +# Title : Inline variable declaration +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0018 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0018.severity = warning +csharp_style_inlined_variable_declaration = true + +# Title : Use pattern matching +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0019 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0019.severity = silent +csharp_style_pattern_matching_over_is_with_cast_check = true + +# Title : Use pattern matching +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0020 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0020.severity = silent +csharp_style_pattern_matching_over_is_with_cast_check + +# Title : Use expression body for constructors +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0021 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0021.severity = warning +csharp_style_expression_bodied_constructors = false + +# Title : Use expression body for methods +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0022 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0022.severity = silent +csharp_style_expression_bodied_methods = when_on_single_line + +# Title : Use expression body for conversion operators +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0023 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0023.severity = warning +csharp_style_expression_bodied_operators = false + +# Title : Use expression body for operators +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0024 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0024.severity = warning +csharp_style_expression_bodied_operators = false + +# Title : Use expression body for properties +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0025 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0025.severity = warning +csharp_style_expression_bodied_properties = true + +# Title : Use expression body for indexers +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0026 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0026.severity = warning +csharp_style_expression_bodied_indexers = true + +# Title : Use expression body for accessors +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0027 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0027.severity = warning +csharp_style_expression_bodied_accessors = true + +# Title : Simplify collection initialization +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0028.severity = warning +dotnet_style_collection_initializer = true + +# Title : Use coalesce expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0029 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0029.severity = warning +dotnet_style_coalesce_expression = true + +# Title : Use coalesce expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0030 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0030.severity = warning +dotnet_style_coalesce_expression = true + +# Title : Use null propagation +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0031 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0031.severity = warning +dotnet_style_null_propagation = true + +# Title : Use auto property +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0032 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0032.severity = warning +dotnet_style_prefer_auto_properties = true + +# Title : Use explicitly provided tuple name +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0033 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0033.severity = warning +dotnet_style_explicit_tuple_names = true + +# Title : Simplify 'default' expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0034 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0034.severity = warning +csharp_prefer_simple_default_expression = true + +# Title : Unreachable code detected +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0035 +# Tags : Telemetry, EnforceOnBuild_Never, NotConfigurable, Unnecessary +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line +dotnet_diagnostic.IDE0035.severity = warning + +# Title : Order modifiers +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0036 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0036.severity = silent +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async + +# Title : Use inferred member name +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0037 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled, Unnecessary +dotnet_diagnostic.IDE0037.severity = silent +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true + +# Title : Use local function +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0039 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0039.severity = silent +csharp_style_prefer_local_over_anonymous_function = false + +# Title : Add accessibility modifiers +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0040 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0040.severity = warning +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Title : Use 'is null' check +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0041 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0041.severity = warning +dotnet_style_prefer_is_null_check_over_reference_equality_method = true + +# Title : Deconstruct variable declaration +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0042 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0042.severity = silent +csharp_style_deconstructed_variable_declaration = true + +# Title : Invalid format string +# Category : Compiler +# Tags : EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0043.severity = warning + +# Title : Add readonly modifier +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0044 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0044.severity = warning +dotnet_style_readonly_field = true:warning + +# Title : Convert to conditional expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0045 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0045.severity = silent +dotnet_style_prefer_conditional_expression_over_assignment = true + +# Title : Convert to conditional expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0046 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0046.severity = silent +dotnet_style_prefer_conditional_expression_over_return = true + +# Title : Remove unnecessary parentheses +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0047 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0047.severity = silent +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Title : Add parentheses for clarity +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0048 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0048.severity = silent +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Title : Use language keywords instead of framework type names for type references +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0049 +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line. This metadata was entered manually, since it is not exposed in the normal way from the analyzer assembly. +dotnet_diagnostic.IDE0049.severity = none + +# Title : Convert to tuple +# Category : Style +# Tags : Telemetry +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line +dotnet_diagnostic.IDE0050.severity = silent + +# Title : Remove unused private members +# Category : CodeQuality +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended, Unnecessary +dotnet_diagnostic.IDE0051.severity = none + +# Title : Remove unread private members +# Category : CodeQuality +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0052 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended, Unnecessary +dotnet_diagnostic.IDE0052.severity = warning + +# Title : Use expression body for lambda expressions +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0053 +# Tags : Telemetry, EnforceOnBuild_Recommended +# Comment : This metadata was entered manually, since it is not exposed in the normal way from the analyzer assembly. +dotnet_diagnostic.IDE0053.severity = suggestion +csharp_style_expression_bodied_lambdas = when_on_single_line + +# Title : Use compound assignment +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0054 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0054.severity = warning +dotnet_style_prefer_compound_assignment = true + +# Title : Fix formatting +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0055.severity = warning +dotnet_style_namespace_match_folder = true +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_labels = flush_left +csharp_indent_switch_labels = true +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = false + +# Title : Use index operator +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0056 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0056.severity = silent +csharp_style_prefer_index_operator = true + +# Title : Use range operator +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0057 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0057.severity = silent +csharp_style_prefer_range_operator = true:warning + +# Title : Expression value is never used +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0058 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0058.severity = none + +# Title : Unnecessary assignment of a value +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0059 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended, Unnecessary +dotnet_diagnostic.IDE0059.severity = none + +# Title : Remove unused parameter +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0060 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended, Unnecessary +dotnet_diagnostic.IDE0060.severity = warning +dotnet_code_quality_unused_parameters = all + +# Title : Use expression body for local functions +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0061 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0061.severity = warning +csharp_style_expression_bodied_local_functions = true + +# Title : Make local function 'static' +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0062 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0062.severity = warning +csharp_prefer_static_local_function = true + +# Title : Use simple 'using' statement +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0063 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0063.severity = warning +csharp_prefer_simple_using_statement = true + +# Title : Make readonly fields writable +# Category : CodeQuality +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0064 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0064.severity = warning + +# Title : Misplaced using directive +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0065 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0065.severity = warning +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true +csharp_using_directive_placement = outside_namespace:suggestion + +# Title : Convert switch statement to expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0066 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0066.severity = warning +csharp_style_prefer_switch_expression = true:warning + +# Title : Use 'System.HashCode' +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0070 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0070.severity = warning + +# Title : Simplify interpolation +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0071 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0071.severity = warning +dotnet_style_prefer_simplified_interpolation = true + +# Title : Add missing cases +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0072 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0072.severity = silent + +# Title : The file header is missing or not located at the top of the file +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0073 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0073.severity = warning + +# Title : Use compound assignment +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0074 +# Tags : Telemetry, EnforceOnBuild_Recommended +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line +dotnet_diagnostic.IDE0074.severity = suggestion +dotnet_style_prefer_compound_assignment = true + +# Title : Simplify conditional expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0075 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0075.severity = warning +dotnet_style_prefer_simplified_boolean_expressions = true + +# Title : Invalid global 'SuppressMessageAttribute' +# Category : CodeQuality +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0076 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended, Unnecessary +dotnet_diagnostic.IDE0076.severity = warning + +# Title : Avoid legacy format target in 'SuppressMessageAttribute' +# Category : CodeQuality +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0077 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0077.severity = warning + +# Title : Use pattern matching +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0078 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0078.severity = silent +csharp_style_prefer_pattern_matching = true + +# Title : Remove unnecessary suppression +# Category : Style +# Tags : Telemetry +# Comment : This diagnostic only triggers in Visual Studio, not when building from the command-line +dotnet_diagnostic.IDE0079.severity = suggestion +dotnet_remove_unnecessary_suppression_exclusions = CS0618 + +# Title : Remove unnecessary suppression operator +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0080 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0080.severity = warning + +# Title : 'typeof' can be converted to 'nameof' +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0082 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0082.severity = warning + +# Title : Use pattern matching +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0083 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0083.severity = silent +csharp_style_prefer_not_pattern = true + +# Title : Use 'new(...)' +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0090 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0090.severity = warning +csharp_style_implicit_object_creation_when_type_is_apparent = true + +# Title : Remove redundant equality +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0100 +# Tags : Telemetry, EnforceOnBuild_Recommended +# Comment : S1125 triggers in more cases +dotnet_diagnostic.IDE0100.severity = none + +# Title : Remove unnecessary discard +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0110 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0110.severity = warning + +# Title : Simplify LINQ expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0120 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0120.severity = silent + +# Title : Namespace does not match folder structure +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0130 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0130.severity = silent + +# Title : Prefer 'null' check over type check +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0150 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0150.severity = warning +csharp_style_prefer_null_check_over_type_check = true + +# Title : Convert to block scoped namespace +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0160 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0160.severity = silent +csharp_style_namespace_declarations = file_scoped + +# Title : Convert to file-scoped namespace +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0161 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0161.severity = silent +csharp_style_namespace_declarations = file_scoped + +# Title : Property pattern can be simplified +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0170 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0170.severity = silent +csharp_style_prefer_extended_property_pattern = true + +# Title : Use tuple to swap values +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0180 +# Tags : Telemetry, EnforceOnBuild_HighlyRecommended +dotnet_diagnostic.IDE0180.severity = silent +csharp_style_prefer_tuple_swap = true + +# Title : Null check can be simplified +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0190 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0190.severity = silent + +# Title : Remove unnecessary lambda expression +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0200 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0200.severity = silent + +# Title : Convert to top-level statements +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0210 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0210.severity = silent + +# Title : Convert to 'Program.Main' style program +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0211 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0211.severity = silent + +# Title : Add explicit cast +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0220 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0220.severity = silent + +# Title : Use UTF-8 string literal +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0230 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE0230.severity = silent + +# Title : Remove redundant nullable directive +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0240 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0240.severity = warning + +# Title : Remove unnecessary nullable directive +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0241 +# Tags : Telemetry, EnforceOnBuild_Recommended, Unnecessary +dotnet_diagnostic.IDE0241.severity = warning + +# Title : Make struct 'readonly' +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0250 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE0250.severity = warning + +# Title : Delegate invocation can be simplified. +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide1005 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE1005.severity = warning +csharp_style_conditional_delegate_call = true + +# Title : Naming Styles +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide1006 +# Tags : Telemetry, EnforceOnBuild_Recommended +dotnet_diagnostic.IDE1006.severity = warning +dotnet_naming_rule.interface_should_be_ipascalcase.severity = warning +dotnet_naming_rule.interface_should_be_ipascalcase.symbols = interface +dotnet_naming_rule.interface_should_be_ipascalcase.style = ipascalcase +dotnet_naming_rule.types_should_be_pascalcase.severity = warning +dotnet_naming_rule.types_should_be_pascalcase.symbols = types +dotnet_naming_rule.types_should_be_pascalcase.style = pascalcase +dotnet_naming_rule.constant_should_be_pascalcase.severity = warning +dotnet_naming_rule.constant_should_be_pascalcase.symbols = constant +dotnet_naming_rule.constant_should_be_pascalcase.style = pascalcase +dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = warning +dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase +dotnet_naming_rule.method_parameter_should_be_camelcase.severity = warning +dotnet_naming_rule.method_parameter_should_be_camelcase.symbols = method_parameter +dotnet_naming_rule.method_parameter_should_be_camelcase.style = camelcase +dotnet_naming_rule.local_variable_should_be_camelcase.severity = warning +dotnet_naming_rule.local_variable_should_be_camelcase.symbols = local_variable +dotnet_naming_rule.local_variable_should_be_camelcase.style = camelcase +dotnet_naming_rule.type_parameter_should_be_tpascalcase.severity = warning +dotnet_naming_rule.type_parameter_should_be_tpascalcase.symbols = type_parameter +dotnet_naming_rule.type_parameter_should_be_tpascalcase.style = tpascalcase +dotnet_naming_rule.private_field_should_be__camelcase.severity = warning +dotnet_naming_rule.private_field_should_be__camelcase.symbols = private_field +dotnet_naming_rule.private_field_should_be__camelcase.style = _camelcase +dotnet_naming_rule.field_should_be_pascalcase.severity = warning +dotnet_naming_rule.field_should_be_pascalcase.symbols = field +dotnet_naming_rule.field_should_be_pascalcase.style = pascalcase +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = +dotnet_naming_symbols.method_parameter.applicable_kinds = parameter +dotnet_naming_symbols.method_parameter.applicable_accessibilities = * +dotnet_naming_symbols.method_parameter.required_modifiers = +dotnet_naming_symbols.local_variable.applicable_kinds = local +dotnet_naming_symbols.local_variable.applicable_accessibilities = local +dotnet_naming_symbols.local_variable.required_modifiers = +dotnet_naming_symbols.type_parameter.applicable_kinds = type_parameter +dotnet_naming_symbols.type_parameter.applicable_accessibilities = * +dotnet_naming_symbols.type_parameter.required_modifiers = +dotnet_naming_symbols.constant.applicable_kinds = field, local +dotnet_naming_symbols.constant.applicable_accessibilities = * +dotnet_naming_symbols.constant.required_modifiers = const +dotnet_naming_symbols.private_field.applicable_kinds = field +dotnet_naming_symbols.private_field.applicable_accessibilities = private +dotnet_naming_symbols.private_field.required_modifiers = +dotnet_naming_symbols.field.applicable_kinds = field +dotnet_naming_symbols.field.applicable_accessibilities = public, internal, protected, protected_internal, private_protected, local +dotnet_naming_symbols.field.required_modifiers = +dotnet_naming_style.pascalcase.required_prefix = +dotnet_naming_style.pascalcase.required_suffix = +dotnet_naming_style.pascalcase.word_separator = +dotnet_naming_style.pascalcase.capitalization = pascal_case +dotnet_naming_style.ipascalcase.required_prefix = I +dotnet_naming_style.ipascalcase.required_suffix = +dotnet_naming_style.ipascalcase.word_separator = +dotnet_naming_style.ipascalcase.capitalization = pascal_case +dotnet_naming_style.camelcase.required_prefix = +dotnet_naming_style.camelcase.required_suffix = +dotnet_naming_style.camelcase.word_separator = +dotnet_naming_style.camelcase.capitalization = camel_case +dotnet_naming_style._camelcase.required_prefix = _ +dotnet_naming_style._camelcase.required_suffix = +dotnet_naming_style._camelcase.word_separator = +dotnet_naming_style._camelcase.capitalization = camel_case +dotnet_naming_style.tpascalcase.required_prefix = T +dotnet_naming_style.tpascalcase.required_suffix = +dotnet_naming_style.tpascalcase.word_separator = +dotnet_naming_style.tpascalcase.capitalization = pascal_case + +# Title : Avoid multiple blank lines +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2000 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE2000.severity = warning +dotnet_style_allow_multiple_blank_lines_experimental = false + +# Title : Embedded statements must be on their own line +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2001 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE2001.severity = warning + +# Title : Consecutive braces must not have blank line between them +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2002 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE2002.severity = warning + +# Title : Blank line required between block and subsequent statement +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2003 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE2003.severity = silent + +# Title : Blank line not allowed after constructor initializer colon +# Category : Style +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2004 +# Tags : Telemetry, EnforceOnBuild_WhenExplicitlyEnabled +dotnet_diagnostic.IDE2004.severity = warning + +# Title : Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +# Category : Trimming +dotnet_diagnostic.IL2026.severity = warning + +# Title : The value passed as the assembly name or type name to the CreateInstance method can't be statically analyzed. +# Category : Trimming +dotnet_diagnostic.IL2032.severity = warning + +# Title : The 'DynamicallyAccessedMembersAttribute' is not allowed on methods. It is allowed on method return value or method parameters. +# Category : Trimming +dotnet_diagnostic.IL2041.severity = warning + +# Title : 'DynamicallyAccessedMembersAttribute' on property conflicts with the same attribute on its accessor. +# Category : Trimming +dotnet_diagnostic.IL2043.severity = warning + +# Title : 'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides. +# Category : Trimming +dotnet_diagnostic.IL2046.severity = warning + +# Title : Correctness of COM interop cannot be guaranteed after trimming. Interfaces and interface members might be removed. +# Category : Trimming +dotnet_diagnostic.IL2050.severity = warning + +# Title : Either the type on which the MakeGenericType is called can't be statically determined, or the type parameters to be used for generic arguments can't be statically determined. +# Category : Trimming +dotnet_diagnostic.IL2055.severity = warning + +# Title : Unrecognized value passed to the parameter of method. It's not possible to guarantee the availability of the target type. +# Category : Trimming +dotnet_diagnostic.IL2057.severity = warning + +# Title : Parameters passed to method cannot be analyzed. Consider using methods 'System.Type.GetType' and `System.Activator.CreateInstance` instead. +# Category : Trimming +dotnet_diagnostic.IL2058.severity = warning + +# Title : The type passed to the RunClassConstructor is not statically known, Trimmer can't make sure that its static constructor is available. +# Category : Trimming +dotnet_diagnostic.IL2059.severity = warning + +# Title : Call to 'System.Reflection.MethodInfo.MakeGenericMethod' can not be statically analyzed. It's not possible to guarantee the availability of requirements of the generic method. +# Category : Trimming +dotnet_diagnostic.IL2060.severity = warning + +# Title : The parameter of method has a DynamicallyAccessedMembersAttribute, but the value passed to it can not be statically analyzed. +# Category : Trimming +dotnet_diagnostic.IL2062.severity = warning + +# Title : The return value of method has a DynamicallyAccessedMembersAttribute, but the value returned from the method can not be statically analyzed. +# Category : Trimming +dotnet_diagnostic.IL2063.severity = warning + +# Title : The field has a DynamicallyAccessedMembersAttribute, but the value assigned to it can not be statically analyzed. +# Category : Trimming +dotnet_diagnostic.IL2064.severity = warning + +# Title : The method has a DynamicallyAccessedMembersAttribute (which applies to the implicit 'this' parameter), but the value used for the 'this' parameter can not be statically analyzed. +# Category : Trimming +dotnet_diagnostic.IL2065.severity = warning + +# Title : The generic parameter of type or method has a DynamicallyAccessedMembersAttribute, but the value used for it can not be statically analyzed. +# Category : Trimming +dotnet_diagnostic.IL2066.severity = warning + +# Title : Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The parameter of method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2067.severity = warning + +# Title : Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The parameter of method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2068.severity = warning + +# Title : Value stored in field does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The parameter of method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2069.severity = warning + +# Title : 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The parameter of method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2070.severity = warning + +# Title : Generic argument does not satisfy 'DynamicallyAccessedMembersAttribute' in target method or type. The parameter of method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2071.severity = warning + +# Title : Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2072.severity = warning + +# Title : Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The return value of the source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2073.severity = warning + +# Title : Value stored in field does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The return value of the source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2074.severity = warning + +# Title : 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2075.severity = warning + +# Title : Target generic argument does not satisfy 'DynamicallyAccessedMembersAttribute' in target method or type. The return value of the source method does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. +# Category : Trimming +dotnet_diagnostic.IL2076.severity = warning + +# Title : Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The source field does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2077.severity = warning + +# Title : Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The source field does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2078.severity = warning + +# Title : Value stored in target field does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The source field does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2079.severity = warning + +# Title : 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The source field does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2080.severity = warning + +# Title : Target generic argument does not satisfy 'DynamicallyAccessedMembersAttribute' in target method or type. The source field does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2081.severity = warning + +# Title : Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The implicit 'this' argument of source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2082.severity = warning + +# Title : Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The implicit 'this' argument of source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2083.severity = warning + +# Title : Value stored in target field does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The implicit 'this' argument of source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2084.severity = warning + +# Title : 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The implicit 'this' argument of source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2085.severity = warning + +# Title : Target generic argument does not satisfy 'DynamicallyAccessedMembersAttribute' in target method or type. The implicit 'this' argument of source method does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2086.severity = warning + +# Title : Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The generic parameter of the source method or type does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2087.severity = warning + +# Title : Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The generic parameter of the source method or type does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2088.severity = warning + +# Title : Value stored in target field does not satisfy 'DynamicallyAccessedMembersAttribute' requirements. The generic parameter of the source method or type does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2089.severity = warning + +# Title : 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The generic parameter of the source method or type does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2090.severity = warning + +# Title : Target generic argument does not satisfy 'DynamicallyAccessedMembersAttribute' in target method or type. The generic parameter of the source method or type does not have matching annotations. +# Category : Trimming +dotnet_diagnostic.IL2091.severity = warning + +# Title : 'DynamicallyAccessedMemberTypes' on the parameter of method don't match overridden parameter of method. All overridden members must have the same 'DynamicallyAccessedMembersAttribute' usage. +# Category : Trimming +dotnet_diagnostic.IL2092.severity = warning + +# Title : 'DynamicallyAccessedMemberTypes' on the return value of method don't match overridden return value of method. All overridden members must have the same 'DynamicallyAccessedMembersAttribute' usage. +# Category : Trimming +dotnet_diagnostic.IL2093.severity = warning + +# Title : 'DynamicallyAccessedMemberTypes' on the implicit 'this' parameter of method don't match overridden implicit 'this' parameter of method. All overridden members must have the same 'DynamicallyAccessedMembersAttribute' usage. +# Category : Trimming +dotnet_diagnostic.IL2094.severity = warning + +# Title : 'DynamicallyAccessedMemberTypes' on the generic parameter of method or type don't match overridden generic parameter method or type. All overridden members must have the same 'DynamicallyAccessedMembersAttribute' usage. +# Category : Trimming +dotnet_diagnostic.IL2095.severity = warning + +# Title : Call to 'Type.GetType' method can perform case insensitive lookup of the type, currently ILLink can not guarantee presence of all the matching types. +# Category : Trimming +dotnet_diagnostic.IL2096.severity = warning + +# Title : Field has 'DynamicallyAccessedMembersAttribute', but that attribute can only be applied to fields of type 'System.Type' or 'System.String'. +# Category : Trimming +dotnet_diagnostic.IL2097.severity = warning + +# Title : Parameter of method has 'DynamicallyAccessedMembersAttribute', but that attribute can only be applied to parameters of type 'System.Type' or 'System.String'. +# Category : Trimming +dotnet_diagnostic.IL2098.severity = warning + +# Title : Property has 'DynamicallyAccessedMembersAttribute', but that attribute can only be applied to properties of type 'System.Type' or 'System.String'. +# Category : Trimming +dotnet_diagnostic.IL2099.severity = warning + +# Title : Value passed to the parameter of method cannot be statically determined as a property accessor. +# Category : Trimming +dotnet_diagnostic.IL2103.severity = warning + +# Title : Return type of method has 'DynamicallyAccessedMembersAttribute', but that attribute can only be applied to properties of type 'System.Type' or 'System.String'. +# Category : Trimming +dotnet_diagnostic.IL2106.severity = warning + +# Title : Types that derive from a base class with 'RequiresUnreferencedCodeAttribute' need to explicitly use the 'RequiresUnreferencedCodeAttribute' or suppress this warning +# Category : Trimming +dotnet_diagnostic.IL2109.severity = warning + +# Title : Field with 'DynamicallyAccessedMembersAttribute' is accessed via reflection. Trimmer can't guarantee availability of the requirements of the field. +# Category : Trimming +dotnet_diagnostic.IL2110.severity = warning + +# Title : Method with parameters or return value with `DynamicallyAccessedMembersAttribute` is accessed via reflection. Trimmer can't guarantee availability of the requirements of the method. +# Category : Trimming +dotnet_diagnostic.IL2111.severity = warning + +# Title : The use of 'RequiresUnreferencedCodeAttribute' on static constructors is disallowed since is a method not callable by the user, is only called by the runtime. Placing the attribute directly on the static constructor will have no effect, instead use 'RequiresUnreferencedCodeAttribute' on the type which will handle warning and silencing from the static constructor. +# Category : Trimming +dotnet_diagnostic.IL2116.severity = warning + +# Title : Avoid accessing Assembly file path when publishing as a single file +# Category : SingleFile +dotnet_diagnostic.IL3000.severity = warning + +# Title : Avoid using accessing Assembly file path when publishing as a single-file +# Category : Publish +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/il3000 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.IL3000.severity = warning + +# Title : Avoid accessing Assembly file path when publishing as a single file +# Category : SingleFile +dotnet_diagnostic.IL3001.severity = warning + +# Title : Avoid using accessing Assembly file path when publishing as a single-file +# Category : Publish +# Help Link: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/il3001 +# Tags : Telemetry, EnabledRuleInAggressiveMode +dotnet_diagnostic.IL3001.severity = warning + +# Title : Avoid calling members marked with 'RequiresAssemblyFilesAttribute' when publishing as a single-file +# Category : SingleFile +dotnet_diagnostic.IL3002.severity = warning + +# Title : 'RequiresAssemblyFilesAttribute' annotations must match across all interface implementations or overrides. +# Category : SingleFile +dotnet_diagnostic.IL3003.severity = warning + +# Title : The use of 'RequiresAssemblyFilesAttribute' on static constructors is disallowed since is a method not callable by the user, is only called by the runtime. Placing the attribute directly on the static constructor will have no effect, instead use 'RequiresUnreferencedCodeAttribute' on the type which will handle warning and silencing from the static constructor. +# Category : SingleFile +dotnet_diagnostic.IL3004.severity = warning + +# Title : Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. +# Category : AOT +dotnet_diagnostic.IL3050.severity = warning + +# Title : 'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides. +# Category : AOT +dotnet_diagnostic.IL3051.severity = warning + +# Title : The use of 'RequiresDynamicCodeAttribute' on static constructors is disallowed since is a method not callable by the user, is only called by the runtime. Placing the attribute directly on the static constructor will have no effect, instead use 'RequiresUnreferencedCodeAttribute' on the type which will handle warning and silencing from the static constructor. +# Category : AOT +dotnet_diagnostic.IL3056.severity = warning + +# Title : Use source generated logging methods for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a000 +dotnet_diagnostic.R9A000.severity = none + +# Title : Use 'Microsoft.IO.RecyclableMemoryStream' instead of 'System.IO.MemoryStream' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a001 +dotnet_diagnostic.R9A001.severity = none + +# Title : Use higher performance methods from 'IExtendedDistributedCache' +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a003 +dotnet_diagnostic.R9A003.severity = none + +# Title : Use the 'Microsoft.R9.Extensions.Caching.Redis' package instead of 'Microsoft.Extensions.Caching.StackExchangeRedis' +# Category : Resilience +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a005 +dotnet_diagnostic.R9A005.severity = warning + +# Title : Update return type to match metric type +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a006 +dotnet_diagnostic.R9A006.severity = error + +# Title : Remove method body +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a007 +dotnet_diagnostic.R9A007.severity = error + +# Title : Add a parameter of type 'IMeter' to the method declaration +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a008 +dotnet_diagnostic.R9A008.severity = error + +# Title : Update method parameters for dimensions to be string type +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a009 +dotnet_diagnostic.R9A009.severity = error + +# Title : Make method static +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a010 +dotnet_diagnostic.R9A010.severity = error + +# Title : Make method partial +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a011 +dotnet_diagnostic.R9A011.severity = error + +# Title : Make method public +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a012 +dotnet_diagnostic.R9A012.severity = warning + +# Title : Seal non-public classes for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a013 +dotnet_diagnostic.R9A013.severity = none + +# Title : Use the 'Microsoft.R9.Extensions.Diagnostics.Throws' class instead of explicitly throwing exception for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a014 +dotnet_diagnostic.R9A014.severity = none + +# Title : Use the 'Microsoft.R9.Extensions.Diagnostics.Throws' class instead of explicitly throwing exception for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a015 +dotnet_diagnostic.R9A015.severity = none + +# Title : Use eager options validation +# Category : Reliability +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a016 +dotnet_diagnostic.R9A016.severity = none + +# Title : Use asynchronous operations instead of legacy thread blocking code +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a017 +dotnet_diagnostic.R9A017.severity = none + +# Title : Use 'Microsoft.R9.Extensions.Text.CompositeFormat' instead of 'string.Format' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a018 +dotnet_diagnostic.R9A018.severity = none + +# Title : Remove unnecessary dictionary lookups +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a019 +dotnet_diagnostic.R9A019.severity = none + +# Title : Remove unnecessary set lookups +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a020 +dotnet_diagnostic.R9A020.severity = none + +# Title : Perform message formatting in the body of the logging method +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a021 +dotnet_diagnostic.R9A021.severity = none + +# Title : Use 'System.TimeProvider' to make the code easier to test +# Category : Reliability +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a022 +dotnet_diagnostic.R9A022.severity = none + +# Title : Use 'Microsoft.R9.Extensions.Time.PerfStopwatch' instead of 'System.Diagnostics.Stopwatch' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a023 +dotnet_diagnostic.R9A023.severity = none + +# Title : Propagate data classification +# Category : Privacy +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a024 +dotnet_diagnostic.R9A024.severity = none + +# Title : Use fixed format for 'System.ObsoleteAttribute' message +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a025 +dotnet_diagnostic.R9A025.severity = none + +# Title : Minimum deprecation period for an obsolete public API +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a026 +dotnet_diagnostic.R9A026.severity = none + +# Title : Use fixed API surface format for soft deleted members +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a027 +dotnet_diagnostic.R9A027.severity = none + +# Title : Argument provided for user input parameter on user data vending API is not from any request context +# Category : Privacy +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a028 +dotnet_diagnostic.R9A028.severity = none + +# Title : Using experimental API +# Category : Reliability +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a029 +dotnet_diagnostic.R9A029.severity = none + +# Title : Use the character-based overloads of 'String.StartsWith' or 'String.EndsWith' +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a030 +dotnet_diagnostic.R9A030.severity = none + +# Title : Make types declared in an executable internal +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a031 +dotnet_diagnostic.R9A031.severity = none + +# Title : Consider using an array instead of a collection +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a032 +dotnet_diagnostic.R9A032.severity = none + +# Title : Replace uses of 'Enum.GetName' and 'Enum.ToString' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a033 +dotnet_diagnostic.R9A033.severity = none + +# Title : Optimize method group use to avoid allocations +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a034 +dotnet_diagnostic.R9A034.severity = none + +# Title : Make struct readonly +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a035 +dotnet_diagnostic.R9A035.severity = none + +# Title : Use 'Microsoft.R9.Extensions.Text.NumericExtensions.ToInvariantString' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a036 +dotnet_diagnostic.R9A036.severity = none + +# Title : Use 'System.ValueTuple' instead of 'System.Tuple' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a037 +dotnet_diagnostic.R9A037.severity = none + +# Title : Use 'Microsoft.R9.Extensions.Pools.PoolFactory' instead for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a038 +dotnet_diagnostic.R9A038.severity = none + +# Title : Remove superfluous null checks when compiling in a nullable context +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a039 +dotnet_diagnostic.R9A039.severity = none + +# Title : Use generic collections instead of legacy collections for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a040 +dotnet_diagnostic.R9A040.severity = none + +# Title : Use concrete types when possible for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a041 +dotnet_diagnostic.R9A041.severity = none + +# Title : Annotate all User Data APIs parameters +# Category : Privacy +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a042 +dotnet_diagnostic.R9A042.severity = warning + +# Title : Use 'Microsoft.R9.Extensions.Text.StringSplitExtensions.TrySplit' for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a043 +dotnet_diagnostic.R9A043.severity = none + +# Title : Assign array of literal values to a static field for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a044 +dotnet_diagnostic.R9A044.severity = none + +# Title : Use 'Array.Empty' instead of allocating a 0-element array for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a045 +dotnet_diagnostic.R9A045.severity = none + +# Title : Source generated metrics (fast metrics) should be located in 'Metric' class +# Category : Readability +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a046 +dotnet_diagnostic.R9A046.severity = warning + +# Title : Do not use manual metrics, use fast (source generated) metrics instead for better performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a047 +dotnet_diagnostic.R9A047.severity = none + +# Title : Use the 'Count' or 'Length' properties instead of the 'Any' method for improved performance +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a048 +dotnet_diagnostic.R9A048.severity = none + +# Title : Newly added API must be annotated with experimental attribute +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a049 +dotnet_diagnostic.R9A049.severity = none + +# Title : An experimental API was marked as obsolete +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a050 +dotnet_diagnostic.R9A050.severity = none + +# Title : A stable API was marked as experimental +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a051 +dotnet_diagnostic.R9A051.severity = none + +# Title : A stable API was deleted outside the deprecation period +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a052 +dotnet_diagnostic.R9A052.severity = none + +# Title : A deprecated API is not annotated with the obsolete attribute +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a053 +dotnet_diagnostic.R9A053.severity = none + +# Title : A deprecated API is marked as experimental +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a054 +dotnet_diagnostic.R9A054.severity = none + +# Title : The signature of a stable API has changed +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a055 +dotnet_diagnostic.R9A055.severity = none + +# Title : Fire-and-forget async call inside a 'using' block +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a056 +dotnet_diagnostic.R9A056.severity = warning + +# Title : Use consistent versions of R9 assemblies +# Category : Correctness +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a057 +dotnet_diagnostic.R9A057.severity = warning + +# Title : Consider removing unnecessary conditional access operator (?) +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a058 +dotnet_diagnostic.R9A058.severity = suggestion + +# Title : Consider removing unnecessary null coalescing assignment (??=) +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a059 +dotnet_diagnostic.R9A059.severity = suggestion + +# Title : Consider removing unnecessary null coalescing operator (??) +# Category : Performance +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a060 +dotnet_diagnostic.R9A060.severity = suggestion + +# Title : The async method doesn't support cancellation +# Category : Resilience +# Help Link: https://eng.ms/docs/experiences-devices/r9-sdk/docs/static-analysis/analyzers/r9a061 +dotnet_diagnostic.R9A061.severity = none + +# Title : +# Category : Style +# Tags : Telemetry, EnforceOnBuild_Never, NotConfigurable +dotnet_diagnostic.RemoveUnnecessaryImportsFixable.severity = silent + +# Title : Methods and properties should be named in PascalCase +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-100 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S100.severity = none + +# Title : Method overrides should not change parameter defaults +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1006 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1006.severity = warning + +# Title : Types should be named in PascalCase +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-101 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S101.severity = none + +# Title : Lines should not be too long +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-103 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S103.severity = warning + +# Title : Files should not have too many lines of code +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-104 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S104.severity = warning + +# Title : Destructors should not throw exceptions +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1048 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1048.severity = none + +# Title : Tabulation characters should not be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-105 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S105.severity = none + +# Title : Standard outputs should not be used directly to log anything +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-106 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S106.severity = none + +# Title : Collapsible "if" statements should be merged +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1066 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1066.severity = none + +# Title : Expressions should not be too complex +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1067 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1067.severity = warning + +# Title : Methods should not have too many parameters +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-107 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S107.severity = warning + +# Title : URIs should not be hardcoded +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1075 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1075.severity = warning + +# Title : Nested blocks of code should not be left empty +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-108 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S108.severity = warning + +# Title : Magic numbers should not be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-109 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S109.severity = warning + +# Title : Inheritance tree of classes should not be too deep +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-110 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S110.severity = warning + +# Title : Fields should not have public accessibility +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1104 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1104.severity = none + +# Title : A close curly brace should be located at the beginning of a line +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1109 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1109.severity = none + +# Title : Redundant pairs of parentheses should be removed +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1110 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1110.severity = none + +# Title : Empty statements should be removed +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1116 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1116.severity = none + +# Title : Local variables should not shadow class fields +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1117 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1117.severity = warning + +# Title : Utility classes should not have public constructors +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1118 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1118.severity = none + +# Title : General exceptions should never be thrown +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-112 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S112.severity = none + +# Title : Assignments should not be made from within sub-expressions +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1121 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1121.severity = warning + +# Title : "Obsolete" attributes should include explanations +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1123 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1123.severity = none + +# Title : Boolean literals should not be redundant +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1125 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1125.severity = warning + +# Title : Unused "using" should be removed +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1128 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1128.severity = warning + +# Title : Files should contain an empty newline at the end +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-113 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S113.severity = none + +# Title : Track uses of "FIXME" tags +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1134 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1134.severity = warning + +# Title : Track uses of "TODO" tags +# Category : Info Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1135 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1135.severity = warning + +# Title : Unused private types or members should be removed +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1144 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay, Unnecessary +dotnet_diagnostic.S1144.severity = warning + +# Title : Useless "if(true) {...}" and "if(false){...}" blocks should be removed +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1145 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1145.severity = warning + +# Title : Exit methods should not be called +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1147 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1147.severity = warning + +# Title : "switch case" clauses should not have too many lines of code +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1151 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1151.severity = none + +# Title : "Any()" should be used to test for emptiness +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1155 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1155.severity = warning + +# Title : Exceptions should not be thrown in finally blocks +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1163 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1163.severity = none + +# Title : Empty arrays and collections should be returned instead of null +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1168 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1168.severity = warning + +# Title : Unused method parameters should be removed +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1172 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1172.severity = none + +# Title : Overriding members should do more than simply call the same member in the base class +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1185 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1185.severity = warning + +# Title : Methods should not be empty +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1186 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1186.severity = warning + +# Title : String literals should not be duplicated +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1192 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1192.severity = suggestion + +# Title : Nested code blocks should not be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1199 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1199.severity = warning + +# Title : Classes should not be coupled to too many other classes (Single Responsibility Principle) +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1200 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1200.severity = none + +# Title : "Equals(Object)" and "GetHashCode()" should be overridden in pairs +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1206 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1206.severity = none + +# Title : Control structures should use curly braces +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-121 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S121.severity = none + +# Title : "Equals" and the comparison operators should be overridden when implementing "IComparable" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1210 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1210.severity = none + +# Title : "GC.Collect" should not be called +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1215 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1215.severity = none + +# Title : Statements should be on separate lines +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-122 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S122.severity = none + +# Title : Method parameters, caught exceptions and foreach variables' initial values should not be ignored +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1226 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1226.severity = warning + +# Title : break statements should not be used except for switch cases +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1227 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1227.severity = none + +# Title : Floating point numbers should not be tested for equality +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1244 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1244.severity = error + +# Title : Sections of code should not be commented out +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-125 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S125.severity = warning + +# Title : "if ... else if" constructs should end with "else" clauses +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-126 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S126.severity = none + +# Title : A "while" loop should be used instead of a "for" loop +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1264 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1264.severity = warning + +# Title : "for" loop stop conditions should be invariant +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-127 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S127.severity = warning + +# Title : "switch" statements should have at least 3 "case" clauses +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1301 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1301.severity = none + +# Title : Track uses of in-source issue suppressions +# Category : Info Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1309 +# Tags : C#, MainSourceScope, TestSourceScope +# Comment : Suppressions are frequently necessary. +dotnet_diagnostic.S1309.severity = none + +# Title : "switch/Select" statements should contain a "default/Case Else" clauses +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-131 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S131.severity = none + +# Title : Using hardcoded IP addresses is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1313 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1313.severity = warning + +# Title : Control flow statements "if", "switch", "for", "foreach", "while", "do" and "try" should not be nested too deeply +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-134 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S134.severity = none + +# Title : Functions should not have too many lines of code +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-138 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S138.severity = none + +# Title : Culture should be specified for "string" operations +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1449 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1449.severity = warning + +# Title : Private fields only used as local variables in methods should become local variables +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1450 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1450.severity = warning + +# Title : Track lack of copyright and license headers +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1451 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1451.severity = none + +# Title : "switch" statements should not have too many "case" clauses +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1479 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1479.severity = warning + +# Title : Unused local variables should be removed +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1481 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1481.severity = none + +# Title : Methods and properties should not be too complex +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1541 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1541.severity = none + +# Title : Tests should not be ignored +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1607 +# Tags : C#, TestSourceScope, SonarWay +dotnet_diagnostic.S1607.severity = warning + +# Title : Strings should not be concatenated using '+' in a loop +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1643 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1643.severity = warning + +# Title : Variables should not be self-assigned +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1656 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1656.severity = warning + +# Title : Multiple variables should not be declared on the same line +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1659 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1659.severity = warning + +# Title : An abstract class should have both abstract and concrete methods +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1694 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1694.severity = warning + +# Title : NullReferenceException should not be caught +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1696 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1696.severity = warning + +# Title : Short-circuit logic should be used to prevent null pointer dereferences in conditionals +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1697 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1697.severity = warning + +# Title : "==" should not be used when "Equals" is overridden +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1698 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S1698.severity = warning + +# Title : Constructors should only call non-overridable methods +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1699 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1699.severity = warning + +# Title : Loops with at most one iteration should be refactored +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1751 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1751.severity = warning + +# Title : Identical expressions should not be used on both sides of a binary operator +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1764 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1764.severity = warning + +# Title : "switch" statements should not be nested +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1821 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1821.severity = none + +# Title : Objects should not be created to be dropped immediately without being used +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1848 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1848.severity = warning + +# Title : Unused assignments should be removed +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1854 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1854.severity = none + +# Title : "ToString()" calls should not be redundant +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1858 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1858.severity = warning + +# Title : Related "if/else if" statements should not have the same condition +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1862 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1862.severity = warning + +# Title : Two branches in a conditional structure should not have exactly the same implementation +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1871 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S1871.severity = warning + +# Title : Redundant casts should not be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1905 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1905.severity = none + +# Title : Inheritance list should not be redundant +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1939 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1939.severity = warning + +# Title : Boolean checks should not be inverted +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1940 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1940.severity = warning + +# Title : Inappropriate casts should not be made +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1944 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S1944.severity = warning + +# Title : "for" loop increment clauses should modify the loops' counters +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-1994 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S1994.severity = warning + +# Title : Hashes should include an unpredictable salt +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2053 +# Tags : C#, MainSourceScope, SonarWay +# Comment : Analysis is too slow +dotnet_diagnostic.S2053.severity = none + +# Title : Hard-coded credentials are security-sensitive +# Category : Blocker Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2068 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2068.severity = warning + +# Title : SHA-1 and Message-Digest hash algorithms should not be used in secure contexts +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2070 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2070.severity = none + +# Title : Formatting SQL queries is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2077 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2077.severity = warning + +# Title : Creating cookies without the "secure" flag is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2092 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2092.severity = warning + +# Title : Collections should not be passed as arguments to their own methods +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2114 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2114.severity = warning + +# Title : A secure password should be used when connecting to a database +# Category : Blocker Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2115 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2115.severity = warning + +# Title : Values should not be uselessly incremented +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2123 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2123.severity = warning + +# Title : Underscores should be used to make large numbers readable +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2148 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2148.severity = warning + +# Title : "sealed" classes should not have "protected" members +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2156 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2156.severity = warning + +# Title : Short-circuit logic should be used in boolean contexts +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2178 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2178.severity = warning + +# Title : Integral numbers should not be shifted by zero or more than their number of bits-1 +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2183 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2183.severity = warning + +# Title : Results of integer division should not be assigned to floating point variables +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2184 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2184.severity = warning + +# Title : TestCases should contain tests +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2187 +# Tags : C#, TestSourceScope, SonarWay +dotnet_diagnostic.S2187.severity = warning + +# Title : Recursion should not be infinite +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2190 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2190.severity = warning + +# Title : Modulus results should not be checked for direct equality +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2197 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2197.severity = warning + +# Title : Return values from functions without side effects should not be ignored +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2201 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2201.severity = none + +# Title : Runtime type checking should be simplified +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2219 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2219.severity = warning + +# Title : "Exception" should not be caught +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2221 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2221.severity = none + +# Title : Locks should be released on all paths +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2222 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2222.severity = warning + +# Title : Non-constant static fields should not be visible +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2223 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2223.severity = warning + +# Title : "ToString()" method should not return null +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2225 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2225.severity = warning + +# Title : Console logging should not be used +# Category : Minor Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2228 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2228.severity = none + +# Title : Parameters should be passed in the correct order +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2234 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2234.severity = warning + +# Title : Using pseudorandom number generators (PRNGs) is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2245 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2245.severity = warning + +# Title : A "for" loop update clause should move the counter in the right direction +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2251 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2251.severity = warning + +# Title : For-loop conditions should be true at least once +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2252 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2252.severity = warning + +# Title : Writing cookies is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2255 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2255.severity = warning + +# Title : Using non-standard cryptographic algorithms is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2257 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2257.severity = warning + +# Title : Null pointers should not be dereferenced +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2259 +# Tags : C#, MainSourceScope, SonarWay +# Comment : Redundant, covered by modern C# compiler +dotnet_diagnostic.S2259.severity = none + +# Title : Composite format strings should not lead to unexpected behavior at runtime +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2275 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2275.severity = warning + +# Title : Neither DES (Data Encryption Standard) nor DESede (3DES) should be used +# Category : Blocker Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2278 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2278.severity = warning + +# Title : Field-like events should not be virtual +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2290 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2290.severity = warning + +# Title : Overflow checking should not be disabled for "Enumerable.Sum" +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2291 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2291.severity = warning + +# Title : Trivial properties should be auto-implemented +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2292 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2292.severity = none + +# Title : "nameof" should be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2302 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2302.severity = warning + +# Title : "async" and "await" should not be used as identifiers +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2306 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2306.severity = warning + +# Title : Methods and properties that don't access instance data should be static +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2325 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2325.severity = none + +# Title : Unused type parameters should be removed +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2326 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +# Comment : Valid pattern used in a number of places +dotnet_diagnostic.S2326.severity = none + +# Title : "try" statements with identical "catch" and/or "finally" blocks should be merged +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2327 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2327.severity = warning + +# Title : "GetHashCode" should not reference mutable fields +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2328 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2328.severity = warning + +# Title : Array covariance should not be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2330 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2330.severity = warning + +# Title : Redundant modifiers should not be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2333 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2333.severity = warning + +# Title : Public constant members should not be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2339 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2339.severity = none + +# Title : Enumeration types should comply with a naming convention +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2342 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2342.severity = none + +# Title : Enumeration type names should not have "Flags" or "Enum" suffixes +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2344 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2344.severity = warning + +# Title : Flags enumerations should explicitly initialize all their members +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2345 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2345.severity = warning + +# Title : Flags enumerations zero-value members should be named "None" +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2346 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2346.severity = none + +# Title : Fields should be private +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2357 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2357.severity = none + +# Title : Optional parameters should not be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2360 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2360.severity = none + +# Title : Properties should not make collection or array copies +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2365 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2365.severity = warning + +# Title : Public methods should not have multidimensional array parameters +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2368 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2368.severity = warning + +# Title : Exceptions should not be thrown from property getters +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2372 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2372.severity = warning + +# Title : Write-only properties should not be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2376 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2376.severity = warning + +# Title : Mutable fields should not be "public static" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2386 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2386.severity = warning + +# Title : Child class fields should not shadow parent class fields +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2387 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2387.severity = warning + +# Title : Types and methods should not have too many generic parameters +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2436 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2436.severity = warning + +# Title : Silly bit operations should not be performed +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2437 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay, Unnecessary +dotnet_diagnostic.S2437.severity = warning + +# Title : Whitespace and control characters in string literals should be explicit +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2479 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2479.severity = warning + +# Title : Generic exceptions should not be ignored +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2486 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2486.severity = warning + +# Title : Shared resources should not be used for locking +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2551 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2551.severity = warning + +# Title : Conditionally executed code should be reachable +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2583 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2583.severity = none + +# Title : Boolean expressions should not be gratuitous +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2589 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2589.severity = warning + +# Title : Setting loose file permissions is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2612 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2612.severity = warning + +# Title : The length returned from a stream read should be checked +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2674 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2674.severity = warning + +# Title : Multiline blocks should be enclosed in curly braces +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2681 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2681.severity = warning + +# Title : "NaN" should not be used in comparisons +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2688 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2688.severity = warning + +# Title : "IndexOf" checks should not be for positive numbers +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2692 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2692.severity = warning + +# Title : Instance members should not write to "static" fields +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2696 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2696.severity = warning + +# Title : Tests should include assertions +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2699 +# Tags : C#, TestSourceScope, SonarWay +dotnet_diagnostic.S2699.severity = warning + +# Title : Literal boolean values should not be used in assertions +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2701 +# Tags : C#, TestSourceScope +dotnet_diagnostic.S2701.severity = warning + +# Title : "catch" clauses should do more than rethrow +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2737 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2737.severity = warning + +# Title : Static fields should not be used in generic types +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2743 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2743.severity = none + +# Title : XML parsers should not be vulnerable to XXE attacks +# Category : Blocker Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2755 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2755.severity = warning + +# Title : "=+" should not be used instead of "+=" +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2757 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2757.severity = warning + +# Title : The ternary operator should not return the same value regardless of the condition +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2758 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2758.severity = warning + +# Title : Sequential tests should not check the same condition +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2760 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2760.severity = warning + +# Title : Doubled prefix operators "!!" and "~~" should not be used +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2761 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2761.severity = warning + +# Title : SQL keywords should be delimited by whitespace +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2857 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2857.severity = warning + +# Title : "IDisposables" should be disposed +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2930 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +# Comment : Duplicate, see CA2000 +dotnet_diagnostic.S2930.severity = none + +# Title : Classes with "IDisposable" members should implement "IDisposable" +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2931 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2931.severity = warning + +# Title : Fields that are only assigned in the constructor should be "readonly" +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2933 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2933.severity = none + +# Title : Property assignments should not be made for "readonly" fields not constrained to reference types +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2934 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2934.severity = warning + +# Title : Classes should "Dispose" of members from the classes' own "Dispose" methods +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2952 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S2952.severity = warning + +# Title : Methods named "Dispose" should implement "IDisposable.Dispose" +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2953 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S2953.severity = warning + +# Title : Generic parameters not constrained to reference types should not be compared to "null" +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2955 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S2955.severity = warning + +# Title : "IEnumerable" LINQs should be simplified +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2971 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2971.severity = warning + +# Title : "Object.ReferenceEquals" should not be used for value types +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2995 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2995.severity = warning + +# Title : "ThreadStatic" fields should not be initialized +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2996 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2996.severity = warning + +# Title : "IDisposables" created in a "using" statement should not be returned +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-2997 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S2997.severity = warning + +# Title : "ThreadStatic" should not be used on non-static fields +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3005 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3005.severity = warning + +# Title : Static fields should not be updated in constructors +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3010 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3010.severity = warning + +# Title : Reflection should not be used to increase accessibility of classes, methods, or fields +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3011 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3011.severity = warning + +# Title : Members should not be initialized to default values +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3052 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3052.severity = none + +# Title : Types should not have members with visibility set higher than the type's visibility +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3059 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3059.severity = none + +# Title : "is" should not be used with "this" +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3060 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3060.severity = warning + +# Title : "async" methods should not return "void" +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3168 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3168.severity = none + +# Title : Multiple "OrderBy" calls should not be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3169 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3169.severity = warning + +# Title : Delegates should not be subtracted +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3172 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3172.severity = warning + +# Title : "interface" instances should not be cast to concrete types +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3215 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3215.severity = warning + +# Title : "ConfigureAwait(false)" should be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3216 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3216.severity = none + +# Title : "Explicit" conversions of "foreach" loops should not be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3217 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3217.severity = warning + +# Title : Inner class members should not shadow outer class "static" or type members +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3218 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3218.severity = warning + +# Title : Method calls should not resolve ambiguously to overloads with "params" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3220 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3220.severity = warning + +# Title : "GC.SuppressFinalize" should not be invoked for types without destructors +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3234 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3234.severity = warning + +# Title : Redundant parentheses should not be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3235 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3235.severity = warning + +# Title : Caller information arguments should not be provided explicitly +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3236 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3236.severity = warning + +# Title : "value" parameters should be used +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3237 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3237.severity = warning + +# Title : The simplest possible condition syntax should be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3240 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3240.severity = none + +# Title : Methods should not return values that are never used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3241 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3241.severity = warning + +# Title : Method parameters should be declared with base types +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3242 +# Tags : C#, MainSourceScope, TestSourceScope +# Comment : We want to encourage concrete types instead of interface types when possible as it's considerably faster. +dotnet_diagnostic.S3242.severity = none + +# Title : Anonymous delegates should not be used to unsubscribe from Events +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3244 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3244.severity = warning + +# Title : Generic type parameters should be co/contravariant when possible +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3246 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3246.severity = warning + +# Title : Duplicate casts should not be made +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3247 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3247.severity = warning + +# Title : Classes directly extending "object" should not call "base" in "GetHashCode" or "Equals" +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3249 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3249.severity = warning + +# Title : Implementations should be provided for "partial" methods +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3251 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3251.severity = warning + +# Title : Constructor and destructor declarations should not be redundant +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3253 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3253.severity = warning + +# Title : Default parameter values should not be passed as arguments +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3254 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3254.severity = warning + +# Title : "string.IsNullOrEmpty" should be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3256 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3256.severity = warning + +# Title : Declarations and initializations should be as concise as possible +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3257 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3257.severity = warning + +# Title : Non-derived "private" classes and records should be "sealed" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3260 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3260.severity = none + +# Title : Namespaces should not be empty +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3261 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3261.severity = warning + +# Title : "params" should be used on overrides +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3262 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3262.severity = warning + +# Title : Static fields should appear in the order they must be initialized +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3263 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3263.severity = warning + +# Title : Events should be invoked +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3264 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3264.severity = warning + +# Title : Non-flags enums should not be used in bitwise operations +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3265 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3265.severity = warning + +# Title : Loops should be simplified with "LINQ" expressions +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3267 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3267.severity = none + +# Title : Cipher Block Chaining IVs should be unpredictable +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3329 +# Tags : C#, MainSourceScope, SonarWay +# Comment : Analysis is too slow +dotnet_diagnostic.S3329.severity = none + +# Title : Creating cookies without the "HttpOnly" flag is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3330 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3330.severity = warning + +# Title : Caller information parameters should come at the end of the parameter list +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3343 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3343.severity = warning + +# Title : Expressions used in "Debug.Assert" should not produce side effects +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3346 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3346.severity = warning + +# Title : Unchanged local variables should be "const" +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3353 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3353.severity = warning + +# Title : Ternary operators should not be nested +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3358 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3358.severity = warning + +# Title : "this" should not be exposed from constructors +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3366 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3366.severity = warning + +# Title : Attribute, EventArgs, and Exception type names should end with the type being extended +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3376 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3376.severity = none + +# Title : "base.Equals" should not be used to check for reference equality in "Equals" if "base" is not "object" +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3397 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3397.severity = warning + +# Title : Methods should not return constants +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3400 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3400.severity = warning + +# Title : Assertion arguments should be passed in the correct order +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3415 +# Tags : C#, TestSourceScope, SonarWay +dotnet_diagnostic.S3415.severity = warning + +# Title : Method overloads with default parameter values should not overlap +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3427 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3427.severity = warning + +# Title : "[ExpectedException]" should not be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3431 +# Tags : C#, TestSourceScope +dotnet_diagnostic.S3431.severity = warning + +# Title : Test method signatures should be correct +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3433 +# Tags : C#, TestSourceScope, SonarWay +dotnet_diagnostic.S3433.severity = warning + +# Title : Variables should not be checked against the values they're about to be assigned +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3440 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3440.severity = warning + +# Title : Redundant property names should be omitted in anonymous classes +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3441 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3441.severity = warning + +# Title : "abstract" classes should not have "public" constructors +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3442 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3442.severity = warning + +# Title : Type should not be examined on "System.Type" instances +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3443 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3443.severity = warning + +# Title : Interfaces should not simply inherit from base interfaces with colliding members +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3444 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3444.severity = warning + +# Title : Exceptions should not be explicitly rethrown +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3445 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3445.severity = warning + +# Title : "[Optional]" should not be used on "ref" or "out" parameters +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3447 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3447.severity = warning + +# Title : Right operands of shift operators should be integers +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3449 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3449.severity = warning + +# Title : Parameters with "[DefaultParameterValue]" attributes should also be marked "[Optional]" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3450 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3450.severity = warning + +# Title : "[DefaultValue]" should not be used when "[DefaultParameterValue]" is meant +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3451 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3451.severity = warning + +# Title : Classes should not have only "private" constructors +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3453 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3453.severity = warning + +# Title : "string.ToCharArray()" and "ReadOnlySpan.ToArray()" should not be called redundantly +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3456 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3456.severity = warning + +# Title : Composite format strings should be used correctly +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3457 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3457.severity = warning + +# Title : Empty "case" clauses that fall through to the "default" should be omitted +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3458 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3458.severity = warning + +# Title : Unassigned members should be removed +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3459 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3459.severity = warning + +# Title : Type inheritance should not be recursive +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3464 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3464.severity = warning + +# Title : Optional parameters should be passed to "base" calls +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3466 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3466.severity = warning + +# Title : Empty "default" clauses should be removed +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3532 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3532.severity = warning + +# Title : "ServiceContract" and "OperationContract" attributes should be used together +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3597 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3597.severity = warning + +# Title : One-way "OperationContract" methods should have "void" return type +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3598 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3598.severity = warning + +# Title : "params" should not be introduced on overrides +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3600 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3600.severity = warning + +# Title : Methods with "Pure" attribute should return a value +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3603 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3603.severity = warning + +# Title : Member initializer values should not be redundant +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3604 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3604.severity = warning + +# Title : Nullable type comparison should not be redundant +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3610 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3610.severity = warning + +# Title : Jump statements should not be redundant +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3626 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3626.severity = warning + +# Title : Empty nullable value should not be accessed +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3655 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3655.severity = warning + +# Title : Exception constructors should not throw exceptions +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3693 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3693.severity = warning + +# Title : Track use of "NotImplementedException" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3717 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3717.severity = warning + +# Title : Cognitive Complexity of methods should not be too high +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3776 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +# Comment : Code gets complicated +dotnet_diagnostic.S3776.severity = none + +# Title : "SafeHandle.DangerousGetHandle" should not be called +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3869 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3869.severity = warning + +# Title : Exception types should be "public" +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3871 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3871.severity = none + +# Title : Parameter names should not duplicate the names of their methods +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3872 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3872.severity = warning + +# Title : "out" and "ref" parameters should not be used +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3874 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3874.severity = none + +# Title : "operator==" should not be overloaded on reference types +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3875 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3875.severity = warning + +# Title : Strings or integral types should be used for indexers +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3876 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3876.severity = warning + +# Title : Exceptions should not be thrown from unexpected methods +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3877 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3877.severity = none + +# Title : Finalizers should not be empty +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3880 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3880.severity = warning + +# Title : "IDisposable" should be implemented correctly +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3881 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3881.severity = none + +# Title : "CoSetProxyBlanket" and "CoInitializeSecurity" should not be used +# Category : Blocker Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3884 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3884.severity = warning + +# Title : "Assembly.Load" should be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3885 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3885.severity = warning + +# Title : Mutable, non-private fields should not be "readonly" +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3887 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3887.severity = warning + +# Title : Neither "Thread.Resume" nor "Thread.Suspend" should be used +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3889 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3889.severity = warning + +# Title : Classes that provide "Equals()" should implement "IEquatable" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3897 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3897.severity = warning + +# Title : Value types should implement "IEquatable" +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3898 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3898.severity = none + +# Title : Arguments of public methods should be validated against null +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3900 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3900.severity = none + +# Title : "Assembly.GetExecutingAssembly" should not be called +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3902 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3902.severity = warning + +# Title : Types should be defined in named namespaces +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3903 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +# Comment : Doesn't work with file-scoped namespaces, so disabling for now. +dotnet_diagnostic.S3903.severity = none + +# Title : Assemblies should have version information +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3904 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S3904.severity = warning + +# Title : Event Handlers should have the correct signature +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3906 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3906.severity = warning + +# Title : Generic event handlers should be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3908 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3908.severity = warning + +# Title : Collections should implement the generic interface +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3909 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3909.severity = none + +# Title : All branches in a conditional structure should not have exactly the same implementation +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3923 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3923.severity = warning + +# Title : "ISerializable" should be implemented correctly +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3925 +# Tags : C#, MainSourceScope, SonarWay +# Comment : TODO - is ISerializable still relevant? +dotnet_diagnostic.S3925.severity = none + +# Title : Deserialization methods should be provided for "OptionalField" members +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3926 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3926.severity = warning + +# Title : Serialization event handlers should be implemented correctly +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3927 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3927.severity = warning + +# Title : Parameter names used into ArgumentException constructors should match an existing one +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3928 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3928.severity = none + +# Title : Number patterns should be regular +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3937 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3937.severity = warning + +# Title : Calculations should not overflow +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3949 +# Tags : C#, MainSourceScope, TestSourceScope, Unnecessary +dotnet_diagnostic.S3949.severity = suggestion + +# Title : "Generic.List" instances should not be part of public APIs +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3956 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S3956.severity = warning + +# Title : "static readonly" constants should be "const" instead +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3962 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3962.severity = none + +# Title : "static" fields should be initialized inline +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3963 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3963.severity = none + +# Title : Objects should not be disposed more than once +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3966 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3966.severity = warning + +# Title : Multidimensional arrays should not be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3967 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3967.severity = warning + +# Title : "GC.SuppressFinalize" should not be called +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3971 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3971.severity = warning + +# Title : Conditionals should start on new lines +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3972 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3972.severity = warning + +# Title : A conditionally executed single line should be denoted by indentation +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3973 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3973.severity = warning + +# Title : Collection sizes and array length comparisons should make sense +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3981 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3981.severity = warning + +# Title : Exceptions should not be created without being thrown +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3984 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3984.severity = warning + +# Title : Assemblies should be marked as CLS compliant +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3990 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3990.severity = none + +# Title : Assemblies should explicitly specify COM visibility +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3992 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3992.severity = none + +# Title : Custom attributes should be marked with "System.AttributeUsageAttribute" +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3993 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3993.severity = none + +# Title : URI Parameters should not be strings +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3994 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3994.severity = none + +# Title : URI return values should not be strings +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3995 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3995.severity = warning + +# Title : URI properties should not be strings +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3996 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3996.severity = warning + +# Title : String URI overloads should call "System.Uri" overloads +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3997 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S3997.severity = warning + +# Title : Threads should not lock on objects with weak identity +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-3998 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S3998.severity = warning + +# Title : Pointers to unmanaged memory should not be visible +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4000 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4000.severity = warning + +# Title : Disposable types should declare finalizers +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4002 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4002.severity = warning + +# Title : Collection properties should be readonly +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4004 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4004.severity = none + +# Title : "System.Uri" arguments should be used instead of strings +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4005 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4005.severity = none + +# Title : Inherited member visibility should not be decreased +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4015 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4015.severity = warning + +# Title : Enumeration members should not be named "Reserved" +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4016 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4016.severity = warning + +# Title : Method signatures should not contain nested generic types +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4017 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4017.severity = none + +# Title : All type parameters should be used in the parameter list to enable type inference +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4018 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4018.severity = none + +# Title : Base class methods should not be hidden +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4019 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4019.severity = warning + +# Title : Enumerations should have "Int32" storage +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4022 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4022.severity = warning + +# Title : Interfaces should not be empty +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4023 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4023.severity = warning + +# Title : Child class fields should not differ from parent class fields only by capitalization +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4025 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4025.severity = warning + +# Title : Assemblies should be marked with "NeutralResourcesLanguageAttribute" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4026 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4026.severity = warning + +# Title : Exceptions should provide standard constructors +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4027 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4027.severity = none + +# Title : Classes implementing "IEquatable" should be sealed +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4035 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4035.severity = warning + +# Title : Searching OS commands in PATH is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4036 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4036.severity = warning + +# Title : Interface methods should be callable by derived types +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4039 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4039.severity = warning + +# Title : Strings should be normalized to uppercase +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4040 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4040.severity = none + +# Title : Type names should not match namespaces +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4041 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4041.severity = warning + +# Title : Generics should be used when appropriate +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4047 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4047.severity = warning + +# Title : Properties should be preferred +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4049 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4049.severity = warning + +# Title : Operators should be overloaded consistently +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4050 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4050.severity = warning + +# Title : Types should not extend outdated base types +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4052 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4052.severity = warning + +# Title : Literals should not be passed as localized parameters +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4055 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4055.severity = none + +# Title : Overloads with a "CultureInfo" or an "IFormatProvider" parameter should be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4056 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4056.severity = warning + +# Title : Locales should be set for data types +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4057 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4057.severity = warning + +# Title : Overloads with a "StringComparison" parameter should be used +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4058 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4058.severity = none + +# Title : Property names should not match get methods +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4059 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4059.severity = warning + +# Title : Non-abstract attributes should be sealed +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4060 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4060.severity = none + +# Title : "params" should be used instead of "varargs" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4061 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4061.severity = warning + +# Title : Operator overloads should have named alternatives +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4069 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4069.severity = none + +# Title : Non-flags enums should not be marked with "FlagsAttribute" +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4070 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4070.severity = none + +# Title : Method overloads should be grouped together +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4136 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4136.severity = warning + +# Title : Duplicate values should not be passed as arguments +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4142 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4142.severity = warning + +# Title : Collection elements should not be replaced unconditionally +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4143 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4143.severity = warning + +# Title : Methods should not have identical implementations +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4144 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4144.severity = warning + +# Title : Empty collections should not be accessed or iterated +# Category : Minor Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4158 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4158.severity = warning + +# Title : Classes should implement their "ExportAttribute" interfaces +# Category : Blocker Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4159 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4159.severity = warning + +# Title : Native methods should be wrapped +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4200 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4200.severity = warning + +# Title : Null checks should not be used with "is" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4201 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4201.severity = warning + +# Title : Windows Forms entry points should be marked with STAThread +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4210 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4210.severity = warning + +# Title : Members should not have conflicting transparency annotations +# Category : Major Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4211 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4211.severity = warning + +# Title : Serialization constructors should be secured +# Category : Major Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4212 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4212.severity = warning + +# Title : "P/Invoke" methods should not be visible +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4214 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4214.severity = warning + +# Title : Events should have proper arguments +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4220 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4220.severity = warning + +# Title : Extension methods should not extend "object" +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4225 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4225.severity = warning + +# Title : Extensions should be in separate namespaces +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4226 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4226.severity = none + +# Title : "ConstructorArgument" parameters should exist in constructors +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4260 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4260.severity = warning + +# Title : Methods should be named according to their synchronicities +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4261 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4261.severity = none + +# Title : Getters and setters should access the expected fields +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4275 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4275.severity = warning + +# Title : "Shared" parts should not be created with "new" +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4277 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4277.severity = warning + +# Title : Weak SSL/TLS protocols should not be used +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4423 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4423.severity = warning + +# Title : Cryptographic keys should be robust +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4426 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4426.severity = warning + +# Title : "PartCreationPolicyAttribute" should be used with "ExportAttribute" +# Category : Major Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4428 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4428.severity = warning + +# Title : AES encryption algorithm should be used with secured mode +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4432 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4432.severity = warning + +# Title : LDAP connections should be authenticated +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4433 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4433.severity = warning + +# Title : Parameter validation in yielding methods should be wrapped +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4456 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4456.severity = warning + +# Title : Parameter validation in "async"/"await" methods should be wrapped +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4457 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S4457.severity = warning + +# Title : Calls to "async" methods should not be blocking +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4462 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4462.severity = none + +# Title : Unread "private" fields should be removed +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4487 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay, Unnecessary +dotnet_diagnostic.S4487.severity = none + +# Title : Disabling CSRF protections is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4502 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4502.severity = warning + +# Title : Delivering code in production with debug features activated is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4507 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4507.severity = warning + +# Title : "default" clauses should be first or last +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4524 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4524.severity = warning + +# Title : ASP.NET HTTP request validation feature should not be disabled +# Category : Major Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4564 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4564.severity = warning + +# Title : "new Guid()" should not be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4581 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4581.severity = warning + +# Title : Calls to delegate's method "BeginInvoke" should be paired with calls to "EndInvoke" +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4583 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4583.severity = warning + +# Title : Non-async "Task/Task" methods should not return null +# Category : Critical Bug +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4586 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4586.severity = warning + +# Title : String offset-based methods should be preferred for finding substrings from offsets +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4635 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S4635.severity = warning + +# Title : Using regular expressions is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4784 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4784.severity = warning + +# Title : Encrypting data is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4787 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4787.severity = warning + +# Title : Using weak hashing algorithms is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4790 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4790.severity = warning + +# Title : Configuring loggers is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4792 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4792.severity = warning + +# Title : Using Sockets is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4818 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4818.severity = warning + +# Title : Using command line arguments is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4823 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4823.severity = warning + +# Title : Reading the Standard Input is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4829 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4829.severity = warning + +# Title : Server certificates should be verified during SSL/TLS connections +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4830 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S4830.severity = none + +# Title : Controlling permissions is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-4834 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S4834.severity = warning + +# Title : "ValueTask" should be consumed correctly +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5034 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S5034.severity = warning + +# Title : Expanding archive files without controlling resource consumption is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5042 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5042.severity = warning + +# Title : Having a permissive Cross-Origin Resource Sharing policy is security-sensitive +# Category : Minor Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5122 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5122.severity = warning + +# Title : Using clear-text protocols is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5332 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5332.severity = warning + +# Title : Using publicly writable directories is security-sensitive +# Category : Critical Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5443 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5443.severity = warning + +# Title : Insecure temporary file creation methods should not be used +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5445 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5445.severity = warning + +# Title : Encryption algorithms should be used with secure mode and padding scheme +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5542 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5542.severity = warning + +# Title : Cipher algorithms should be robust +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5547 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5547.severity = warning + +# Title : JWT should be signed and verified with strong cipher algorithms +# Category : Critical Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5659 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5659.severity = warning + +# Title : Allowing requests with excessive content length is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5693 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S5693.severity = warning + +# Title : Disabling ASP.NET "Request Validation" feature is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5753 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5753.severity = warning + +# Title : Deserializing objects without performing data validation is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5766 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S5766.severity = warning + +# Title : Types allowed to be deserialized should be restricted +# Category : Major Vulnerability +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-5773 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S5773.severity = warning + +# Title : Use a testable date/time provider +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6354 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S6354.severity = none + +# Title : Azure Functions should be stateless +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6419 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S6419.severity = warning + +# Title : Client instances should not be recreated on each Azure Function invocation +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6420 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S6420.severity = warning + +# Title : Azure Functions should use Structured Error Handling +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6421 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S6421.severity = warning + +# Title : Calls to "async" methods should not be blocking in Azure Functions +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6422 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S6422.severity = warning + +# Title : Azure Functions should log all failures +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6423 +# Tags : C#, MainSourceScope +dotnet_diagnostic.S6423.severity = warning + +# Title : Interfaces for durable entities should satisfy the restrictions +# Category : Blocker Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6424 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S6424.severity = warning + +# Title : Not specifying a timeout for regular expressions is security-sensitive +# Category : Major Security Hotspot +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-6444 +# Tags : C#, MainSourceScope, SonarWay +dotnet_diagnostic.S6444.severity = warning + +# Title : Literal suffixes should be upper case +# Category : Minor Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-818 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S818.severity = warning + +# Title : Increment (++) and decrement (--) operators should not be used in a method call or mixed with other operators in an expression +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-881 +# Tags : C#, MainSourceScope, TestSourceScope +dotnet_diagnostic.S881.severity = suggestion + +# Title : "goto" statement should not be used +# Category : Major Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-907 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S907.severity = warning + +# Title : Parameter names should match base declaration and other partial definitions +# Category : Critical Code Smell +# Help Link: https://rules.sonarsource.com/csharp/RSPEC-927 +# Tags : C#, MainSourceScope, TestSourceScope, SonarWay +dotnet_diagnostic.S927.severity = none + +# Title : Copy-paste token calculator +# Category : +# Tags : MainSourceScope, TestSourceScope, Utility, NotConfigurable +dotnet_diagnostic.S9999-cpd.severity = warning + +# Title : Log generator +# Category : +# Tags : MainSourceScope, TestSourceScope, Utility, NotConfigurable +dotnet_diagnostic.S9999-log.severity = none + +# Title : File metadata generator +# Category : +# Tags : MainSourceScope, TestSourceScope, Utility, NotConfigurable +dotnet_diagnostic.S9999-metadata.severity = warning + +# Title : Metrics calculator +# Category : +# Tags : MainSourceScope, TestSourceScope, Utility, NotConfigurable +dotnet_diagnostic.S9999-metrics.severity = warning + +# Title : Symbol reference calculator +# Category : +# Tags : MainSourceScope, TestSourceScope, Utility, NotConfigurable +dotnet_diagnostic.S9999-symbolRef.severity = warning + +# Title : Token type calculator +# Category : +# Tags : MainSourceScope, TestSourceScope, Utility, NotConfigurable +dotnet_diagnostic.S9999-token-type.severity = warning + +# Title : XML comment analysis disabled +# Category : StyleCop.CSharp.SpecialRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA0001.md +dotnet_diagnostic.SA0001.severity = none + +# Title : Invalid settings file +# Category : StyleCop.CSharp.SpecialRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA0002.md +dotnet_diagnostic.SA0002.severity = warning + +# Title : Keywords should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1000.md +dotnet_diagnostic.SA1000.severity = none + +# Title : Commas should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1001.md +dotnet_diagnostic.SA1001.severity = none + +# Title : Semicolons should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1002.md +dotnet_diagnostic.SA1002.severity = none + +# Title : Symbols should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1003.md +dotnet_diagnostic.SA1003.severity = none + +# Title : Documentation lines should begin with single space +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1004.md +dotnet_diagnostic.SA1004.severity = warning + +# Title : Single line comments should begin with single space +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1005.md +dotnet_diagnostic.SA1005.severity = warning + +# Title : Preprocessor keywords should not be preceded by space +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1006.md +dotnet_diagnostic.SA1006.severity = warning + +# Title : Operator keyword should be followed by space +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1007.md +dotnet_diagnostic.SA1007.severity = none + +# Title : Opening parenthesis should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1008.md +dotnet_diagnostic.SA1008.severity = none + +# Title : Closing parenthesis should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1009.md +dotnet_diagnostic.SA1009.severity = none + +# Title : Opening square brackets should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1010.md +dotnet_diagnostic.SA1010.severity = none + +# Title : Closing square brackets should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1011.md +dotnet_diagnostic.SA1011.severity = none + +# Title : Opening braces should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1012.md +dotnet_diagnostic.SA1012.severity = none + +# Title : Closing braces should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1013.md +dotnet_diagnostic.SA1013.severity = none + +# Title : Opening generic brackets should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1014.md +dotnet_diagnostic.SA1014.severity = none + +# Title : Closing generic brackets should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1015.md +dotnet_diagnostic.SA1015.severity = none + +# Title : Opening attribute brackets should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1016.md +dotnet_diagnostic.SA1016.severity = none + +# Title : Closing attribute brackets should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1017.md +dotnet_diagnostic.SA1017.severity = none + +# Title : Nullable type symbols should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1018.md +dotnet_diagnostic.SA1018.severity = none + +# Title : Member access symbols should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1019.md +dotnet_diagnostic.SA1019.severity = none + +# Title : Increment decrement symbols should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1020.md +dotnet_diagnostic.SA1020.severity = none + +# Title : Negative signs should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1021.md +dotnet_diagnostic.SA1021.severity = none + +# Title : Positive signs should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1022.md +dotnet_diagnostic.SA1022.severity = none + +# Title : Dereference and access of symbols should be spaced correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1023.md +dotnet_diagnostic.SA1023.severity = none + +# Title : Colons Should Be Spaced Correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1024.md +dotnet_diagnostic.SA1024.severity = none + +# Title : Code should not contain multiple whitespace in a row +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1025.md +dotnet_diagnostic.SA1025.severity = none + +# Title : Code should not contain space after new or stackalloc keyword in implicitly typed array allocation +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1026.md +dotnet_diagnostic.SA1026.severity = none + +# Title : Use tabs correctly +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1027.md +dotnet_diagnostic.SA1027.severity = warning + +# Title : Code should not contain trailing whitespace +# Category : StyleCop.CSharp.SpacingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1028.md +# Tags : Unnecessary +dotnet_diagnostic.SA1028.severity = warning + +# Title : Do not prefix calls with base unless local implementation exists +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1100.md +dotnet_diagnostic.SA1100.severity = warning + +# Title : Prefix local calls with this +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1101.md +dotnet_diagnostic.SA1101.severity = none + +# Title : Query clause should follow previous clause +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1102.md +dotnet_diagnostic.SA1102.severity = warning + +# Title : Query clauses should be on separate lines or all on one line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1103.md +dotnet_diagnostic.SA1103.severity = warning + +# Title : Query clause should begin on new line when previous clause spans multiple lines +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1104.md +dotnet_diagnostic.SA1104.severity = warning + +# Title : Query clauses spanning multiple lines should begin on own line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1105.md +dotnet_diagnostic.SA1105.severity = warning + +# Title : Code should not contain empty statements +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1106.md +# Tags : Unnecessary +dotnet_diagnostic.SA1106.severity = warning + +# Title : Code should not contain multiple statements on one line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1107.md +dotnet_diagnostic.SA1107.severity = none + +# Title : Block statements should not contain embedded comments +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1108.md +dotnet_diagnostic.SA1108.severity = warning + +# Title : Block statements should not contain embedded regions +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1109.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1109.severity = warning + +# Title : Opening parenthesis or bracket should be on declaration line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1110.md +dotnet_diagnostic.SA1110.severity = warning + +# Title : Closing parenthesis should be on line of last parameter +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1111.md +dotnet_diagnostic.SA1111.severity = warning + +# Title : Closing parenthesis should be on line of opening parenthesis +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1112.md +dotnet_diagnostic.SA1112.severity = warning + +# Title : Comma should be on the same line as previous parameter +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1113.md +dotnet_diagnostic.SA1113.severity = warning + +# Title : Parameter list should follow declaration +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1114.md +dotnet_diagnostic.SA1114.severity = warning + +# Title : Parameter should follow comma +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1115.md +dotnet_diagnostic.SA1115.severity = none + +# Title : Split parameters should start on line after declaration +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1116.md +dotnet_diagnostic.SA1116.severity = none + +# Title : Parameters should be on same line or separate lines +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1117.md +dotnet_diagnostic.SA1117.severity = none + +# Title : Parameter should not span multiple lines +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1118.md +dotnet_diagnostic.SA1118.severity = warning + +# Title : Statement should not use unnecessary parenthesis +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1119.md +dotnet_diagnostic.SA1119.severity = warning + +# Title : Statement should not use unnecessary parenthesis +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1119.md +# Tags : Unnecessary, NotConfigurable +dotnet_diagnostic.SA1119_p.severity = none + +# Title : Comments should contain text +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1120.md +dotnet_diagnostic.SA1120.severity = warning + +# Title : Use built-in type alias +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1121.md +# Tags : Unnecessary +dotnet_diagnostic.SA1121.severity = warning + +# Title : Use string.Empty for empty strings +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1122.md +dotnet_diagnostic.SA1122.severity = none + +# Title : Do not place regions within elements +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1123.md +dotnet_diagnostic.SA1123.severity = warning + +# Title : Do not use regions +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1124.md +dotnet_diagnostic.SA1124.severity = none + +# Title : Use shorthand for nullable types +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1125.md +dotnet_diagnostic.SA1125.severity = warning + +# Title : Prefix calls correctly +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1126.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1126.severity = none + +# Title : Generic type constraints should be on their own line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1127.md +dotnet_diagnostic.SA1127.severity = warning + +# Title : Put constructor initializers on their own line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1128.md +dotnet_diagnostic.SA1128.severity = warning + +# Title : Do not use default value type constructor +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1129.md +dotnet_diagnostic.SA1129.severity = warning + +# Title : Use lambda syntax +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1130.md +dotnet_diagnostic.SA1130.severity = warning + +# Title : Use readable conditions +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1131.md +dotnet_diagnostic.SA1131.severity = warning + +# Title : Do not combine fields +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1132.md +# Comment : S1169 handles fields and variables +dotnet_diagnostic.SA1132.severity = none + +# Title : Do not combine attributes +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1133.md +dotnet_diagnostic.SA1133.severity = warning + +# Title : Attributes should not share line +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1134.md +dotnet_diagnostic.SA1134.severity = none + +# Title : Using directives should be qualified +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1135.md +dotnet_diagnostic.SA1135.severity = warning + +# Title : Enum values should be on separate lines +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1136.md +dotnet_diagnostic.SA1136.severity = warning + +# Title : Elements should have the same indentation +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1137.md +# Comment : Doesn't work with file-scoped namespaces +dotnet_diagnostic.SA1137.severity = none + +# Title : Use literal suffix notation instead of casting +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1139.md +dotnet_diagnostic.SA1139.severity = warning + +# Title : Use tuple syntax +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1141.md +dotnet_diagnostic.SA1141.severity = warning + +# Title : Refer to tuple fields by name +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1142.md +dotnet_diagnostic.SA1142.severity = none + +# Title : Using directives should be placed correctly +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1200.md +dotnet_diagnostic.SA1200.severity = none + +# Title : Elements should appear in the correct order +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1201.md +dotnet_diagnostic.SA1201.severity = none + +# Title : Elements should be ordered by access +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1202.md +dotnet_diagnostic.SA1202.severity = none + +# Title : Constants should appear before fields +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1203.md +dotnet_diagnostic.SA1203.severity = warning + +# Title : Static elements should appear before instance elements +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1204.md +dotnet_diagnostic.SA1204.severity = warning + +# Title : Partial elements should declare access +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1205.md +dotnet_diagnostic.SA1205.severity = warning + +# Title : Declaration keywords should follow order +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1206.md +dotnet_diagnostic.SA1206.severity = warning + +# Title : Protected should come before internal +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1207.md +dotnet_diagnostic.SA1207.severity = warning + +# Title : System using directives should be placed before other using directives +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1208.md +dotnet_diagnostic.SA1208.severity = warning + +# Title : Using alias directives should be placed after other using directives +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1209.md +dotnet_diagnostic.SA1209.severity = warning + +# Title : Using directives should be ordered alphabetically by namespace +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1210.md +dotnet_diagnostic.SA1210.severity = warning + +# Title : Using alias directives should be ordered alphabetically by alias name +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1211.md +dotnet_diagnostic.SA1211.severity = warning + +# Title : Property accessors should follow order +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1212.md +dotnet_diagnostic.SA1212.severity = warning + +# Title : Event accessors should follow order +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1213.md +dotnet_diagnostic.SA1213.severity = warning + +# Title : Readonly fields should appear before non-readonly fields +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1214.md +dotnet_diagnostic.SA1214.severity = warning + +# Title : Using static directives should be placed at the correct location +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1216.md +dotnet_diagnostic.SA1216.severity = warning + +# Title : Using static directives should be ordered alphabetically +# Category : StyleCop.CSharp.OrderingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1217.md +dotnet_diagnostic.SA1217.severity = warning + +# Title : Element should begin with upper-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md +dotnet_diagnostic.SA1300.severity = none + +# Title : Element should begin with lower-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1301.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1301.severity = none + +# Title : Interface names should begin with I +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1302.md +dotnet_diagnostic.SA1302.severity = none + +# Title : Const field names should begin with upper-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md +dotnet_diagnostic.SA1303.severity = none + +# Title : Non-private readonly fields should begin with upper-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1304.md +dotnet_diagnostic.SA1304.severity = none + +# Title : Field names should not use Hungarian notation +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1305.md +dotnet_diagnostic.SA1305.severity = none + +# Title : Field names should begin with lower-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1306.md +dotnet_diagnostic.SA1306.severity = none + +# Title : Accessible fields should begin with upper-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1307.md +dotnet_diagnostic.SA1307.severity = none + +# Title : Variable names should not be prefixed +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1308.md +dotnet_diagnostic.SA1308.severity = none + +# Title : Field names should not begin with underscore +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1309.md +dotnet_diagnostic.SA1309.severity = none + +# Title : Field names should not contain underscore +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1310.md +dotnet_diagnostic.SA1310.severity = warning + +# Title : Static readonly fields should begin with upper-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md +dotnet_diagnostic.SA1311.severity = none + +# Title : Variable names should begin with lower-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md +dotnet_diagnostic.SA1312.severity = none + +# Title : Parameter names should begin with lower-case letter +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1313.md +dotnet_diagnostic.SA1313.severity = none + +# Title : Type parameter names should begin with T +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1314.md +dotnet_diagnostic.SA1314.severity = none + +# Title : Tuple element names should use correct casing +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1316.md +dotnet_diagnostic.SA1316.severity = warning + +# Title : Access modifier should be declared +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1400.md +dotnet_diagnostic.SA1400.severity = warning + +# Title : Fields should be private +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md +dotnet_diagnostic.SA1401.severity = none + +# Title : File may only contain a single type +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1402.md +dotnet_diagnostic.SA1402.severity = warning + +# Title : File may only contain a single namespace +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1403.md +dotnet_diagnostic.SA1403.severity = warning + +# Title : Code analysis suppression should have justification +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1404.md +dotnet_diagnostic.SA1404.severity = warning + +# Title : Debug.Assert should provide message text +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1405.md +dotnet_diagnostic.SA1405.severity = warning + +# Title : Debug.Fail should provide message text +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1406.md +dotnet_diagnostic.SA1406.severity = warning + +# Title : Arithmetic expressions should declare precedence +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1407.md +dotnet_diagnostic.SA1407.severity = warning + +# Title : Conditional expressions should declare precedence +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1408.md +dotnet_diagnostic.SA1408.severity = warning + +# Title : Remove unnecessary code +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1409.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1409.severity = warning + +# Title : Remove delegate parenthesis when possible +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1410.md +# Tags : Unnecessary +dotnet_diagnostic.SA1410.severity = warning + +# Title : Attribute constructor should not use unnecessary parenthesis +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1411.md +# Tags : Unnecessary +dotnet_diagnostic.SA1411.severity = warning + +# Title : Store files as UTF-8 with byte order mark +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1412.md +# Comment : Pedantic +dotnet_diagnostic.SA1412.severity = none + +# Title : Use trailing comma in multi-line initializers +# Category : StyleCop.CSharp.MaintainabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1413.md +dotnet_diagnostic.SA1413.severity = none + +# Title : Tuple types in signatures should have element names +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1414.md +dotnet_diagnostic.SA1414.severity = warning + +# Title : Braces for multi-line statements should not share line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1500.md +dotnet_diagnostic.SA1500.severity = warning + +# Title : Statement should not be on a single line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1501.md +dotnet_diagnostic.SA1501.severity = warning + +# Title : Element should not be on a single line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1502.md +dotnet_diagnostic.SA1502.severity = warning + +# Title : Braces should not be omitted +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1503.md +dotnet_diagnostic.SA1503.severity = none + +# Title : All accessors should be single-line or multi-line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1504.md +dotnet_diagnostic.SA1504.severity = warning + +# Title : Opening braces should not be followed by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1505.md +dotnet_diagnostic.SA1505.severity = warning + +# Title : Element documentation headers should not be followed by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1506.md +dotnet_diagnostic.SA1506.severity = warning + +# Title : Code should not contain multiple blank lines in a row +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1507.md +dotnet_diagnostic.SA1507.severity = none + +# Title : Closing braces should not be preceded by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1508.md +dotnet_diagnostic.SA1508.severity = none + +# Title : Opening braces should not be preceded by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1509.md +dotnet_diagnostic.SA1509.severity = warning + +# Title : Chained statement blocks should not be preceded by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1510.md +dotnet_diagnostic.SA1510.severity = warning + +# Title : While-do footer should not be preceded by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1511.md +dotnet_diagnostic.SA1511.severity = warning + +# Title : Single-line comments should not be followed by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1512.md +dotnet_diagnostic.SA1512.severity = none + +# Title : Closing brace should be followed by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1513.md +dotnet_diagnostic.SA1513.severity = warning + +# Title : Element documentation header should be preceded by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1514.md +dotnet_diagnostic.SA1514.severity = warning + +# Title : Single-line comment should be preceded by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1515.md +dotnet_diagnostic.SA1515.severity = warning + +# Title : Elements should be separated by blank line +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1516.md +dotnet_diagnostic.SA1516.severity = none + +# Title : Code should not contain blank lines at start of file +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1517.md +dotnet_diagnostic.SA1517.severity = warning + +# Title : Use line endings correctly at end of file +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1518.md +dotnet_diagnostic.SA1518.severity = none + +# Title : Braces should not be omitted from multi-line child statement +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1519.md +dotnet_diagnostic.SA1519.severity = warning + +# Title : Use braces consistently +# Category : StyleCop.CSharp.LayoutRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1520.md +dotnet_diagnostic.SA1520.severity = warning + +# Title : Elements should be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1600.md +dotnet_diagnostic.SA1600.severity = none + +# Title : Partial elements should be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1601.md +dotnet_diagnostic.SA1601.severity = none + +# Title : Enumeration items should be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1602.md +dotnet_diagnostic.SA1602.severity = none + +# Title : Documentation should contain valid XML +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1603.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1603.severity = warning + +# Title : Element documentation should have summary +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1604.md +dotnet_diagnostic.SA1604.severity = warning + +# Title : Partial element documentation should have summary +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1605.md +dotnet_diagnostic.SA1605.severity = warning + +# Title : Element documentation should have summary text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1606.md +dotnet_diagnostic.SA1606.severity = warning + +# Title : Partial element documentation should have summary text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1607.md +dotnet_diagnostic.SA1607.severity = warning + +# Title : Element documentation should not have default summary +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1608.md +dotnet_diagnostic.SA1608.severity = warning + +# Title : Property documentation should have value +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1609.md +dotnet_diagnostic.SA1609.severity = none + +# Title : Property documentation should have value text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1610.md +dotnet_diagnostic.SA1610.severity = none + +# Title : Element parameters should be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1611.md +dotnet_diagnostic.SA1611.severity = none + +# Title : Element parameter documentation should match element parameters +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1612.md +dotnet_diagnostic.SA1612.severity = warning + +# Title : Element parameter documentation should declare parameter name +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1613.md +dotnet_diagnostic.SA1613.severity = warning + +# Title : Element parameter documentation should have text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1614.md +dotnet_diagnostic.SA1614.severity = warning + +# Title : Element return value should be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1615.md +dotnet_diagnostic.SA1615.severity = none + +# Title : Element return value documentation should have text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1616.md +dotnet_diagnostic.SA1616.severity = warning + +# Title : Void return value should not be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1617.md +dotnet_diagnostic.SA1617.severity = warning + +# Title : Generic type parameters should be documented +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1618.md +dotnet_diagnostic.SA1618.severity = warning + +# Title : Generic type parameters should be documented partial class +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1619.md +dotnet_diagnostic.SA1619.severity = warning + +# Title : Generic type parameter documentation should match type parameters +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1620.md +dotnet_diagnostic.SA1620.severity = warning + +# Title : Generic type parameter documentation should declare parameter name +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1621.md +dotnet_diagnostic.SA1621.severity = warning + +# Title : Generic type parameter documentation should have text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1622.md +dotnet_diagnostic.SA1622.severity = warning + +# Title : Property summary documentation should match accessors +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1623.md +dotnet_diagnostic.SA1623.severity = warning + +# Title : Property summary documentation should omit accessor with restricted access +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1624.md +dotnet_diagnostic.SA1624.severity = warning + +# Title : Element documentation should not be copied and pasted +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1625.md +dotnet_diagnostic.SA1625.severity = warning + +# Title : Single-line comments should not use documentation style slashes +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1626.md +dotnet_diagnostic.SA1626.severity = warning + +# Title : Documentation text should not be empty +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1627.md +dotnet_diagnostic.SA1627.severity = warning + +# Title : Documentation text should begin with a capital letter +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1628.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1628.severity = warning + +# Title : Documentation text should end with a period +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1629.md +dotnet_diagnostic.SA1629.severity = warning + +# Title : Documentation text should contain whitespace +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1630.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1630.severity = warning + +# Title : Documentation should meet character percentage +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1631.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1631.severity = warning + +# Title : Documentation text should meet minimum character length +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1632.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1632.severity = warning + +# Title : File should have header +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1633.md +dotnet_diagnostic.SA1633.severity = none + +# Title : File header should show copyright +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1634.md +dotnet_diagnostic.SA1634.severity = none + +# Title : File header should have copyright text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1635.md +dotnet_diagnostic.SA1635.severity = none + +# Title : File header copyright text should match +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1636.md +dotnet_diagnostic.SA1636.severity = none + +# Title : File header should contain file name +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1637.md +dotnet_diagnostic.SA1637.severity = none + +# Title : File header file name documentation should match file name +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1638.md +dotnet_diagnostic.SA1638.severity = none + +# Title : File header should have summary +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1639.md +dotnet_diagnostic.SA1639.severity = none + +# Title : File header should have valid company text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1640.md +dotnet_diagnostic.SA1640.severity = none + +# Title : File header company name text should match +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1641.md +dotnet_diagnostic.SA1641.severity = none + +# Title : Constructor summary documentation should begin with standard text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1642.md +dotnet_diagnostic.SA1642.severity = warning + +# Title : Destructor summary documentation should begin with standard text +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1643.md +dotnet_diagnostic.SA1643.severity = warning + +# Title : Documentation headers should not contain blank lines +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1644.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1644.severity = warning + +# Title : Included documentation file does not exist +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1645.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1645.severity = warning + +# Title : Included documentation XPath does not exist +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1646.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1646.severity = warning + +# Title : Include node does not contain valid file and path +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1647.md +# Tags : NotConfigurable +dotnet_diagnostic.SA1647.severity = warning + +# Title : inheritdoc should be used with inheriting class +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1648.md +dotnet_diagnostic.SA1648.severity = warning + +# Title : File name should match first type name +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1649.md +dotnet_diagnostic.SA1649.severity = warning + +# Title : Element documentation should be spelled correctly +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1650.md +# Tags : NotConfigurable +# Comment : Deprecated +dotnet_diagnostic.SA1650.severity = none + +# Title : Do not use placeholder elements +# Category : StyleCop.CSharp.DocumentationRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1651.md +dotnet_diagnostic.SA1651.severity = warning + +# Title : Do not prefix local calls with 'this.' +# Category : StyleCop.CSharp.ReadabilityRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SX1101.md +# Tags : Unnecessary +dotnet_diagnostic.SX1101.severity = warning + +# Title : Field names should begin with underscore +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SX1309.md +dotnet_diagnostic.SX1309.severity = none + +# Title : Static field names should begin with underscore +# Category : StyleCop.CSharp.NamingRules +# Help Link: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SX1309S.md +dotnet_diagnostic.SX1309S.severity = none + +# Title : Avoid legacy thread switching APIs +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD001.md +dotnet_diagnostic.VSTHRD001.severity = warning + +# Title : Avoid problematic synchronous waits +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD002.md +dotnet_diagnostic.VSTHRD002.severity = warning + +# Title : Avoid awaiting foreign Tasks +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD003.md +dotnet_diagnostic.VSTHRD003.severity = warning + +# Title : Await SwitchToMainThreadAsync +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD004.md +dotnet_diagnostic.VSTHRD004.severity = error + +# Title : Invoke single-threaded types on Main thread +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD010.md +# Tags : CompilationEnd +dotnet_diagnostic.VSTHRD010.severity = warning + +# Title : Use AsyncLazy +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD011.md +dotnet_diagnostic.VSTHRD011.severity = error + +# Title : Provide JoinableTaskFactory where allowed +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD012.md +dotnet_diagnostic.VSTHRD012.severity = warning + +# Title : Avoid async void methods +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD100.md +dotnet_diagnostic.VSTHRD100.severity = error + +# Title : Avoid unsupported async delegates +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD101.md +dotnet_diagnostic.VSTHRD101.severity = error + +# Title : Implement internal logic asynchronously +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD102.md +dotnet_diagnostic.VSTHRD102.severity = suggestion + +# Title : Call async methods when in an async method +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD103.md +dotnet_diagnostic.VSTHRD103.severity = none + +# Title : Offer async methods +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD104.md +dotnet_diagnostic.VSTHRD104.severity = none + +# Title : Avoid method overloads that assume TaskScheduler.Current +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD105.md +dotnet_diagnostic.VSTHRD105.severity = warning + +# Title : Use InvokeAsync to raise async events +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD106.md +dotnet_diagnostic.VSTHRD106.severity = warning + +# Title : Await Task within using expression +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD107.md +dotnet_diagnostic.VSTHRD107.severity = error + +# Title : Assert thread affinity unconditionally +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD108.md +dotnet_diagnostic.VSTHRD108.severity = warning + +# Title : Switch instead of assert in async methods +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD109.md +dotnet_diagnostic.VSTHRD109.severity = error + +# Title : Observe result of async calls +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD110.md +dotnet_diagnostic.VSTHRD110.severity = none + +# Title : Use ConfigureAwait(bool) +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD111.md +dotnet_diagnostic.VSTHRD111.severity = none + +# Title : Implement System.IAsyncDisposable +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD112.md +dotnet_diagnostic.VSTHRD112.severity = suggestion + +# Title : Check for System.IAsyncDisposable +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD113.md +dotnet_diagnostic.VSTHRD113.severity = suggestion + +# Title : Avoid returning a null Task +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD114.md +dotnet_diagnostic.VSTHRD114.severity = warning + +# Title : Avoid returning a null Task +# Category : Usage +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD114.md +dotnet_diagnostic.VSTHRD114.severity = error + +# Title : Use "Async" suffix for async methods +# Category : Style +# Help Link: https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD200.md +dotnet_diagnostic.VSTHRD200.severity = none + +# Title : Test classes must be public +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1000 +dotnet_diagnostic.xUnit1000.severity = error + +# Title : Fact methods cannot have parameters +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1001 +dotnet_diagnostic.xUnit1001.severity = error + +# Title : Test methods cannot have multiple Fact or Theory attributes +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1002 +dotnet_diagnostic.xUnit1002.severity = error + +# Title : Theory methods must have test data +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1003 +dotnet_diagnostic.xUnit1003.severity = error + +# Title : Test methods should not be skipped +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1004 +dotnet_diagnostic.xUnit1004.severity = suggestion + +# Title : Fact methods should not have test data +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1005 +dotnet_diagnostic.xUnit1005.severity = error + +# Title : Theory methods should have parameters +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1006 +dotnet_diagnostic.xUnit1006.severity = warning + +# Title : ClassData must point at a valid class +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1007 +dotnet_diagnostic.xUnit1007.severity = error + +# Title : Test data attribute should only be used on a Theory +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1008 +dotnet_diagnostic.xUnit1008.severity = error + +# Title : InlineData values must match the number of method parameters +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1009 +dotnet_diagnostic.xUnit1009.severity = error + +# Title : The value is not convertible to the method parameter type +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1010 +dotnet_diagnostic.xUnit1010.severity = error + +# Title : There is no matching method parameter +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1011 +dotnet_diagnostic.xUnit1011.severity = error + +# Title : Null should not be used for value type parameters +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1012 +dotnet_diagnostic.xUnit1012.severity = warning + +# Title : Public method should be marked as test +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1013 +dotnet_diagnostic.xUnit1013.severity = warning + +# Title : MemberData should use nameof operator for member name +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1014 +dotnet_diagnostic.xUnit1014.severity = warning + +# Title : MemberData must reference an existing member +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1015 +dotnet_diagnostic.xUnit1015.severity = error + +# Title : MemberData must reference a public member +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1016 +dotnet_diagnostic.xUnit1016.severity = error + +# Title : MemberData must reference a static member +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1017 +dotnet_diagnostic.xUnit1017.severity = error + +# Title : MemberData must reference a valid member kind +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1018 +dotnet_diagnostic.xUnit1018.severity = error + +# Title : MemberData must reference a member providing a valid data type +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1019 +dotnet_diagnostic.xUnit1019.severity = error + +# Title : MemberData must reference a property with a public getter +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1020 +dotnet_diagnostic.xUnit1020.severity = error + +# Title : MemberData should not have parameters if the referenced member is not a method +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1021 +dotnet_diagnostic.xUnit1021.severity = error + +# Title : Theory methods cannot have a parameter array +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1022 +dotnet_diagnostic.xUnit1022.severity = error + +# Title : Theory methods cannot have default parameter values +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1023 +dotnet_diagnostic.xUnit1023.severity = error + +# Title : Test methods cannot have overloads +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1024 +dotnet_diagnostic.xUnit1024.severity = error + +# Title : InlineData should be unique within the Theory it belongs to +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1025 +dotnet_diagnostic.xUnit1025.severity = warning + +# Title : Theory methods should use all of their parameters +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1026 +dotnet_diagnostic.xUnit1026.severity = warning + +# Title : Collection definition classes must be public +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1027 +dotnet_diagnostic.xUnit1027.severity = error + +# Title : Test classes decorated with 'Xunit.IClassFixture' or 'Xunit.ICollectionFixture' should add a constructor argument of type TFixture +# Category : Usage +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit1033 +dotnet_diagnostic.xUnit1033.severity = suggestion + +# Title : Constants and literals should be the expected argument +# Category : Assertions +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit2000 +dotnet_diagnostic.xUnit2000.severity = warning + +# Title : Do not use invalid equality check +# Category : Assertions +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit2001 +dotnet_diagnostic.xUnit2001.severity = silent + +# Title : Do not use null check on value type +# Category : Assertions +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit2002 +dotnet_diagnostic.xUnit2002.severity = warning + +# Title : Do not use equality check to test for null value +# Category : Assertions +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit2003 +dotnet_diagnostic.xUnit2003.severity = warning + +# Title : Do not use equality check to test for boolean conditions +# Category : Assertions +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit2004 +dotnet_diagnostic.xUnit2004.severity = warning + +# Title : Do not use identity check on value type +# Category : Assertions +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit2005 +dotnet_diagnostic.xUnit2005.severity = warning + +# Title : Do not use invalid string equality check +# Category : Assertions +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit2006 +dotnet_diagnostic.xUnit2006.severity = warning + +# Title : Do not use typeof expression to check the type +# Category : Assertions +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit2007 +dotnet_diagnostic.xUnit2007.severity = warning + +# Title : Do not use boolean check to match on regular expressions +# Category : Assertions +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit2008 +dotnet_diagnostic.xUnit2008.severity = warning + +# Title : Do not use boolean check to check for substrings +# Category : Assertions +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit2009 +dotnet_diagnostic.xUnit2009.severity = warning + +# Title : Do not use boolean check to check for string equality +# Category : Assertions +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit2010 +dotnet_diagnostic.xUnit2010.severity = warning + +# Title : Do not use empty collection check +# Category : Assertions +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit2011 +dotnet_diagnostic.xUnit2011.severity = warning + +# Title : Do not use boolean check to check if a value exists in a collection +# Category : Assertions +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit2012 +dotnet_diagnostic.xUnit2012.severity = warning + +# Title : Do not use equality check to check for collection size. +# Category : Assertions +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit2013 +dotnet_diagnostic.xUnit2013.severity = warning + +# Title : Do not use throws check to check for asynchronously thrown exception +# Category : Assertions +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit2014 +dotnet_diagnostic.xUnit2014.severity = error + +# Title : Do not use typeof expression to check the exception type +# Category : Assertions +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit2015 +dotnet_diagnostic.xUnit2015.severity = warning + +# Title : Keep precision in the allowed range when asserting equality of doubles or decimals. +# Category : Assertions +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit2016 +dotnet_diagnostic.xUnit2016.severity = error + +# Title : Do not use Contains() to check if a value exists in a collection +# Category : Assertions +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit2017 +dotnet_diagnostic.xUnit2017.severity = warning + +# Title : Do not compare an object's exact type to an abstract class or interface +# Category : Assertions +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit2018 +dotnet_diagnostic.xUnit2018.severity = warning + +# Title : Do not use obsolete throws check to check for asynchronously thrown exception +# Category : Assertions +# Help Link: https://xunit.github.io/xunit.analyzers/rules/xUnit2019 +dotnet_diagnostic.xUnit2019.severity = warning + +# Title : Test case classes must derive directly or indirectly from Xunit.LongLivedMarshalByRefObject +# Category : Extensibility +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit3000 +dotnet_diagnostic.xUnit3000.severity = error + +# Title : Classes that implement Xunit.Abstractions.IXunitSerializable must have a public parameterless constructor +# Category : Extensibility +# Help Link: https://xunit.net/xunit.analyzers/rules/xUnit3001 +dotnet_diagnostic.xUnit3001.severity = error + diff --git a/test/Directory.Build.props b/test/Directory.Build.props new file mode 100644 index 0000000000..d6f7ab7ce4 --- /dev/null +++ b/test/Directory.Build.props @@ -0,0 +1,11 @@ + + + false + + + + + + $(LatestTargetFramework) + + diff --git a/test/Directory.Build.targets b/test/Directory.Build.targets new file mode 100644 index 0000000000..47cb284b86 --- /dev/null +++ b/test/Directory.Build.targets @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/test/Generators/Directory.Build.props b/test/Generators/Directory.Build.props new file mode 100644 index 0000000000..09ef8730ba --- /dev/null +++ b/test/Generators/Directory.Build.props @@ -0,0 +1,16 @@ + + + + + + <_RestoreOutputPath>$(RestoreOutputPath.Replace('Unit', 'Generated')) + <_GeneratedFilesDir>$([MSBuild]::NormalizePath('$(_RestoreOutputPath)', '$(Configuration)', '$(TargetFramework)', 'generated')) + + + + + + + + + diff --git a/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/BasicRequestsTests.cs b/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/BasicRequestsTests.cs new file mode 100644 index 0000000000..ee9bf2d687 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/BasicRequestsTests.cs @@ -0,0 +1,253 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.AutoClient; +using Moq; +using Moq.Protected; +using TestClasses; +using Xunit; + +namespace Microsoft.Gen.AutoClient.Test; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "IDisposable inside mock setups")] +public class BasicRequestsTests +{ + private readonly Mock _handlerMock = new(MockBehavior.Strict); + private readonly Mock _factoryMock = new(MockBehavior.Strict); + private readonly IBasicTestClient _sut; + + public BasicRequestsTests() + { + _factoryMock.Setup(m => m.CreateClient("MyClient")).Returns(new HttpClient(_handlerMock.Object) + { + BaseAddress = new Uri("https://example.com/") + }); + + var services = new ServiceCollection(); + services.AddSingleton(_ => _factoryMock.Object); + services.AddBasicTestClient(); + var provider = services.BuildServiceProvider(); + + _sut = provider.GetRequiredService(); + } + + [Fact] + public async Task DeleteRequest() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Delete && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success DELETE!") + }); + + var response = await _sut.DeleteUsers(); + + Assert.Equal("Success DELETE!", response); + } + + [Fact] + public async Task GetRequest() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success GET!") + }); + + var response = await _sut.GetUsers(); + + Assert.Equal("Success GET!", response); + } + + [Fact] + public async Task GetRequestCancellationToken() + { + var ct = new CancellationToken(true); + + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success GET!") + }); + +#if NET5_0_OR_GREATER + await Assert.ThrowsAsync(() => _sut.GetUsersWithCancellationToken(ct)); +#else + await Assert.ThrowsAsync(() => _sut.GetUsersWithCancellationToken(ct)); +#endif + } + + [Fact] + public async Task HeadRequest() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Head && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success HEAD!") + }); + + var response = await _sut.HeadUsers(); + + Assert.Equal("Success HEAD!", response); + } + + [Fact] + public async Task OptionsRequest() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Options && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success OPTIONS!") + }); + + var response = await _sut.OptionsUsers(); + + Assert.Equal("Success OPTIONS!", response); + } + + [Fact] + public async Task PatchRequest() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == new HttpMethod("PATCH") && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success PATCH!") + }); + + var response = await _sut.PatchUsers(); + + Assert.Equal("Success PATCH!", response); + } + + [Fact] + public async Task PostRequest() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Post && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success POST!") + }); + + var response = await _sut.PostUsers(); + + Assert.Equal("Success POST!", response); + } + + [Fact] + public async Task PutRequest() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Put && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success PUT!") + }); + + var response = await _sut.PutUsers(); + + Assert.Equal("Success PUT!", response); + } + + [Fact] + public async Task UnsuccessfulResponse() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.Forbidden, + Content = new StringContent("Forbidden") + }); + + var ex = await Assert.ThrowsAsync(() => _sut.GetUsers()); + Assert.Equal(403, ex.StatusCode); + Assert.Equal("Forbidden", ex.HttpError!.RawContent); + Assert.Equal("/api/users", ex.Path); + } + + [Fact] + public async Task FullUrlUsesBaseAddress() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.ToString() == "https://example.com/api/users"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("Ok") + }); + + var response = await _sut.GetUsers(); + + Assert.Equal("Ok", response); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/BodyTests.cs b/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/BodyTests.cs new file mode 100644 index 0000000000..dfa047653c --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/BodyTests.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Moq.Protected; +using TestClasses; +using Xunit; + +namespace Microsoft.Gen.AutoClient.Test; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "IDisposable inside mock setups")] +[System.Diagnostics.CodeAnalysis.SuppressMessage("Critical Code Smell", "S1067:Expressions should not be too complex", Justification = "Mock conditions")] +public class BodyTests +{ + private readonly Mock _handlerMock = new(MockBehavior.Strict); + private readonly Mock _factoryMock = new(MockBehavior.Strict); + + private readonly IBodyTestClient _sut; + + public BodyTests() + { + _factoryMock.Setup(m => m.CreateClient("MyClient")).Returns(new HttpClient(_handlerMock.Object) + { + BaseAddress = new Uri("https://example.com/") + }); + + var services = new ServiceCollection(); + services.AddSingleton(_ => _factoryMock.Object); + services.AddBodyTestClient(); + var provider = services.BuildServiceProvider(); + + _sut = provider.GetRequiredService(); + } + + [Fact] + public async Task JsonBody() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Post && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users" && + message.Content!.Headers.ContentType!.ToString() == "application/json; charset=utf-8" && + message.Content!.ReadAsStringAsync().Result == @"{""Value"":""MyBodyObjectValue""}"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success!") + }); + + var response = await _sut.PostUsers(new IBodyTestClient.BodyObject()); + + Assert.Equal("Success!", response); + } + + [Fact] + public async Task TextBody() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Put && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users" && + message.Content!.Headers.ContentType!.ToString() == "text/plain; charset=utf-8" && + message.Content!.ReadAsStringAsync().Result == "MyBodyObjectValue"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success!") + }); + + var response = await _sut.PutUsers(new IBodyTestClient.BodyObject()); + + Assert.Equal("Success!", response); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/HeadersTests.cs b/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/HeadersTests.cs new file mode 100644 index 0000000000..f94ab084c9 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/HeadersTests.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Moq.Protected; +using TestClasses; +using Xunit; + +namespace Microsoft.Gen.AutoClient.Test; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "IDisposable inside mock setups")] +[System.Diagnostics.CodeAnalysis.SuppressMessage("Critical Code Smell", "S1067:Expressions should not be too complex", Justification = "Mock conditions")] +public class HeadersTests +{ + private readonly Mock _handlerMock = new(MockBehavior.Strict); + private readonly Mock _factoryMock = new(MockBehavior.Strict); + + private readonly IStaticHeaderTestClient _sutStatic; + private readonly IParamHeaderTestClient _sutParam; + + public HeadersTests() + { + _factoryMock.Setup(m => m.CreateClient("MyClient")).Returns(new HttpClient(_handlerMock.Object) + { + BaseAddress = new Uri("https://example.com/") + }); + + var services = new ServiceCollection(); + services.AddSingleton(_ => _factoryMock.Object); + services.AddStaticHeaderTestClient(); + services.AddParamHeaderTestClient(); + var provider = services.BuildServiceProvider(); + + _sutStatic = provider.GetRequiredService(); + _sutParam = provider.GetRequiredService(); + } + + [Fact] + public async Task StaticHeader() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users" && + message.Headers.GetValues("X-MyHeader").First() == "MyValue"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success!") + }); + + var response = await _sutStatic.GetUsers(); + + Assert.Equal("Success!", response); + } + + [Fact] + public async Task StaticHeaderMultipleInMethod() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users" && + message.Headers.GetValues("X-MyHeader").First() == "MyValue" && + message.Headers.GetValues("X-MyHeader1").First() == "MyValue" && + message.Headers.GetValues("X-MyHeader2").First() == "MyValue"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success!") + }); + + var response = await _sutStatic.GetUsersHeaders(); + + Assert.Equal("Success!", response); + } + + [Fact] + public async Task HeaderFromParameter() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users" && + message.Headers.GetValues("X-MyHeader").First() == "MyParamValue"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success!") + }); + + var response = await _sutParam.GetUsers("MyParamValue"); + + Assert.Equal("Success!", response); + } + + [Fact] + public async Task HeaderFromParameterCustomObject() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users" && + message.Headers.GetValues("X-MyHeader").First() == "CustomObjectToString"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success!") + }); + + var response = await _sutParam.GetUsersObject(new IParamHeaderTestClient.CustomObject()); + + Assert.Equal("Success!", response); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/PathTests.cs b/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/PathTests.cs new file mode 100644 index 0000000000..af4162e85d --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/PathTests.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Moq.Protected; +using TestClasses; +using Xunit; + +namespace Microsoft.Gen.AutoClient.Test; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "IDisposable inside mock setups")] +public class PathTests +{ + private readonly Mock _handlerMock = new(MockBehavior.Strict); + private readonly Mock _factoryMock = new(MockBehavior.Strict); + + private readonly IPathTestClient _sut; + + public PathTests() + { + _factoryMock.Setup(m => m.CreateClient("MyClient")).Returns(new HttpClient(_handlerMock.Object) + { + BaseAddress = new Uri("https://example.com/") + }); + + var services = new ServiceCollection(); + services.AddSingleton(_ => _factoryMock.Object); + services.AddPathTestClient(); + var provider = services.BuildServiceProvider(); + + _sut = provider.GetRequiredService(); + } + + [Fact] + public async Task SimplePath() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users/myUser"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success!") + }); + + var response = await _sut.GetUser("myUser"); + + Assert.Equal("Success!", response); + } + + [Fact] + public async Task MultiplePathParameters() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users/myTenant/myUser"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success!") + }); + + var response = await _sut.GetUserFromTenant("myTenant", "myUser"); + + Assert.Equal("Success!", response); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/QueryTests.cs b/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/QueryTests.cs new file mode 100644 index 0000000000..90e11a5907 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/QueryTests.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Moq.Protected; +using TestClasses; +using Xunit; + +namespace Microsoft.Gen.AutoClient.Test; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "IDisposable inside mock setups")] +public class QueryTests +{ + private readonly Mock _handlerMock = new(MockBehavior.Strict); + private readonly Mock _factoryMock = new(MockBehavior.Strict); + + private readonly IQueryTestClient _sut; + + public QueryTests() + { + _factoryMock.Setup(m => m.CreateClient("MyClient")).Returns(new HttpClient(_handlerMock.Object) + { + BaseAddress = new Uri("https://example.com/") + }); + + var services = new ServiceCollection(); + services.AddSingleton(_ => _factoryMock.Object); + services.AddQueryTestClient(); + var provider = services.BuildServiceProvider(); + + _sut = provider.GetRequiredService(); + } + + [Fact] + public async Task QueryFromParameter() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users?paramQuery=myValue"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success!") + }); + + var response = await _sut.GetUsers("myValue"); + + Assert.Equal("Success!", response); + } + + [Fact] + public async Task QueryFromParameterCustom() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users?paramQueryCustom=myValue"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success!") + }); + + var response = await _sut.GetUsersCustom("myValue"); + + Assert.Equal("Success!", response); + } + + [Fact] + public async Task MultipleQueryParams() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users?paramQuery1=myValue1¶mQuery2=myValue2"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success!") + }); + + var response = await _sut.GetUsers2("myValue1", "myValue2"); + + Assert.Equal("Success!", response); + } + + [Fact] + public async Task QueryParamCustomObject() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users?paramQuery=CustomObjectToString"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success!") + }); + + var response = await _sut.GetUsersObject(new IQueryTestClient.CustomObject()); + + Assert.Equal("Success!", response); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/RestApiClientOptionsTests.cs b/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/RestApiClientOptionsTests.cs new file mode 100644 index 0000000000..7eddd3c314 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/RestApiClientOptionsTests.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Moq.Protected; +using TestClasses; +using Xunit; + +namespace Microsoft.Gen.AutoClient.Test; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "IDisposable inside mock setups")] +[System.Diagnostics.CodeAnalysis.SuppressMessage("Critical Code Smell", "S1067:Expressions should not be too complex", Justification = "Mock conditions")] +public class RestApiClientOptionsTests +{ + private readonly Mock _handlerMock = new(MockBehavior.Strict); + private readonly Mock _factoryMock = new(MockBehavior.Strict); + + private readonly IRestApiClientOptionsApi _sut; + + public RestApiClientOptionsTests() + { + _factoryMock.Setup(m => m.CreateClient("MyClient")).Returns(new HttpClient(_handlerMock.Object) + { + BaseAddress = new Uri("https://example.com/") + }); + + var services = new ServiceCollection(); + services.AddSingleton(_ => _factoryMock.Object); + services.AddRestApiClientOptionsApi(options => + { + options.JsonSerializerOptions = new JsonSerializerOptions + { + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase + }; + }); + var provider = services.BuildServiceProvider(); + + _sut = provider.GetRequiredService(); + } + + [Fact] + public async Task JsonBodyUsesJsonSerializerOptions() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Post && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/dict" && + message.Content!.Headers.ContentType!.ToString() == "application/json; charset=utf-8" && + message.Content!.ReadAsStringAsync().Result == @"{""myProperty"":""MyValue""}"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success!") + }); + + var response = await _sut.PostDictionary(new Dictionary + { + ["MyProperty"] = "MyValue" + }); + + Assert.Equal("Success!", response); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/SpecialReturnTypeTests.cs b/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/SpecialReturnTypeTests.cs new file mode 100644 index 0000000000..24634cc4ff --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/SpecialReturnTypeTests.cs @@ -0,0 +1,154 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.AutoClient; +using Moq; +using Moq.Protected; +using TestClasses; +using Xunit; + +namespace Microsoft.Gen.AutoClient.Test; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "IDisposable inside mock setups")] +public class SpecialReturnTypeTests +{ + private readonly Mock _handlerMock = new(MockBehavior.Strict); + private readonly Mock _factoryMock = new(MockBehavior.Strict); + + private readonly IReturnTypesTestClient _sut; + + public SpecialReturnTypeTests() + { + _factoryMock.Setup(m => m.CreateClient("MyClient")).Returns(new HttpClient(_handlerMock.Object) + { + BaseAddress = new Uri("https://example.com/") + }); + + var services = new ServiceCollection(); + services.AddSingleton(_ => _factoryMock.Object); + services.AddReturnTypesTestClient(); + var provider = services.BuildServiceProvider(); + + _sut = provider.GetRequiredService(); + } + + [Fact(Skip = "Flaky test, see https://github.com/dotnet/r9/issues/95")] + public async Task CustomObjectReturnType() + { + var responseObject = new IReturnTypesTestClient.CustomObject + { + CustomProperty = "CustomValue" + }; + var serialized = JsonSerializer.Serialize(responseObject); + + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(serialized, Encoding.UTF8, "application/json") + }); + + var response = await _sut.GetUsers(); + + Assert.Equal("CustomValue", response.CustomProperty); + } + + [Fact] + public async Task RawResponseReturnType() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Some raw string content") + }); + + var response = await _sut.GetUsersRaw(); + var rawResponseString = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Some raw string content", rawResponseString); + } + + [Fact] + public async Task PlainTextString() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(@"""Some raw string content""") + }); + + var response = await _sut.GetUsersTextPlain(); + + Assert.Equal(@"""Some raw string content""", response); + } + + [Fact] + public async Task JsonTextString() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(@"""Some raw string content""", Encoding.UTF8, "application/json") + }); + + var response = await _sut.GetUsersTextPlain(); + + Assert.Equal(@"""Some raw string content""", response); + } + + [Fact] + public async Task UnsupportedContentType() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("", Encoding.UTF8, "text/xml") + }); + + var ex = await Assert.ThrowsAsync(() => _sut.GetUsers()); + Assert.Equal($"The 'ReturnTypesTest' REST API returned an unsupported content type ('text/xml').", ex.Message); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/TelemetryTests.cs b/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/TelemetryTests.cs new file mode 100644 index 0000000000..b36e3a4046 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Generated/Common/TelemetryTests.cs @@ -0,0 +1,167 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Telemetry; +using Moq; +using Moq.Protected; +using TestClasses; +using Xunit; + +namespace Microsoft.Gen.AutoClient.Test; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "IDisposable inside mock setups")] +[System.Diagnostics.CodeAnalysis.SuppressMessage("Critical Code Smell", "S1067:Expressions should not be too complex", Justification = "Mock conditions")] +public class TelemetryTests +{ + private readonly Mock _handlerMock = new(MockBehavior.Strict); + private readonly Mock _factoryMock = new(MockBehavior.Strict); + + private readonly IRequestMetadataTestClient _sutClient; + private readonly IRequestMetadataTestApi _sutApi; + private readonly ICustomRequestMetadataTestClient _sutCustom; + + public TelemetryTests() + { + _factoryMock.Setup(m => m.CreateClient("MyClient")).Returns(new HttpClient(_handlerMock.Object) + { + BaseAddress = new Uri("https://example.com/") + }); + + var services = new ServiceCollection(); + services.AddSingleton(_ => _factoryMock.Object); + services.AddRequestMetadataTestClient(); + services.AddRequestMetadataTestApi(); + services.AddCustomRequestMetadataTestClient(); + var provider = services.BuildServiceProvider(); + + _sutClient = provider.GetRequiredService(); + _sutApi = provider.GetRequiredService(); + _sutCustom = provider.GetRequiredService(); + } + + [Fact] + public async Task ClientDependencyComplexPath() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users/myUser?search=searchParam" && + message.GetRequestMetadata()!.RequestRoute == "/api/users/{userId}?search={search}" && + message.GetRequestMetadata()!.DependencyName == "RequestMetadataTest" && + message.GetRequestMetadata()!.RequestName == "GetUser"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success!") + }); + + var response = await _sutClient.GetUser("myUser", "searchParam"); + + Assert.Equal("Success!", response); + } + + [Fact] + public async Task ClientDependencyMethodNameAsync() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users" && + message.GetRequestMetadata()!.RequestRoute == "/api/users" && + message.GetRequestMetadata()!.DependencyName == "RequestMetadataTest" && + message.GetRequestMetadata()!.RequestName == "GetUsers"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success!") + }); + + var response = await _sutClient.GetUsersAsync(); + + Assert.Equal("Success!", response); + } + + [Fact] + public async Task ApiDependencyMethodNameAsync() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users" && + message.GetRequestMetadata()!.RequestRoute == "/api/users" && + message.GetRequestMetadata()!.DependencyName == "RequestMetadataTest" && + message.GetRequestMetadata()!.RequestName == "GetUsers"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success!") + }); + + var response = await _sutApi.GetUsersAsync(); + + Assert.Equal("Success!", response); + } + + [Fact] + public async Task CustomDependencyName() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/user" && + message.GetRequestMetadata()!.RequestRoute == "/api/user" && + message.GetRequestMetadata()!.DependencyName == "MyDependency" && + message.GetRequestMetadata()!.RequestName == "GetUser"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success!") + }); + + var response = await _sutCustom.GetUser(); + + Assert.Equal("Success!", response); + } + + [Fact] + public async Task CustomRequestName() + { + _handlerMock + .Protected() + .Setup>("SendAsync", ItExpr.Is(message => + message.Method == HttpMethod.Get && + message.RequestUri != null && + message.RequestUri.PathAndQuery == "/api/users" && + message.GetRequestMetadata()!.RequestRoute == "/api/users" && + message.GetRequestMetadata()!.DependencyName == "MyDependency" && + message.GetRequestMetadata()!.RequestName == "MyRequestName"), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Success!") + }); + + var response = await _sutCustom.GetUsers(); + + Assert.Equal("Success!", response); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/Generated/Directory.Build.props b/test/Generators/Microsoft.Gen.AutoClient/Generated/Directory.Build.props new file mode 100644 index 0000000000..da0aacda95 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Generated/Directory.Build.props @@ -0,0 +1,31 @@ + + + + + Microsoft.Gen.AutoClient.Test + Tests for code generated by Gen.AutoClient. + + + + $(NetCoreTargetFrameworks) + $(NetCoreTargetFrameworks)$(ConditionalNet462) + true + true + $(NoWarn);IDE0161;S1144 + + + + + + + + + + + + + + + + + diff --git a/test/Generators/Microsoft.Gen.AutoClient/Generated/Roslyn3.8/Microsoft.Gen.AutoClient.Roslyn3.8.Generated.Tests.csproj b/test/Generators/Microsoft.Gen.AutoClient/Generated/Roslyn3.8/Microsoft.Gen.AutoClient.Roslyn3.8.Generated.Tests.csproj new file mode 100644 index 0000000000..eac2eac217 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Generated/Roslyn3.8/Microsoft.Gen.AutoClient.Roslyn3.8.Generated.Tests.csproj @@ -0,0 +1,5 @@ + + + 3.8 + + diff --git a/test/Generators/Microsoft.Gen.AutoClient/Generated/Roslyn4.0/Microsoft.Gen.AutoClient.Roslyn4.0.Generated.Tests.csproj b/test/Generators/Microsoft.Gen.AutoClient/Generated/Roslyn4.0/Microsoft.Gen.AutoClient.Roslyn4.0.Generated.Tests.csproj new file mode 100644 index 0000000000..0cd2e6b265 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Generated/Roslyn4.0/Microsoft.Gen.AutoClient.Roslyn4.0.Generated.Tests.csproj @@ -0,0 +1,6 @@ + + + 4.0 + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + diff --git a/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IBasicTestClient.cs b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IBasicTestClient.cs new file mode 100644 index 0000000000..967d561571 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IBasicTestClient.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.AutoClient; + +namespace TestClasses +{ + [AutoClient("MyClient")] + public interface IBasicTestClient + { + [Get("/api/users")] + public Task GetUsers(CancellationToken cancellationToken = default); + + [Delete("/api/users")] + public Task DeleteUsers(CancellationToken cancellationToken = default); + + [Head("/api/users")] + public Task HeadUsers(CancellationToken cancellationToken = default); + + [Options("/api/users")] + public Task OptionsUsers(CancellationToken cancellationToken = default); + + [Patch("/api/users")] + public Task PatchUsers(CancellationToken cancellationToken = default); + + [Post("/api/users")] + public Task PostUsers(CancellationToken cancellationToken = default); + + [Put("/api/users")] + public Task PutUsers(CancellationToken cancellationToken = default); + + [Get("/api/users")] + public Task GetUsersWithCancellationToken(CancellationToken cancellationToken); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IBodyTestClient.cs b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IBodyTestClient.cs new file mode 100644 index 0000000000..ea130ebbfc --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IBodyTestClient.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.AutoClient; + +namespace TestClasses +{ + [AutoClient("MyClient")] + public interface IBodyTestClient + { + [Post("/api/users")] + public Task PostUsers([Body] BodyObject body, CancellationToken cancellationToken = default); + + [Put("/api/users")] + public Task PutUsers([Body(BodyContentType.TextPlain)] BodyObject body, CancellationToken cancellationToken = default); + + public class BodyObject + { + public string Value { get; set; } = "MyBodyObjectValue"; + + public override string? ToString() => Value; + } + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/TestClasses/ICustomRequestMetadataTestClient.cs b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/ICustomRequestMetadataTestClient.cs new file mode 100644 index 0000000000..be3743b152 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/ICustomRequestMetadataTestClient.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.AutoClient; + +namespace TestClasses +{ + [AutoClient("MyClient", "MyDependency")] + public interface ICustomRequestMetadataTestClient + { + [Get("/api/user")] + public Task GetUser(CancellationToken cancellationToken = default); + + [Get("/api/users")] + [RequestName("MyRequestName")] + public Task GetUsers(CancellationToken cancellationToken = default); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IParamHeaderTestClient.cs b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IParamHeaderTestClient.cs new file mode 100644 index 0000000000..99d379e337 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IParamHeaderTestClient.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.AutoClient; + +namespace TestClasses +{ + [AutoClient("MyClient")] + public interface IParamHeaderTestClient + { + [Get("/api/users")] + public Task GetUsers([Header("X-MyHeader")] string headerValue, CancellationToken cancellationToken = default); + + [Get("/api/users")] + public Task GetUsersObject([Header("X-MyHeader")] CustomObject headerValue, CancellationToken cancellationToken = default); + + public class CustomObject + { + public override string? ToString() => "CustomObjectToString"; + } + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IPathTestClient.cs b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IPathTestClient.cs new file mode 100644 index 0000000000..e7f7ac54c3 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IPathTestClient.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.AutoClient; + +namespace TestClasses +{ + [AutoClient("MyClient")] + public interface IPathTestClient + { + [Get("/api/users/{userId}")] + public Task GetUser(string userId, CancellationToken cancellationToken = default); + + [Get("/api/users/{tenantId}/{userId}")] + public Task GetUserFromTenant(string tenantId, string userId, CancellationToken cancellationToken = default); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IQueryTestClient.cs b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IQueryTestClient.cs new file mode 100644 index 0000000000..cd00df0ae6 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IQueryTestClient.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.AutoClient; + +namespace TestClasses +{ + [AutoClient("MyClient")] + public interface IQueryTestClient + { + [Get("/api/users")] + public Task GetUsers([Query] string paramQuery, CancellationToken cancellationToken = default); + + [Get("/api/users")] + public Task GetUsersCustom([Query("paramQueryCustom")] string paramQuery, CancellationToken cancellationToken = default); + + [Get("/api/users")] + public Task GetUsers2([Query] string paramQuery1, [Query] string paramQuery2, CancellationToken cancellationToken = default); + + [Get("/api/users")] + public Task GetUsersObject([Query] CustomObject paramQuery, CancellationToken cancellationToken = default); + + public class CustomObject + { + public override string? ToString() => "CustomObjectToString"; + } + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IRequestMetadataTestApi.cs b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IRequestMetadataTestApi.cs new file mode 100644 index 0000000000..d1a228d46e --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IRequestMetadataTestApi.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.AutoClient; + +namespace TestClasses +{ + [AutoClient("MyClient")] + public interface IRequestMetadataTestApi + { + [Get("/api/users")] + public Task GetUsersAsync(CancellationToken cancellationToken = default); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IRequestMetadataTestClient.cs b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IRequestMetadataTestClient.cs new file mode 100644 index 0000000000..a9f9af0292 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IRequestMetadataTestClient.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.AutoClient; + +namespace TestClasses +{ + [AutoClient("MyClient")] + public interface IRequestMetadataTestClient + { + [Get("/api/users/{userId}")] + public Task GetUser(string userId, [Query] string search, CancellationToken cancellationToken = default); + + [Get("/api/users")] + public Task GetUsersAsync(CancellationToken cancellationToken = default); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IRestApiClientOptionsApi.cs b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IRestApiClientOptionsApi.cs new file mode 100644 index 0000000000..9aed97957f --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IRestApiClientOptionsApi.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.AutoClient; + +namespace TestClasses +{ + [AutoClient("MyClient")] + public interface IRestApiClientOptionsApi + { + [Post("/api/dict")] + public Task PostDictionary([Body] Dictionary body, CancellationToken cancellationToken = default); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IReturnTypesTestClient.cs b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IReturnTypesTestClient.cs new file mode 100644 index 0000000000..60d5b49b8a --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IReturnTypesTestClient.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.AutoClient; + +namespace TestClasses +{ + [AutoClient("MyClient")] + public interface IReturnTypesTestClient + { + [Get("/api/users")] + public Task GetUsers(CancellationToken cancellationToken = default); + + [Get("/api/users")] + public Task GetUsersRaw(CancellationToken cancellationToken = default); + + [Get("/api/users")] + public Task GetUsersTextPlain(CancellationToken cancellationToken = default); + + public class CustomObject + { + public string CustomProperty { get; set; } = string.Empty; + } + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IStaticHeaderTestClient.cs b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IStaticHeaderTestClient.cs new file mode 100644 index 0000000000..5fa4353c04 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/TestClasses/IStaticHeaderTestClient.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.AutoClient; + +namespace TestClasses +{ + [AutoClient("MyClient")] + [StaticHeader("X-MyHeader", "MyValue")] + public interface IStaticHeaderTestClient + { + [Get("/api/users")] + public Task GetUsers(CancellationToken cancellationToken = default); + + [Get("/api/users")] + [StaticHeader("X-MyHeader1", "MyValue")] + [StaticHeader("X-MyHeader2", "MyValue")] + public Task GetUsersHeaders(CancellationToken cancellationToken = default); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/BodyContentTypeParamExtensionsTests.cs b/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/BodyContentTypeParamExtensionsTests.cs new file mode 100644 index 0000000000..474c7aa5cb --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/BodyContentTypeParamExtensionsTests.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Gen.AutoClient.Model; +using Xunit; + +namespace Microsoft.Gen.AutoClient.Test; + +public class BodyContentTypeParamExtensionsTests +{ + [Fact] + public void ConvertToString() + { + Assert.Equal("application/json", BodyContentTypeParamExtensions.ConvertToString(BodyContentTypeParam.ApplicationJson)); + Assert.Equal("text/plain", BodyContentTypeParamExtensions.ConvertToString(BodyContentTypeParam.TextPlain)); + Assert.Equal("", BodyContentTypeParamExtensions.ConvertToString((BodyContentTypeParam)999)); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/DiagDescriptorsTests.cs b/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/DiagDescriptorsTests.cs new file mode 100644 index 0000000000..8c18558493 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/DiagDescriptorsTests.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Xunit; + +namespace Microsoft.Gen.AutoClient.Test; + +public class DiagDescriptorsTests +{ + public static IEnumerable DiagDescriptorsData() + { + var type = typeof(DiagDescriptors); + foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.GetProperty)) + { + var value = property.GetValue(type, null); + yield return new[] { value }; + } + } + + [Theory] + [MemberData(nameof(DiagDescriptorsData))] + public void ShouldContainValidLinkAndBeEnabled(DiagnosticDescriptor descriptor) + { + Assert.True(descriptor.IsEnabledByDefault, descriptor.Id + " should be enabled by default"); + Assert.EndsWith("/" + descriptor.Id, descriptor.HelpLinkUri, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/EmitterTests.cs b/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/EmitterTests.cs new file mode 100644 index 0000000000..650461228b --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/EmitterTests.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.AutoClient; +using Microsoft.Gen.Shared; +using Xunit; + +namespace Microsoft.Gen.AutoClient.Test; + +public class EmitterTests +{ + [Fact] + public async Task TestEmitter() + { + var sources = new List(); + foreach (var file in Directory.GetFiles("TestClasses")) + { + sources.Add(File.ReadAllText(file)); + } + + var (d, r) = await RoslynTestUtils.RunGenerator( + new Generator(), + new[] + { + Assembly.GetAssembly(typeof(AutoClientAttribute))!, + Assembly.GetAssembly(typeof(GetAttribute))! + }, + sources) + .ConfigureAwait(false); + + Assert.Empty(d); + Assert.Single(r); + + var goldenClient = File.ReadAllText("GoldenFiles/Microsoft.Gen.AutoClient/Microsoft.Gen.AutoClient.Generator/AutoClients.g.cs"); + var result = r[0].SourceText.ToString(); + Assert.Equal(goldenClient, result); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/ParserTests.cs b/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/ParserTests.cs new file mode 100644 index 0000000000..a3635e3ee7 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/ParserTests.cs @@ -0,0 +1,495 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Extensions.Http.AutoClient; +using Microsoft.Gen.Shared; +using Xunit; + +namespace Microsoft.Gen.AutoClient.Test; + +public class ParserTests +{ + [Fact] + public void NoSymbols() + { + var comp = CSharpCompilation.Create(null); + var p = new Parser(comp, (_) => { }, default); + Assert.Empty(p.GetRestApiClasses(new List())); + } + + [Fact] + public async Task ApiIsClass() + { + var d = await RunGenerator(@" + [AutoClient(""MyClient"")] + public class C + { + [Get(""/api/users"")] + public Task GetUsers(CancellationToken cancellationToken) { return """"; } + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task NestedNamespace() + { + var d = await RunGenerator(@" + namespace ParentNamespace + { + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public Task GetUsers(CancellationToken cancellationToken); + } + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task NoAttributes() + { + var d = await RunGenerator(@" + public interface IClient + { + public Task GetUsers(CancellationToken cancellationToken); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task InvalidInterfaceName() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface Client + { + [Get(""/api/users"")] + public Task GetUsers(CancellationToken cancellationToken); + }"); + + var d = Assert.Single(ds); + Assert.Equal(DiagDescriptors.ErrorInterfaceName.Id, d.Id); + } + + [Fact] + public async Task InvalidMethodReturnType() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public string GetUsers(CancellationToken cancellationToken); + }"); + + Assert.Contains(DiagDescriptors.ErrorInvalidReturnType.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task InvalidMethodReturnTypeNullable() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public Task GetUsers(CancellationToken cancellationToken); + }"); + + Assert.Contains(DiagDescriptors.ErrorInvalidReturnType.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task InvalidMethodReturnTypeMultipleTypeArguments() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public Task GetUsers(CancellationToken cancellationToken); + }"); + + Assert.Contains(DiagDescriptors.ErrorInvalidReturnType.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task GenericInterface() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public Task GetUsers(CancellationToken cancellationToken); + }"); + + var d = Assert.Single(ds); + Assert.Equal(DiagDescriptors.ErrorInterfaceIsGeneric.Id, d.Id); + } + + [Fact] + public async Task MissingRestApiAttribute() + { + var d = await RunGenerator(@" + public interface IClient + { + [Get(""/api/users"")] + public Task GetUsers(CancellationToken cancellationToken); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task NestedClass() + { + var ds = await RunGenerator(@" + public class B + { + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public Task GetUsers(CancellationToken cancellationToken); + } + }"); + + var d = Assert.Single(ds); + Assert.Equal(DiagDescriptors.ErrorClientMustNotBeNested.Id, d.Id); + } + + [Fact] + public async Task MissingRestApiMethods() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + public Task GetUsers(CancellationToken cancellationToken); + }"); + + Assert.Contains(DiagDescriptors.WarningRestClientWithoutRestMethods.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task MultipleApiMethodAttributes() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + [Post(""/api/users"")] + public Task GetUsers(CancellationToken cancellationToken); + }"); + + Assert.Contains(DiagDescriptors.ErrorApiMethodMoreThanOneAttribute.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task WithRequestNameAttributes() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [RequestName(""MyRequest"")] + [Post(""/api/users"")] + public Task GetUsers(CancellationToken cancellationToken); + }"); + + Assert.Empty(ds); + } + + [Fact] + public async Task WithQueryInPath() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Post(""/api/users?query=true"")] + public Task GetUsers(CancellationToken cancellationToken); + }"); + + Assert.Contains(DiagDescriptors.ErrorPathWithQuery.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task InvalidMethodName() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public Task _GetUsers(CancellationToken cancellationToken); + }"); + + Assert.Contains(DiagDescriptors.ErrorInvalidMethodName.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task GenericMethodUnsupported() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public Task GetUsers(CancellationToken cancellationToken); + }"); + + Assert.Contains(DiagDescriptors.ErrorMethodIsGeneric.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task StaticMethodUnsupported() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public static string GetUsers(CancellationToken cancellationToken); + }"); + + Assert.Contains(DiagDescriptors.ErrorStaticMethod.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task MissingNamespace() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public Task GetUsers(CancellationToken cancellationToken); + }", inNamespace: false); + + Assert.Empty(ds); + } + + [Fact] + public async Task InvalidMethodAttribute() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [AutoClient(""/api/users"")] + public Task GetUsers(CancellationToken cancellationToken); + }"); + + Assert.Contains(DiagDescriptors.ErrorMissingMethodAttribute.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task InvalidParameterName() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public Task GetUsers(string _param, CancellationToken cancellationToken); + }"); + + Assert.Contains(DiagDescriptors.ErrorInvalidParameterName.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task UnsupportedBodyMethod() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public Task GetUsers([Body] string param, CancellationToken cancellationToken); + }"); + + Assert.Contains(DiagDescriptors.ErrorUnsupportedMethodBody.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task DuplicateBody() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public Task GetUsers([Body] string param, [Body] string param2, CancellationToken cancellationToken); + }"); + + Assert.Contains(DiagDescriptors.ErrorDuplicateBody.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task ParameterMissingUrl() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public Task GetUser(string userId, CancellationToken cancellationToken); + }"); + + Assert.Contains(DiagDescriptors.ErrorMissingParameterUrl.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task SingleCancellationToken() + { + var d = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public Task GetUsers(CancellationToken token); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task MissingCancellationToken() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public Task GetUsers(); + }"); + + Assert.Contains(DiagDescriptors.ErrorMissingCancellationToken.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task ErrorSymbolNotImported() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public Task GetUsers(SomeSymbol symbol, CancellationToken token); + }"); + + Assert.Contains(DiagDescriptors.WarningRestClientWithoutRestMethods.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task DoubleCancellationToken() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public Task GetUsers(CancellationToken token, CancellationToken token2); + }"); + + Assert.Contains(DiagDescriptors.ErrorDuplicateCancellationToken.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task RequestNameDuplicate() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public Task GetUsers(CancellationToken token); + + [Get(""/api/users"")] + public Task GetUsersAsync(CancellationToken token); + }"); + + Assert.Contains(DiagDescriptors.ErrorDuplicateRequestName.Id, ds.Select(x => x.Id)); + } + + [Fact] + public async Task RequestNameDuplicateAttribute() + { + var ds = await RunGenerator(@" + [AutoClient(""MyClient"")] + public interface IClient + { + [Get(""/api/users"")] + public Task GetUsers(CancellationToken token); + + [Get(""/api/users"")] + [RequestName(""GetUsers"")] + public Task GetSomeUsers(CancellationToken token); + }"); + + Assert.Contains(DiagDescriptors.ErrorDuplicateRequestName.Id, ds.Select(x => x.Id)); + } + + private static async Task> RunGenerator( + string code, + bool wrap = true, + bool inNamespace = true, + bool includeBaseReferences = true, + bool includeRestApi = true, + CancellationToken cancellationToken = default) + { + var text = code; + if (wrap) + { + var nspaceStart = "namespace Test {"; + var nspaceEnd = "}"; + if (!inNamespace) + { + nspaceStart = ""; + nspaceEnd = ""; + } + + text = $@" + {nspaceStart} + using Microsoft.Extensions.Http.AutoClient; + using System.Threading; + using System.Threading.Tasks; + {code} + {nspaceEnd} + "; + } + + Assembly[]? refs = null; + if (includeRestApi) + { + refs = new[] + { + Assembly.GetAssembly(typeof(AutoClientAttribute))! + }; + } + + var (d, _) = await RoslynTestUtils.RunGenerator( + new Generator(), + refs, + new[] { text }, + includeBaseReferences: includeBaseReferences, + cancellationToken: cancellationToken).ConfigureAwait(false); + + return d; + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/RestApiMethodParameterTests.cs b/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/RestApiMethodParameterTests.cs new file mode 100644 index 0000000000..c664470e69 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/RestApiMethodParameterTests.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Gen.AutoClient.Model; +using Xunit; + +namespace Microsoft.Gen.AutoClient.Test; + +public class RestApiMethodParameterTests +{ + [Fact] + public void Fields_Should_BeInitialized() + { + var instance = new RestApiMethodParameter(); + Assert.Empty(instance.Name); + Assert.Empty(instance.Type); + Assert.Null(instance.HeaderName); + Assert.Null(instance.QueryKey); + Assert.Null(instance.BodyType); + Assert.False(instance.IsHeader); + Assert.False(instance.IsQuery); + Assert.False(instance.IsBody); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/RestApiMethodTests.cs b/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/RestApiMethodTests.cs new file mode 100644 index 0000000000..c89386afd5 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/RestApiMethodTests.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Gen.AutoClient.Model; +using Xunit; + +namespace Microsoft.Gen.AutoClient.Test; + +public class RestApiMethodTests +{ + [Fact] + public void Fields_Should_BeInitialized() + { + var instance = new RestApiMethod(); + Assert.Empty(instance.AllParameters); + Assert.Empty(instance.FormatParameters); + Assert.Empty(instance.MethodName); + Assert.Empty(instance.HttpMethod!); + Assert.Empty(instance.Path!); + Assert.Empty(instance.ReturnType!); + Assert.Empty(instance.RequestName); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/SymbolLoaderTests.cs b/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/SymbolLoaderTests.cs new file mode 100644 index 0000000000..fd0ad42eea --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Unit/Common/SymbolLoaderTests.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace Microsoft.Gen.AutoClient.Test; + +public class SymbolLoaderTests +{ + [Fact] + public void RestApiAttributeNotAvailable() + { + var comp = CSharpCompilation.Create(null); + Assert.Null(SymbolLoader.LoadSymbols(comp)); + } +} diff --git a/test/Generators/Microsoft.Gen.AutoClient/Unit/Directory.Build.props b/test/Generators/Microsoft.Gen.AutoClient/Unit/Directory.Build.props new file mode 100644 index 0000000000..86c153304c --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Unit/Directory.Build.props @@ -0,0 +1,29 @@ + + + + + Microsoft.Gen.AutoClient.Unit.Test + Unit tests for Gen.AutoClient. + + + + true + true + + + + + + + + + + + + + + + + + + diff --git a/test/Generators/Microsoft.Gen.AutoClient/Unit/Roslyn3.8/Microsoft.Gen.AutoClient.Roslyn3.8.Unit.Tests.csproj b/test/Generators/Microsoft.Gen.AutoClient/Unit/Roslyn3.8/Microsoft.Gen.AutoClient.Roslyn3.8.Unit.Tests.csproj new file mode 100644 index 0000000000..eac2eac217 --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Unit/Roslyn3.8/Microsoft.Gen.AutoClient.Roslyn3.8.Unit.Tests.csproj @@ -0,0 +1,5 @@ + + + 3.8 + + diff --git a/test/Generators/Microsoft.Gen.AutoClient/Unit/Roslyn4.0/Microsoft.Gen.AutoClient.Roslyn4.0.Unit.Tests.csproj b/test/Generators/Microsoft.Gen.AutoClient/Unit/Roslyn4.0/Microsoft.Gen.AutoClient.Roslyn4.0.Unit.Tests.csproj new file mode 100644 index 0000000000..18ce9dd9ba --- /dev/null +++ b/test/Generators/Microsoft.Gen.AutoClient/Unit/Roslyn4.0/Microsoft.Gen.AutoClient.Roslyn4.0.Unit.Tests.csproj @@ -0,0 +1,6 @@ + + + 4.0 + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + diff --git a/test/Generators/Microsoft.Gen.ComplianceReports/GoldenReports/Basic.json b/test/Generators/Microsoft.Gen.ComplianceReports/GoldenReports/Basic.json new file mode 100644 index 0000000000..8d95df3fa2 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ComplianceReports/GoldenReports/Basic.json @@ -0,0 +1,119 @@ + +{ + "Name": "test.dll", + "Types": [ + { + "Name": "Test.Basic", + "Members": [ + { + "Name": "F0", + "Type": "int", + "File": "src-0.cs", + "Line": "18", + "Classifications": [ + { + "Name": "C1" + }, + { + "Name": "C2", + "Notes": "Note 1" + } + ] + }, + { + "Name": "F1", + "Type": "int", + "File": "src-0.cs", + "Line": "21", + "Classifications": [ + { + "Name": "C1" + }, + { + "Name": "C2" + } + ] + }, + { + "Name": "P0", + "Type": "int", + "File": "src-0.cs", + "Line": "11", + "Classifications": [ + { + "Name": "C1" + }, + { + "Name": "C3", + "Notes": "Note 2" + }, + { + "Name": "C4" + } + ] + }, + { + "Name": "P1", + "Type": "int", + "File": "src-0.cs", + "Line": "27", + "Classifications": [ + { + "Name": "C1" + }, + { + "Name": "C3" + } + ] + } + ], + "Logging Methods": [ + { + "Name": "LogHello", + "Parameters": [ + { + "Name": "user", + "Type": "string", + "File": "src-0.cs", + "Line": "30", + "Classifications": [ + { + "Name": "C2", + "Notes": "Note 3" + } + ] + }, + { + "Name": "port", + "Type": "int", + "File": "src-0.cs", + "Line": "30" + } + ] + }, + { + "Name": "LogWorld", + "Parameters": [ + { + "Name": "user", + "Type": "string", + "File": "src-0.cs", + "Line": "33", + "Classifications": [ + { + "Name": "C2" + } + ] + }, + { + "Name": "port", + "Type": "int", + "File": "src-0.cs", + "Line": "33" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/test/Generators/Microsoft.Gen.ComplianceReports/GoldenReports/Inheritance.json b/test/Generators/Microsoft.Gen.ComplianceReports/GoldenReports/Inheritance.json new file mode 100644 index 0000000000..3aaf8fe981 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ComplianceReports/GoldenReports/Inheritance.json @@ -0,0 +1,63 @@ + +{ + "Name": "test.dll", + "Types": [ + { + "Name": "Test.Base", + "Members": [ + { + "Name": "P0", + "Type": "int", + "File": "src-0.cs", + "Line": "11", + "Classifications": [ + { + "Name": "C1" + } + ] + }, + { + "Name": "P1", + "Type": "int", + "File": "src-0.cs", + "Line": "14", + "Classifications": [ + { + "Name": "C2" + } + ] + } + ] + }, + { + "Name": "Test.Inherited", + "Members": [ + { + "Name": "P0", + "Type": "int", + "File": "src-0.cs", + "Line": "11", + "Classifications": [ + { + "Name": "C1" + } + ] + }, + { + "Name": "P1", + "Type": "int", + "File": "src-0.cs", + "Line": "14", + "Classifications": [ + { + "Name": "C2" + }, + { + "Name": "C3" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/test/Generators/Microsoft.Gen.ComplianceReports/GoldenReports/LogMethod.json b/test/Generators/Microsoft.Gen.ComplianceReports/GoldenReports/LogMethod.json new file mode 100644 index 0000000000..0f492a1a86 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ComplianceReports/GoldenReports/LogMethod.json @@ -0,0 +1,34 @@ + +{ + "Name": "test.dll", + "Types": [ + { + "Name": "Test.LogMethod", + "Logging Methods": [ + { + "Name": "LogHello", + "Parameters": [ + { + "Name": "user", + "Type": "string", + "File": "src-0.cs", + "Line": "11", + "Classifications": [ + { + "Name": "C2", + "Notes": "Note 3" + } + ] + }, + { + "Name": "port", + "Type": "int", + "File": "src-0.cs", + "Line": "11" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/test/Generators/Microsoft.Gen.ComplianceReports/TestClasses/Basic.cs b/test/Generators/Microsoft.Gen.ComplianceReports/TestClasses/Basic.cs new file mode 100644 index 0000000000..b64b0458a4 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ComplianceReports/TestClasses/Basic.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Telemetry.Logging; + +namespace Test; + +interface IFoo +{ + [C4] + public int P0 { get; } +} + +[C1] +public class Basic : IFoo +{ + [C2(Notes = "Note 1")] + public int F0; + + [C2(Notes = null!)] + public int F1; + + [C3(Notes = "Note 2")] + public int P0 { get; } + + [C3] + public int P1 { get; } + + [LogMethod("Hello {user}")] + public void LogHello([C2(Notes = "Note 3")] string user, int port); + + [LogMethod("World {user}")] + public void LogWorld([C2] string user, int port); +} diff --git a/test/Generators/Microsoft.Gen.ComplianceReports/TestClasses/Inheritance.cs b/test/Generators/Microsoft.Gen.ComplianceReports/TestClasses/Inheritance.cs new file mode 100644 index 0000000000..0ad51e2d60 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ComplianceReports/TestClasses/Inheritance.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Telemetry.Logging; + +namespace Test; + +public class Base +{ + [C1] + public int P0 { get; } + + [C2] + public virtual int P1 { get; } +} + +public class Inherited : Base +{ + [C3] + public override int P1 { get; } +} diff --git a/test/Generators/Microsoft.Gen.ComplianceReports/TestClasses/LogMethod.cs b/test/Generators/Microsoft.Gen.ComplianceReports/TestClasses/LogMethod.cs new file mode 100644 index 0000000000..4604d457e3 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ComplianceReports/TestClasses/LogMethod.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Telemetry.Logging; + +namespace Test; + +public class LogMethod +{ + [LogMethod("Hello {user}")] + public void LogHello([C2(Notes = "Note 3")] string user, int port); +} diff --git a/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Common/GeneratorTests.cs b/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Common/GeneratorTests.cs new file mode 100644 index 0000000000..443c0a8780 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Common/GeneratorTests.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; +using Microsoft.Gen.Shared; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Gen.ComplianceReports.Tests; + +public class GeneratorTests +{ + private const string TestTaxonomy = @" + using Microsoft.Extensions.Compliance.Classification; + + public sealed class C1Attribute : DataClassificationAttribute + { + public C1Attribute(new DataClassification(""TAX"", 1)) { } + } + + public sealed class C2Attribute : DataClassificationAttribute + { + public C2Attribute(new DataClassification(""TAX"", 2)) { } + } + + public sealed class C3Attribute : DataClassificationAttribute + { + public C3Attribute(new DataClassification(""TAX"", 4)) { } + } + + public sealed class C4Attribute : DataClassificationAttribute + { + public C4Attribute(new DataClassification(""TAX"", 8)) { } + } + "; + + private readonly ITestOutputHelper _output; + + public GeneratorTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public async Task TestAll() + { + foreach (var inputFile in Directory.GetFiles("TestClasses")) + { + var stem = Path.GetFileNameWithoutExtension(inputFile); + var goldenReportFile = $"GoldenReports/{stem}.json"; + + if (File.Exists(goldenReportFile)) + { + var tmp = Path.GetTempFileName(); + var d = await RunGenerator(File.ReadAllText(inputFile), tmp); + Assert.Empty(d); + + var golden = File.ReadAllText(goldenReportFile); + var generated = File.ReadAllText(tmp); + + if (golden != generated) + { + _output.WriteLine($"MISMATCH: goldenReportFile {goldenReportFile}, tmp {tmp}"); + _output.WriteLine("----"); + _output.WriteLine("golden:"); + _output.WriteLine(golden); + _output.WriteLine("----"); + _output.WriteLine("generated:"); + _output.WriteLine(generated); + _output.WriteLine("----"); + } + + Assert.Equal(golden, generated); + File.Delete(tmp); + } + else + { + // generate the golden file if it doesn't already exist + _output.WriteLine($"Generating golden report: {goldenReportFile}"); + _ = await RunGenerator(File.ReadAllText(inputFile), goldenReportFile); + } + } + + static async Task> RunGenerator(string code, string outputFile) + { + var (d, _) = await RoslynTestUtils.RunGenerator( + new Generator(outputFile), + new[] + { + Assembly.GetAssembly(typeof(ILogger))!, + Assembly.GetAssembly(typeof(LogMethodAttribute))!, + Assembly.GetAssembly(typeof(Microsoft.Extensions.Compliance.Classification.DataClassification))!, + }, + new[] + { + code, + TestTaxonomy, + }, + new OptionsProvider()).ConfigureAwait(false); + + return d; + } + } + + [Fact] + public async Task MissingDataClassificationSymbol() + { + const string Source = "class Nothing {}"; + + var (d, _) = await RoslynTestUtils.RunGenerator( + new Generator("Foo"), + null, + new[] + { + Source, + }, + new OptionsProvider()).ConfigureAwait(false); + + Assert.Equal(0, d.Count); + } + + private sealed class Options : AnalyzerConfigOptions + { + public override bool TryGetValue(string key, out string value) + { + if (key == "build_property.GenerateComplianceReport") + { + value = bool.TrueString; + return true; + } + + value = null!; + return false; + } + } + + private sealed class OptionsProvider : AnalyzerConfigOptionsProvider + { + public override AnalyzerConfigOptions GlobalOptions => new Options(); + public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => throw new System.NotSupportedException(); + public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => throw new System.NotSupportedException(); + } +} diff --git a/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Directory.Build.props b/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Directory.Build.props new file mode 100644 index 0000000000..d04ec36965 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Directory.Build.props @@ -0,0 +1,39 @@ + + + + + Microsoft.Gen.ComplianceReports.Test + Unit tests for Gen.ComplianceReports. + + + + true + true + + + + + + + + TestClasses\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + + + + GoldenReports\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + + + + + + + + + + + + + + diff --git a/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Roslyn3.8/Microsoft.Gen.ComplianceReports.Roslyn3.8.Unit.Tests.csproj b/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Roslyn3.8/Microsoft.Gen.ComplianceReports.Roslyn3.8.Unit.Tests.csproj new file mode 100644 index 0000000000..eac2eac217 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Roslyn3.8/Microsoft.Gen.ComplianceReports.Roslyn3.8.Unit.Tests.csproj @@ -0,0 +1,5 @@ + + + 3.8 + + diff --git a/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Roslyn4.0/Microsoft.Gen.ComplianceReports.Roslyn4.0.Unit.Tests.csproj b/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Roslyn4.0/Microsoft.Gen.ComplianceReports.Roslyn4.0.Unit.Tests.csproj new file mode 100644 index 0000000000..18ce9dd9ba --- /dev/null +++ b/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Roslyn4.0/Microsoft.Gen.ComplianceReports.Roslyn4.0.Unit.Tests.csproj @@ -0,0 +1,6 @@ + + + 4.0 + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/Generated/Common/ContextualOptionsTests.cs b/test/Generators/Microsoft.Gen.ContextualOptions/Generated/Common/ContextualOptionsTests.cs new file mode 100644 index 0000000000..8349fc2219 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/Generated/Common/ContextualOptionsTests.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options.Contextual; +using TestClasses; +using Xunit; + +namespace Microsoft.Gen.ContextualOptions.Test; + +[SuppressMessage("Style", "IDE0004:Remove Unnecessary Cast", Justification = "The tests fail without the cast.")] +public class ContextualOptionsTests +{ + private class Receiver : IOptionsContextReceiver + { + public List<(string key, object? value)> Received { get; } = new(); + + public void Receive(string key, T value) => Received.Add((key, value)); + } + + [Fact] + public void Class() + { + Receiver receiver = new(); + ((IOptionsContext)new Class1()).PopulateReceiver(receiver); + Assert.Single(receiver.Received, ("Foo", (object?)"FooValue")); + } + + [Fact] + public void TwoPartClass() + { + Receiver receiver = new(); + ((IOptionsContext)new Class2()).PopulateReceiver(receiver); + Assert.Equal(2, receiver.Received.Count); + Assert.Contains(("Foo", (object?)"FooValue"), receiver.Received); + Assert.Contains(("Bar", (object?)"BarValue"), receiver.Received); + } + + [Fact] + public void Record() + { + Receiver receiver = new(); + ((IOptionsContext)new Record1("PropertyValue")).PopulateReceiver(receiver); + Assert.Single(receiver.Received, ("Foo", (object?)"PropertyValue")); + } + + [Fact] + public void Struct() + { + Receiver receiver = new(); + ((IOptionsContext)default(Struct1)).PopulateReceiver(receiver); + Assert.Single(receiver.Received, ("Foo", (object?)"FooValue")); + } + + [Fact] + public void NonPublicType() + { + Assert.False(typeof(NonPublicStruct).IsPublic); + Assert.IsAssignableFrom(default(NonPublicStruct)); + } +} diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/Generated/Directory.Build.props b/test/Generators/Microsoft.Gen.ContextualOptions/Generated/Directory.Build.props new file mode 100644 index 0000000000..9a0178448b --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/Generated/Directory.Build.props @@ -0,0 +1,29 @@ + + + + + Microsoft.Gen.ContextualOptions.Generated.Test + Tests for code generated by Gen.ContextualOptions. + + + + $(NetCoreTargetFrameworks) + $(NetCoreTargetFrameworks)$(ConditionalNet462) + true + true + true + $(NoWarn);IDE0161;S1144 + Microsoft.Gen.ContextualOptions\Microsoft.Gen.ContextualOptions + + + + + + + + + + + + + diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/Generated/Roslyn3.8/Microsoft.Gen.ContextualOptions.Roslyn3.8.Generated.Tests.csproj b/test/Generators/Microsoft.Gen.ContextualOptions/Generated/Roslyn3.8/Microsoft.Gen.ContextualOptions.Roslyn3.8.Generated.Tests.csproj new file mode 100644 index 0000000000..eac2eac217 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/Generated/Roslyn3.8/Microsoft.Gen.ContextualOptions.Roslyn3.8.Generated.Tests.csproj @@ -0,0 +1,5 @@ + + + 3.8 + + diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/Generated/Roslyn4.0/Microsoft.Gen.ContextualOptions.Roslyn4.0.Generated.Tests.csproj b/test/Generators/Microsoft.Gen.ContextualOptions/Generated/Roslyn4.0/Microsoft.Gen.ContextualOptions.Roslyn4.0.Generated.Tests.csproj new file mode 100644 index 0000000000..32d431d7df --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/Generated/Roslyn4.0/Microsoft.Gen.ContextualOptions.Roslyn4.0.Generated.Tests.csproj @@ -0,0 +1,6 @@ + + + 4.0 + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + \ No newline at end of file diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Class1.cs b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Class1.cs new file mode 100644 index 0000000000..470f141519 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Class1.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options.Contextual; + +namespace TestClasses +{ + [OptionsContext] + public partial class Class1 + { + public string Foo { get; set; } = "FooValue"; + } +} diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Class2A.cs b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Class2A.cs new file mode 100644 index 0000000000..246fa656a3 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Class2A.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace TestClasses +{ + public partial class Class2 + { + [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Needed for testing.")] + public string Bar => "BarValue"; + } +} diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Class2B.cs b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Class2B.cs new file mode 100644 index 0000000000..0e6fe282df --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Class2B.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options.Contextual; + +namespace TestClasses +{ + [OptionsContext] + public partial class Class2 + { + public string Foo { get; } = "FooValue"; + } +} diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/ClassWithNoAttribute.cs b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/ClassWithNoAttribute.cs new file mode 100644 index 0000000000..abdee9533c --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/ClassWithNoAttribute.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace TestClasses +{ + [OptionsContext] + [SuppressMessage("Minor Code Smell", "S2333:Redundant modifiers should not be used", Justification = "Needed for test code.")] + public partial class ClassWithNoAttribute + { + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)] + public sealed class OptionsContextAttribute : Attribute + { + } + + public string Foo { get; } = "FooValue"; + } +} diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/ClassWithUnusableProperties.txt b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/ClassWithUnusableProperties.txt new file mode 100644 index 0000000000..ade9b3e996 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/ClassWithUnusableProperties.txt @@ -0,0 +1,22 @@ +using System; +using System.Collections; +using Microsoft.Extensions.Options.Contextual; + +namespace TestClasses +{ + [OptionsContext] + public partial class ClassWithUnusableProperties : IEnumerator + { + private static string PrivateProperty { get; set; } + public static string StaticProperty { get; set; } + public string SetOnlyProperty { set => throw new NotImplementedException(); } + public ReadOnlySpan RefOnlyProperty => throw new NotImplementedException(); + public string PrivateGetterProperty { private get; set; } + public string this[string x] => throw new NotImplementedException(); + public unsafe int* PointerProperty { get; set; } + public unsafe delegate* FunctionPointerProperty { get; set; } + object IEnumerator.Current => throw new NotImplementedException(); + bool IEnumerator.MoveNext() => throw new NotImplementedException(); + void IEnumerator.Reset() => throw new NotImplementedException(); + } +} diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/NamespacelessRecord.cs b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/NamespacelessRecord.cs new file mode 100644 index 0000000000..981a89256a --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/NamespacelessRecord.cs @@ -0,0 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options.Contextual; + +[OptionsContext] +public partial record NamespacelessRecord(string Foo); diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/NonPartialClass.txt b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/NonPartialClass.txt new file mode 100644 index 0000000000..2c899f48e8 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/NonPartialClass.txt @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. + +using Microsoft.Extensions.Options.Contextual; + +namespace TestClasses +{ + [OptionsContext] + public class NonPartialClass + { + public string Foo { get; } = "FooValue"; + } +} diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/NonPublicStruct.cs b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/NonPublicStruct.cs new file mode 100644 index 0000000000..f9be2b4dbe --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/NonPublicStruct.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options.Contextual; + +namespace TestClasses +{ + [OptionsContext] + internal partial struct NonPublicStruct + { +#pragma warning disable CA1822 // Mark members as static + public string Foo => "FooValue"; + } +} diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Record1.cs b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Record1.cs new file mode 100644 index 0000000000..d7ce94df4b --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Record1.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options.Contextual; + +namespace TestClasses +{ + [OptionsContext] + public partial record Record1(string Foo); +} diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/RefStruct.txt b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/RefStruct.txt new file mode 100644 index 0000000000..bfbdf3b124 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/RefStruct.txt @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. + +using Microsoft.Extensions.Options.Contextual; + +namespace TestClasses +{ + [OptionsContext] + public ref partial struct RefStruct + { + public string Foo => "FooValue"; + } +} diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/StaticClass.txt b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/StaticClass.txt new file mode 100644 index 0000000000..46b840c4a9 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/StaticClass.txt @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. + +using Microsoft.Extensions.Options.Contextual; + +namespace TestClasses +{ + [OptionsContext] + public static partial class StaticClass + { + public static string Foo { get; } = "FooValue"; + } +} diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Struct1.cs b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Struct1.cs new file mode 100644 index 0000000000..ad5483666b --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/TestClasses/Struct1.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options.Contextual; + +namespace TestClasses +{ + [OptionsContext] + public partial struct Struct1 + { +#pragma warning disable CA1822 // Mark members as static + public string Foo => "FooValue"; + } +} diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/ContextualOptionsTests.cs b/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/ContextualOptionsTests.cs new file mode 100644 index 0000000000..ddc08ec404 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/ContextualOptionsTests.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options.Contextual; +using TestClasses; +using Xunit; + +namespace Microsoft.Gen.ContextualOptions.Test; + +[SuppressMessage("Style", "IDE0004:Remove Unnecessary Cast", Justification = "The tests fail without the cast.")] +public class ContextualOptionsTests +{ + private class Receiver : IOptionsContextReceiver + { + public List<(string key, object? value)> Received { get; } = new(); + + public void Receive(string key, T value) => Received.Add((key, value)); + } + + [Fact] + public void Class() + { + Receiver receiver = new(); + ((IOptionsContext)new Class1()).PopulateReceiver(receiver); + Assert.Single(receiver.Received, ("Foo", (object?)"FooValue")); + } + + [Fact] + public void TwoPartClass() + { + Receiver receiver = new(); + ((IOptionsContext)new Class2()).PopulateReceiver(receiver); + Assert.Equal(2, receiver.Received.Count); + Assert.Contains(("Foo", (object?)"FooValue"), receiver.Received); + Assert.Contains(("Bar", (object?)"BarValue"), receiver.Received); + } + + [Fact] + public void Record() + { + Receiver receiver = new(); + ((IOptionsContext)new Record1("PropertyValue")).PopulateReceiver(receiver); + Assert.Single(receiver.Received, ("Foo", (object?)"PropertyValue")); + } + + [Fact] + public void Struct() + { + Receiver receiver = new(); + ((IOptionsContext)default(Struct1)).PopulateReceiver(receiver); + Assert.Single(receiver.Received, ("Foo", (object?)"FooValue")); + } +} diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/DiagDescriptorsTests.cs b/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/DiagDescriptorsTests.cs new file mode 100644 index 0000000000..fe3e8d1b2f --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/DiagDescriptorsTests.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Xunit; + +namespace Microsoft.Gen.ContextualOptions.Test; + +public class DiagDescriptorsTests +{ + public static IEnumerable DiagDescriptorsData() + { + var type = typeof(DiagDescriptors); + foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.GetProperty)) + { + var value = property.GetValue(type, null); + yield return new[] { value }; + } + } + + [Theory] + [MemberData(nameof(DiagDescriptorsData))] + public void ShouldContainValidLinkAndBeEnabled(DiagnosticDescriptor descriptor) + { + Assert.True(descriptor.IsEnabledByDefault, descriptor.Id + " should be enabled by default"); + Assert.EndsWith("/" + descriptor.Id, descriptor.HelpLinkUri, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/EmitterTests.cs b/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/EmitterTests.cs new file mode 100644 index 0000000000..eec0b22fd3 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/EmitterTests.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Extensions.Options.Contextual; +using Microsoft.Gen.ContextualOptions.Model; +using Microsoft.Gen.Shared; +using Moq; +using Xunit; + +namespace Microsoft.Gen.ContextualOptions.Test; + +public class EmitterTests +{ + [Fact] + public void EmmitterDoesNotMakeStructMethodReadonly() + { + var delarations = SyntaxFactory + .ParseCompilationUnit(File.ReadAllText("TestClasses/Struct1.cs")) + .DescendantNodes() + .OfType() + .ToImmutableArray(); + var type = new OptionsContextType( + Mock.Of(sym => sym.Name == "Struct1" && sym.ContainingNamespace.ToString() == "Microsoft.GenContextualOptions.TestClasses"), + delarations, + ImmutableArray.Create("Foo")); + var generatedStruct = new Emitter().Emit(new[] { type }); + var syntaxTree = SyntaxFactory.ParseSyntaxTree(generatedStruct); + Assert.DoesNotContain( + syntaxTree.GetRoot().DescendantNodes().OfType().Single().Members.Single().Modifiers, + mod => mod.IsKind(SyntaxKind.ReadOnlyKeyword)); + } + + [Fact] + public void EmmitterEmitsReceiveCallForAllProperties() + { + var delarations = SyntaxFactory + .ParseCompilationUnit(File.ReadAllText("TestClasses/Class2A.cs")) + .DescendantNodes() + .OfType() + .Concat(SyntaxFactory + .ParseCompilationUnit(File.ReadAllText("TestClasses/Class2B.cs")) + .DescendantNodes() + .OfType()) + .ToImmutableArray(); + + var type = new OptionsContextType( + Mock.Of(sym => sym.Name == "Class2" && sym.ContainingNamespace.ToString() == "Microsoft.GenContextualOptions.TestClasses"), + delarations, + ImmutableArray.Create("Foo", "Bar")); + var generatedStruct = new Emitter().Emit(new[] { type }); + var syntaxTree = SyntaxFactory.ParseSyntaxTree(generatedStruct); + var statements = syntaxTree! + .GetRoot() + .DescendantNodes() + .OfType() + .Single() + .Body! + .Statements + .Select(statement => statement.ToString()); + + Assert.Contains("receiver.Receive(nameof(Foo), Foo);", statements); + Assert.Contains("receiver.Receive(nameof(Bar), Bar);", statements); + } + + [Fact] + public void EmmitterEmitsValidRecord() + { + var delarations = SyntaxFactory + .ParseCompilationUnit(File.ReadAllText("TestClasses/Record1.cs")) + .DescendantNodes() + .OfType() + .ToImmutableArray(); + + var type = new OptionsContextType( + Mock.Of(sym => sym.Name == "Record1" && sym.ContainingNamespace.ToString() == "Microsoft.GenContextualOptions.TestClasses"), + delarations, + ImmutableArray.Create("Foo")); + + var generatedStruct = new Emitter().Emit(new[] { type }); + var syntaxTree = SyntaxFactory.ParseSyntaxTree(generatedStruct); + var statements = syntaxTree! + .GetRoot() + .DescendantNodes() + .OfType() + .Single() + .Body! + .Statements + .Select(statement => statement.ToString()); + + Assert.Single(statements, "receiver.Receive(nameof(Foo), Foo);"); + } + + [Fact] + public void EmmitterEmitsValidNamespacelessType() + { + var delarations = SyntaxFactory + .ParseCompilationUnit(File.ReadAllText("TestClasses/NamespacelessRecord.cs")) + .DescendantNodes() + .OfType() + .ToImmutableArray(); + + var type = new OptionsContextType( + Mock.Of(sym => sym.Name == "NamespacelessRecord" && sym.ContainingNamespace.IsGlobalNamespace), + delarations, + ImmutableArray.Create("Foo")); + + var generatedStruct = new Emitter().Emit(new[] { type }); + var syntaxTree = SyntaxFactory.ParseSyntaxTree(generatedStruct); + var hasNamespace = syntaxTree! + .GetRoot() + .DescendantNodes() + .OfType() + .Any(); + + Assert.False(hasNamespace); + } + + [Fact] + public async Task TestEmitter() + { + var sources = new List(); + foreach (var file in Directory.GetFiles("TestClasses", "*.cs")) + { + sources.Add(File.ReadAllText(file)); + } + + var (d, r) = await RoslynTestUtils.RunGenerator( + new Generator(), + new[] + { + typeof(OptionsContextAttribute).Assembly, + typeof(ReadOnlySpan<>).Assembly + }, + sources) + .ConfigureAwait(false); + + Assert.Empty(d); + _ = Assert.Single(r); + + var golden = File.ReadAllText($"GoldenFiles/Microsoft.Gen.ContextualOptions/Microsoft.Gen.ContextualOptions.Generator/ContextualOptions.g.cs"); + var result = r[0].SourceText.ToString(); + Assert.Equal(golden, result); + } +} diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/ParserTests.cs b/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/ParserTests.cs new file mode 100644 index 0000000000..73923cde30 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/ParserTests.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options.Contextual; +using Microsoft.Gen.ContextualOptions.Model; +using Microsoft.Gen.Shared; +using Xunit; + +namespace Microsoft.Gen.ContextualOptions.Test; + +public class ParserTests +{ + [Fact] + public async Task ShouldWarnWithUnusableProperties() + { + var sources = new[] + { + File.ReadAllText("TestClasses/ClassWithUnusableProperties.txt"), + }; + + var result = await GetParserResult(sources); + + Assert.Single(result); + Assert.Empty(result!.Single().OptionsContextProperties); + Assert.Equal(DiagDescriptors.ContextDoesNotHaveValidProperties, result!.Single().Diagnostics.Single().Descriptor); + Assert.True(result.Single().ShouldEmit); + Assert.Equal("TestClasses.ClassWithUnusableProperties", result.Single().HintName); + } + + [Fact] + public async Task ShouldReturnValidProperties() + { + var sources = new[] + { + File.ReadAllText("TestClasses/NamespacelessRecord.cs"), + }; + + var result = await GetParserResult(sources); + + Assert.Single(result); + Assert.Empty(result!.Single().Diagnostics); + Assert.Equal("Foo", result!.Single().OptionsContextProperties.Single()); + Assert.True(result.Single().ShouldEmit); + Assert.Equal(".NamespacelessRecord", result.Single().HintName); + } + + [Fact] + public async Task ShouldErrorWhenAttributeAppliedToNonPartialClass() + { + var sources = new[] + { + File.ReadAllText("TestClasses/NonPartialClass.txt"), + }; + + var result = await GetParserResult(sources); + + Assert.Single(result); + Assert.Equal(DiagDescriptors.ContextMustBePartial, result!.Single().Diagnostics.Single().Descriptor); + Assert.False(result.Single().ShouldEmit); + } + + [Fact] + public async Task ShouldErrorWhenAttributeAppliedToStaticClass() + { + var sources = new[] + { + File.ReadAllText("TestClasses/StaticClass.txt"), + }; + + var result = await GetParserResult(sources); + + Assert.Single(result); + Assert.Contains(result!.Single().Diagnostics, diag => diag.Descriptor == DiagDescriptors.ContextCannotBeStatic); + Assert.False(result.Single().ShouldEmit); + } + + [Fact] + public async Task ShouldErrorWhenAttributeAppliedToRefStruct() + { + var sources = new[] + { + File.ReadAllText("TestClasses/RefStruct.txt"), + }; + + var result = await GetParserResult(sources); + + Assert.Single(result); + Assert.Contains(result!.Single().Diagnostics, diag => diag.Descriptor == DiagDescriptors.ContextCannotBeRefLike); + Assert.False(result.Single().ShouldEmit); + } + + private static async Task> GetParserResult(string[] sources) => + (await RoslynTestUtils.RunParser( + new ContextReceiver(CancellationToken.None), + (receiver, compilation) => + { + Assert.True(receiver.TryGetTypeDeclarations(compilation, out var typeDeclarations)); + return Parser.GetContextualOptionTypes(typeDeclarations!); + }, + new[] { typeof(OptionsContextAttribute).Assembly, typeof(ReadOnlySpan<>).Assembly }, + sources).ConfigureAwait(true))!; +} diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/SyntaxContextReceiverTests.cs b/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/SyntaxContextReceiverTests.cs new file mode 100644 index 0000000000..ef9f094caf --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/SyntaxContextReceiverTests.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Options.Contextual; +using Microsoft.Gen.Shared; +using Xunit; + +namespace Microsoft.Gen.ContextualOptions.Test; + +public class SyntaxContextReceiverTests +{ + [Fact] + public async Task ShouldCollectTypesWithTheOptionsContextAttribute() + { + var sut = new ContextReceiver(CancellationToken.None); + var sources = new[] + { + File.ReadAllText("TestClasses/Class1.cs"), + File.ReadAllText("TestClasses/Struct1.cs"), + File.ReadAllText("TestClasses/Record1.cs"), + }; + + var comp = await RoslynTestUtils.RunSyntaxContextReceiver(sut, new[] { typeof(OptionsContextAttribute).Assembly }, sources).ConfigureAwait(true); + + Assert.True(sut.TryGetTypeDeclarations(comp, out var typeDeclarations)); + + Assert.Equal(3, typeDeclarations!.Count); + foreach (var declaration in typeDeclarations) + { + Assert.Equal(declaration.Key.Name, declaration.Value.Single().Identifier.Text); + Assert.Equal("TestClasses", declaration.Key.ContainingNamespace.ToString()); + } + } + + [Fact] + public async Task ShouldDoNothingWithoutTheOptionsContextAttributeReferenced() + { + var sut = new ContextReceiver(CancellationToken.None); + var sources = new[] + { + File.ReadAllText("TestClasses/Class1.cs"), + File.ReadAllText("TestClasses/Struct1.cs"), + File.ReadAllText("TestClasses/Record1.cs"), + }; + + var comp = await RoslynTestUtils.RunSyntaxContextReceiver(sut, Enumerable.Empty(), sources).ConfigureAwait(true); + + Assert.False(sut.TryGetTypeDeclarations(comp, out _)); + } + + [Fact] + public async Task ShouldCollectMultiFileTypesWithTheOptionsContextAttribute() + { + var sut = new ContextReceiver(CancellationToken.None); + var sources = new[] + { + File.ReadAllText("TestClasses/Class2A.cs"), + File.ReadAllText("TestClasses/Class2B.cs"), + }; + + var comp = await RoslynTestUtils.RunSyntaxContextReceiver(sut, new[] { typeof(OptionsContextAttribute).Assembly }, sources).ConfigureAwait(true); + Assert.True(sut.TryGetTypeDeclarations(comp, out var typeDeclarations)); + + Assert.Single(typeDeclarations!); + + var declaration = typeDeclarations!.Single(); + Assert.Equal("TestClasses", declaration.Key.ContainingNamespace.ToString()); + Assert.Equal(2, declaration.Value.Count); + Assert.All(declaration.Value.Select(dec => dec.Identifier.Text), className => Assert.Equal("Class2", className)); + Assert.NotEqual(declaration.Value[0], declaration.Value[1]); + } + + [Fact] + public async Task ShouldIgnoreTypesWithoutTheOptionsContextAttribute() + { + var sut = new ContextReceiver(CancellationToken.None); + var sources = new[] { File.ReadAllText("TestClasses/ClassWithNoAttribute.cs"), }; + + var comp = await RoslynTestUtils.RunSyntaxContextReceiver(sut, new[] { typeof(OptionsContextAttribute).Assembly }, sources).ConfigureAwait(true); + + Assert.True(sut.TryGetTypeDeclarations(comp, out var typeDeclarations)); + Assert.Empty(typeDeclarations!); + } +} diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Directory.Build.props b/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Directory.Build.props new file mode 100644 index 0000000000..31206c69dc --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Directory.Build.props @@ -0,0 +1,29 @@ + + + + + Microsoft.Gen.ContextualOptions.Test + Unit tests for Gen.ContextualOptions. + + + + true + true + + + + + + + + + + + + + + + + + + diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Roslyn3.8/Microsoft.Gen.ContextualOptions.Roslyn3.8.Unit.Tests.csproj b/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Roslyn3.8/Microsoft.Gen.ContextualOptions.Roslyn3.8.Unit.Tests.csproj new file mode 100644 index 0000000000..eac2eac217 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Roslyn3.8/Microsoft.Gen.ContextualOptions.Roslyn3.8.Unit.Tests.csproj @@ -0,0 +1,5 @@ + + + 3.8 + + diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Roslyn4.0/Microsoft.Gen.ContextualOptions.Roslyn4.0.Unit.Tests.csproj b/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Roslyn4.0/Microsoft.Gen.ContextualOptions.Roslyn4.0.Unit.Tests.csproj new file mode 100644 index 0000000000..18ce9dd9ba --- /dev/null +++ b/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Roslyn4.0/Microsoft.Gen.ContextualOptions.Roslyn4.0.Unit.Tests.csproj @@ -0,0 +1,6 @@ + + + 4.0 + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + diff --git a/test/Generators/Microsoft.Gen.EnumStrings/Generated/Common/EnumStringsTests.cs b/test/Generators/Microsoft.Gen.EnumStrings/Generated/Common/EnumStringsTests.cs new file mode 100644 index 0000000000..3155c53e78 --- /dev/null +++ b/test/Generators/Microsoft.Gen.EnumStrings/Generated/Common/EnumStringsTests.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using TestClasses; +using Xunit; + +namespace Microsoft.Gen.EnumStrings.Test; + +public static class EnumStringsTests +{ + [Fact] + public static void TestGeneral() + { + Test(i => (Size0)i, v => v.ToInvariantString()); + Test(i => (Size1)i, v => v.ToInvariantString()); + Test(i => (Size2)i, v => v.ToInvariantString()); + Test(i => (Size3)i, v => v.ToInvariantString()); + Test(i => (Size4)i, v => v.ToInvariantString()); + Test(i => (Size5)i, v => v.ToInvariantString()); + Test(i => (Size6)i, v => v.ToInvariantString()); + Test(i => (Size7)i, v => v.ToInvariantString()); + + Test(i => (Flags0)i, v => v.ToInvariantString()); + Test(i => (Flags1)i, v => v.ToInvariantString()); + Test(i => (Flags2)i, v => v.ToInvariantString()); + Test(i => (Flags3)i, v => v.ToInvariantString()); + Test(i => (Flags4)i, v => v.ToInvariantString()); + Test(i => (Flags5)i, v => v.ToInvariantString()); + Test(i => (Flags6)i, v => v.ToInvariantString()); + Test(i => (Flags7)i, v => v.ToInvariantString()); + Test(i => (Flags8)i, v => v.ToInvariantString()); + + Test(i => (SByteEnum1)i, v => v.ToInvariantString()); + Test(i => (SByteEnum2)i, v => v.ToInvariantString()); + Test(i => (SByteEnum3)i, v => v.ToInvariantString()); + + Test(i => (ByteEnum1)i, v => v.ToInvariantString()); + Test(i => (ByteEnum2)i, v => v.ToInvariantString()); + Test(i => (ByteEnum3)i, v => v.ToInvariantString()); + + Test(i => (ShortEnum1)i, v => v.ToInvariantString()); + Test(i => (ShortEnum2)i, v => v.ToInvariantString()); + Test(i => (ShortEnum3)i, v => v.ToInvariantString()); + + Test(i => (UShortEnum1)i, v => v.ToInvariantString()); + Test(i => (UShortEnum2)i, v => v.ToInvariantString()); + Test(i => (UShortEnum3)i, v => v.ToInvariantString()); + + Test(i => (IntEnum1)i, v => v.ToInvariantString()); + Test(i => (IntEnum2)i, v => v.ToInvariantString()); + Test(i => (IntEnum3)i, v => v.ToInvariantString()); + + Test(i => (UIntEnum1)i, v => v.ToInvariantString()); + Test(i => (UIntEnum2)i, v => v.ToInvariantString()); + Test(i => (UIntEnum3)i, v => v.ToInvariantString()); + + Test(i => (LongEnum1)i, v => v.ToInvariantString()); + Test(i => (LongEnum2)i, v => v.ToInvariantString()); + Test(i => (LongEnum3)i, v => v.ToInvariantString()); + + Test(i => (ULongEnum1)i, v => v.ToInvariantString()); + Test(i => (ULongEnum2)i, v => v.ToInvariantString()); + Test(i => (ULongEnum3)i, v => v.ToInvariantString()); + + Test(i => (Options0)i, v => NamespaceX.ClassY.MethodZ(v)); + Test(i => (Options1)i, v => NamespaceA.ClassB.MethodC(v)); + + Test(i => (Overlapping1)i, v => v.ToInvariantString()); + Test(i => (Overlapping2)i, v => v.ToInvariantString()); + + Test(i => (TestClasses.Nested.Fruit)i, v => v.ToInvariantString()); + + Test(i => (Level)i, v => v.ToInvariantString()); + Test(i => (Medal)i, v => v.ToInvariantString()); + + Test(i => (Negative0)i, v => v.ToInvariantString()); + Test(i => (Negative1)i, v => v.ToInvariantString()); + + Test(i => (NegativeLong0)i, v => v.ToInvariantString()); + Test(i => (NegativeLong1)i, v => v.ToInvariantString()); + + static void Test(Func convert, Func extension) + where T : notnull + { + for (int i = -120; i < 120; i++) + { + var v = convert(i); + Assert.Equal(v.ToString(), extension(v)); + } + } + } +} diff --git a/test/Generators/Microsoft.Gen.EnumStrings/Generated/Directory.Build.props b/test/Generators/Microsoft.Gen.EnumStrings/Generated/Directory.Build.props new file mode 100644 index 0000000000..9f23e7440c --- /dev/null +++ b/test/Generators/Microsoft.Gen.EnumStrings/Generated/Directory.Build.props @@ -0,0 +1,33 @@ + + + + + Microsoft.Gen.EnumStrings.Test + Tests for code generated by Gen.EnumStrings. + + + + $(NetCoreTargetFrameworks) + $(NetCoreTargetFrameworks)$(ConditionalNet462) + true + true + true + true + $(NoWarn);IDE0161;S1144 + + + + + + + + + + + + + + + + + diff --git a/test/Generators/Microsoft.Gen.EnumStrings/Generated/Roslyn3.8/Microsoft.Gen.EnumStrings.Roslyn3.8.Generated.Tests.csproj b/test/Generators/Microsoft.Gen.EnumStrings/Generated/Roslyn3.8/Microsoft.Gen.EnumStrings.Roslyn3.8.Generated.Tests.csproj new file mode 100644 index 0000000000..eac2eac217 --- /dev/null +++ b/test/Generators/Microsoft.Gen.EnumStrings/Generated/Roslyn3.8/Microsoft.Gen.EnumStrings.Roslyn3.8.Generated.Tests.csproj @@ -0,0 +1,5 @@ + + + 3.8 + + diff --git a/test/Generators/Microsoft.Gen.EnumStrings/Generated/Roslyn4.0/Microsoft.Gen.EnumStrings.Roslyn4.0.Generated.Tests.csproj b/test/Generators/Microsoft.Gen.EnumStrings/Generated/Roslyn4.0/Microsoft.Gen.EnumStrings.Roslyn4.0.Generated.Tests.csproj new file mode 100644 index 0000000000..0cd2e6b265 --- /dev/null +++ b/test/Generators/Microsoft.Gen.EnumStrings/Generated/Roslyn4.0/Microsoft.Gen.EnumStrings.Roslyn4.0.Generated.Tests.csproj @@ -0,0 +1,6 @@ + + + 4.0 + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + diff --git a/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/AssemblyLevel.cs b/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/AssemblyLevel.cs new file mode 100644 index 0000000000..c9c38f97d2 --- /dev/null +++ b/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/AssemblyLevel.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.EnumStrings; + +[assembly: EnumStrings(typeof(TestClasses.Level))] +[assembly: EnumStrings(typeof(TestClasses.Medal))] + +namespace TestClasses +{ + public enum Level + { + One, + Two, + Three, + } + + public enum Medal + { + Bronze, + Silver, + Gold, + } +} diff --git a/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Flags.cs b/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Flags.cs new file mode 100644 index 0000000000..9a6cf5bdd4 --- /dev/null +++ b/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Flags.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.EnumStrings; + +namespace TestClasses +{ + [Flags] + [EnumStrings] + public enum Flags0 + { + } + + [Flags] + [EnumStrings] + public enum Flags1 + { + Zero = 1, + } + + [Flags] + [EnumStrings] + public enum Flags2 + { + Zero = 1, + One = 2, + } + + [Flags] + [EnumStrings] + public enum Flags3 + { + Zero = 1, + One = 2, + Two = 4, + } + + [Flags] + [EnumStrings] + public enum Flags4 + { + Zero = 1, + One = 2, + Two = 4, + Three = 8, + } + + [Flags] + [EnumStrings] + public enum Flags5 + { + Zero = 1, + One = 2, + Two = 4, + Three = 8, + Four = 16, + } + + [Flags] + [EnumStrings] + public enum Flags6 + { + Zero = 1, + One = 2, + Two = 4, + Three = 8, + Four = 16, + Five = 32, + } + + [Flags] + [EnumStrings] + public enum Flags7 + { + Zero = 1, + Two = 4, + Three = 8, + } + + [Flags] + [EnumStrings] + public enum Flags8 + { + Zero = 1, + Two = 4, + Three = 8, + Ten = 1024, + Eleven = 2048, + } + + [Flags] + [EnumStrings] + public enum Flags9 : ulong + { + Zero = 1, + Two = 4, + Three = 8, + Ten = 1024, + Eleven = 2048, + } +} diff --git a/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Negative.cs b/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Negative.cs new file mode 100644 index 0000000000..79bec7d5a7 --- /dev/null +++ b/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Negative.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.EnumStrings; + +namespace TestClasses +{ + [EnumStrings] + public enum Negative0 + { + MinusOne = -1 + } + + [EnumStrings] + public enum Negative1 + { + MinusOne = -1, + MinusTwo = -2, + MinusThree = -3, + MinusFour = -4, + MinusFive = -5, + MinusSix = -6, + } + + [EnumStrings] + public enum NegativeLong0 : long + { + MinusOne = -1 + } + + [EnumStrings] + public enum NegativeLong1 : long + { + MinusOne = -1, + MinusTwo = -2, + MinusThree = -3, + MinusFour = -4, + MinusFive = -5, + MinusSix = -6, + } +} diff --git a/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Nested.cs b/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Nested.cs new file mode 100644 index 0000000000..5630a21971 --- /dev/null +++ b/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Nested.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.EnumStrings; + +namespace TestClasses +{ + public static class Nested + { + [EnumStrings] + public enum Fruit + { + Banana, + Apple, + Peach, + } + } +} diff --git a/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Options.cs b/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Options.cs new file mode 100644 index 0000000000..6808d9d01b --- /dev/null +++ b/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Options.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.EnumStrings; + +[assembly: EnumStrings(typeof(TestClasses.Options1), ExtensionNamespace = "NamespaceA", ExtensionClassName = "ClassB", ExtensionMethodName = "MethodC")] + +namespace TestClasses +{ + [EnumStrings(ExtensionNamespace = "NamespaceX", ExtensionClassName = "ClassY", ExtensionMethodName = "MethodZ", ExtensionClassModifiers = "public static")] + public enum Options0 + { + Option0, + } + + public enum Options1 + { + Option0, + Option1, + } +} diff --git a/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Overlapping.cs b/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Overlapping.cs new file mode 100644 index 0000000000..79b7a4e07e --- /dev/null +++ b/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Overlapping.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.EnumStrings; + +#pragma warning disable CA1069 +#pragma warning disable CA1027 + +namespace TestClasses +{ + [EnumStrings] + public enum Overlapping1 + { + Zero, + One, + Un = One, + Two, + Deux = Two, + Three, + Four, + } + + [Flags] + [EnumStrings] + public enum Overlapping2 + { + None = 0, + One = 1, + Two = 2, + Deux = 2, + Four = 4, + Eight = 8, + Twelve = 12, + Douze = 12, + Thirteen = 13, + } +} diff --git a/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Sizes.cs b/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Sizes.cs new file mode 100644 index 0000000000..045f72f94a --- /dev/null +++ b/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/Sizes.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.EnumStrings; + +namespace TestClasses +{ + [EnumStrings] + public enum Size0 + { + } + + [EnumStrings] + public enum Size1 + { + Zero, + } + + [EnumStrings] + public enum Size2 + { + Zero, + One, + } + + [EnumStrings] + public enum Size3 + { + Zero, + One, + Two, + } + + [EnumStrings] + public enum Size4 + { + Zero, + One, + Two, + Three, + } + + [EnumStrings] + public enum Size5 + { + Zero, + One, + Two, + Three, + Four, + } + + [EnumStrings] + public enum Size6 + { + One = 1, + Two = 2, + Three = 3, + Four = 4, + Five = 5, + Six = 6, + } + + [EnumStrings] + public enum Size7 + { + One = 1, + Two = 2, + Three = 3, + Four = 4, + Five = 5, + Six = 6, + + Ten = 10, + Eleven = 11, + Twelve = 12, + } +} diff --git a/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/UnderlyingTypes.cs b/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/UnderlyingTypes.cs new file mode 100644 index 0000000000..1d55f4d5f3 --- /dev/null +++ b/test/Generators/Microsoft.Gen.EnumStrings/TestClasses/UnderlyingTypes.cs @@ -0,0 +1,203 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.EnumStrings; + +#pragma warning disable S4022 +#pragma warning disable S2344 +#pragma warning disable S1939 + +namespace TestClasses +{ + [EnumStrings] + public enum SByteEnum1 : sbyte + { + One, + } + + [EnumStrings] + public enum SByteEnum2 : sbyte + { + One, + Two, + Three, + Four + } + + [EnumStrings] + public enum SByteEnum3 : sbyte + { + One, + Two, + Three, + Four = -42 + } + + [EnumStrings] + public enum ByteEnum1 : byte + { + One, + } + + [EnumStrings] + public enum ByteEnum2 : byte + { + One, + Two, + Three, + Four + } + + [EnumStrings] + public enum ByteEnum3 : byte + { + One, + Two, + Three, + Four = 42 + } + + [EnumStrings] + public enum ShortEnum1 : short + { + One, + } + + [EnumStrings] + public enum ShortEnum2 : short + { + One, + Two, + Three, + Four + } + + [EnumStrings] + public enum ShortEnum3 : short + { + One, + Two, + Three, + Four = -42 + } + + [EnumStrings] + public enum UShortEnum1 : ushort + { + One, + } + + [EnumStrings] + public enum UShortEnum2 : ushort + { + One, + Two, + Three, + Four + } + + [EnumStrings] + public enum UShortEnum3 : ushort + { + One, + Two, + Three, + Four = 42 + } + + [EnumStrings] + public enum IntEnum1 : int + { + One, + } + + [EnumStrings] + public enum IntEnum2 : int + { + One, + Two, + Three, + Four + } + + [EnumStrings] + public enum IntEnum3 : int + { + One, + Two, + Three, + Four = -42 + } + + [EnumStrings] + public enum UIntEnum1 : uint + { + One, + } + + [EnumStrings] + public enum UIntEnum2 : uint + { + One, + Two, + Three, + Four + } + + [EnumStrings] + public enum UIntEnum3 : uint + { + One, + Two, + Three, + Four = 42 + } + + [EnumStrings] + public enum LongEnum1 : long + { + One, + } + + [EnumStrings] + public enum LongEnum2 : long + { + One, + Two, + Three, + Four + } + + [EnumStrings] + public enum LongEnum3 : long + { + One, + Two, + Three, + Four = -42 + } + + [EnumStrings] + public enum ULongEnum1 : ulong + { + One, + } + + [EnumStrings] + public enum ULongEnum2 : ulong + { + One, + Two, + Three, + Four + } + + [EnumStrings] + public enum ULongEnum3 : ulong + { + One, + Two, + Three, + Four = 42 + } +} diff --git a/test/Generators/Microsoft.Gen.EnumStrings/Unit/Common/EmitterTests.cs b/test/Generators/Microsoft.Gen.EnumStrings/Unit/Common/EmitterTests.cs new file mode 100644 index 0000000000..1640f38b3b --- /dev/null +++ b/test/Generators/Microsoft.Gen.EnumStrings/Unit/Common/EmitterTests.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Frozen; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.EnumStrings; +using Microsoft.Gen.Shared; +using Xunit; + +namespace Microsoft.Gen.EnumStrings.Test; + +public class EmitterTests +{ + [Fact] + public async Task TestEmitter() + { + var sources = new List(); + foreach (var file in Directory.GetFiles("TestClasses")) + { +#if NETCOREAPP3_1_OR_GREATER + sources.Add("#define NETCOREAPP3_1_OR_GREATER\n" + File.ReadAllText(file)); +#else + sources.Add(File.ReadAllText(file)); +#endif + } + + // try it without the frozen collections + var (d, r) = await RoslynTestUtils.RunGenerator( + new Generator(), + new[] + { + Assembly.GetAssembly(typeof(EnumStringsAttribute))!, + }, + sources).ConfigureAwait(false); + + Assert.Empty(d); + _ = Assert.Single(r); + + // try it again with the frozen collections, this is what we need to compare with the golden files + (d, r) = await RoslynTestUtils.RunGenerator( + new Generator(), + new[] + { + Assembly.GetAssembly(typeof(EnumStringsAttribute))!, + Assembly.GetAssembly(typeof(FrozenDictionary))!, + }, + sources).ConfigureAwait(false); + + Assert.Empty(d); + _ = Assert.Single(r); + + var golden = File.ReadAllText($"GoldenFiles/Microsoft.Gen.EnumStrings/Microsoft.Gen.EnumStrings.Generator/EnumStrings.g.cs"); + var result = r[0].SourceText.ToString(); + Assert.Equal(golden, result); + } +} diff --git a/test/Generators/Microsoft.Gen.EnumStrings/Unit/Common/ParserTests.cs b/test/Generators/Microsoft.Gen.EnumStrings/Unit/Common/ParserTests.cs new file mode 100644 index 0000000000..3ae55bf7ea --- /dev/null +++ b/test/Generators/Microsoft.Gen.EnumStrings/Unit/Common/ParserTests.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.EnumStrings; +using Microsoft.Gen.Shared; +using Xunit; + +namespace Microsoft.Gen.EnumStrings.Test; + +public static class ParserTests +{ + [Fact] + public static async Task InvalidTypeLevelUses() + { + var args = new[] + { + ( "EnumStrings(typeof(string))", DiagDescriptors.IncorrectOverload ), + ( "EnumStrings(ExtensionClassName = \"A.B\")", DiagDescriptors.InvalidExtensionClassName ), + ( "EnumStrings(ExtensionClassName = \"123\")", DiagDescriptors.InvalidExtensionClassName ), + ( "EnumStrings(ExtensionMethodName = \"A.B\")", DiagDescriptors.InvalidExtensionMethodName ), + ( "EnumStrings(ExtensionMethodName = \"123\")", DiagDescriptors.InvalidExtensionMethodName ), + ( "EnumStrings(ExtensionNamespace = \"A.123\")", DiagDescriptors.InvalidExtensionNamespace ), + ( "EnumStrings(ExtensionNamespace = \"123\")", DiagDescriptors.InvalidExtensionNamespace ), + }; + + foreach (var (attrArg, diag) in args) + { + var source = @$" + using Microsoft.Extensions.EnumStrings; + + namespace Test + {{ + [/*0+*/{attrArg}/*-0*/] + public enum Color + {{ + Red, + Green, + Blue, + }} + }} + "; + + var (d, _) = await RoslynTestUtils.RunGenerator( + new Generator(), + new[] { Assembly.GetAssembly(typeof(EnumStringsAttribute))! }, + new[] { source }).ConfigureAwait(false); + + Assert.Equal(1, d.Count); + source.AssertDiagnostic(0, diag, d[0]); + + (d, _) = await RoslynTestUtils.RunGenerator( + new Generator(), + new[] + { + Assembly.GetAssembly(typeof(EnumStringsAttribute))!, + Assembly.GetAssembly(typeof(System.Collections.Frozen.FrozenDictionary))!, + }, + new[] { source }).ConfigureAwait(false); + + Assert.Equal(1, d.Count); + source.AssertDiagnostic(0, diag, d[0]); + } + } + + [Fact] + public static async Task InvalidAssemblyLevelUses() + { + var args = new[] + { + ( "EnumStrings", DiagDescriptors.IncorrectOverload ), + ( "EnumStrings(typeof(MyClass))", DiagDescriptors.InvalidEnumType ), + ( "EnumStrings(typeof(MyJunk))", DiagDescriptors.InvalidEnumType ), + ( "EnumStrings(typeof(Color), ExtensionClassName = \"A.B\")", DiagDescriptors.InvalidExtensionClassName ), + ( "EnumStrings(typeof(Color), ExtensionClassName = \"123\")", DiagDescriptors.InvalidExtensionClassName ), + ( "EnumStrings(typeof(Color), ExtensionMethodName = \"A.B\")", DiagDescriptors.InvalidExtensionMethodName ), + ( "EnumStrings(typeof(Color), ExtensionMethodName = \"123\")", DiagDescriptors.InvalidExtensionMethodName ), + ( "EnumStrings(typeof(Color), ExtensionNamespace = \"A.123\")", DiagDescriptors.InvalidExtensionNamespace ), + ( "EnumStrings(typeof(Color), ExtensionNamespace = \"123\")", DiagDescriptors.InvalidExtensionNamespace ), + }; + + foreach (var (attrArg, diag) in args) + { + var source = @$" + using Microsoft.Extensions.EnumStrings; + + [assembly: /*0+*/{attrArg}/*-0*/] + + public class MyClass + {{ + }} + + public enum Color + {{ + Red, + Green, + Blue, + }} + "; + + var (d, _) = await RoslynTestUtils.RunGenerator( + new Generator(), + new[] { Assembly.GetAssembly(typeof(EnumStringsAttribute))! }, + new[] { source }).ConfigureAwait(false); + + Assert.Equal(1, d.Count); + source.AssertDiagnostic(0, diag, d[0]); + } + } +} diff --git a/test/Generators/Microsoft.Gen.EnumStrings/Unit/Directory.Build.props b/test/Generators/Microsoft.Gen.EnumStrings/Unit/Directory.Build.props new file mode 100644 index 0000000000..eec28eb8c0 --- /dev/null +++ b/test/Generators/Microsoft.Gen.EnumStrings/Unit/Directory.Build.props @@ -0,0 +1,30 @@ + + + + + Microsoft.Gen.EnumStrings.Test + Unit tests for Gen.EnumStrings. + + + + true + true + true + + + + + + + + + + + + + + + + + + diff --git a/test/Generators/Microsoft.Gen.EnumStrings/Unit/Roslyn3.8/Microsoft.Gen.EnumStrings.Roslyn3.8.Unit.Tests.csproj b/test/Generators/Microsoft.Gen.EnumStrings/Unit/Roslyn3.8/Microsoft.Gen.EnumStrings.Roslyn3.8.Unit.Tests.csproj new file mode 100644 index 0000000000..eac2eac217 --- /dev/null +++ b/test/Generators/Microsoft.Gen.EnumStrings/Unit/Roslyn3.8/Microsoft.Gen.EnumStrings.Roslyn3.8.Unit.Tests.csproj @@ -0,0 +1,5 @@ + + + 3.8 + + diff --git a/test/Generators/Microsoft.Gen.EnumStrings/Unit/Roslyn4.0/Microsoft.Gen.EnumStrings.Roslyn4.0.Unit.Tests.csproj b/test/Generators/Microsoft.Gen.EnumStrings/Unit/Roslyn4.0/Microsoft.Gen.EnumStrings.Roslyn4.0.Unit.Tests.csproj new file mode 100644 index 0000000000..18ce9dd9ba --- /dev/null +++ b/test/Generators/Microsoft.Gen.EnumStrings/Unit/Roslyn4.0/Microsoft.Gen.EnumStrings.Roslyn4.0.Unit.Tests.csproj @@ -0,0 +1,6 @@ + + + 4.0 + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + diff --git a/test/Generators/Microsoft.Gen.Logging/Generated/Common/LogMethodAttributeTests.cs b/test/Generators/Microsoft.Gen.Logging/Generated/Common/LogMethodAttributeTests.cs new file mode 100644 index 0000000000..920754ba31 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Generated/Common/LogMethodAttributeTests.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using TestClasses; +using Xunit; + +namespace Microsoft.Gen.Logging.Test; + +public class LogMethodAttributeTests +{ + private readonly FakeLogger _logger = new(); + private readonly StarRedactorProvider _redactorProvider = new(); + + [Fact] + public void AllowsAttributesStaticMethod() + { + _logger.Collector.Clear(); + AttributeTestExtensions.M0(_logger, "arg0"); + Assert.Null(_logger.LatestRecord.Exception); + Assert.Equal("M0 arg0", _logger.LatestRecord.Message); + Assert.Equal(1, _logger.Collector.Count); + } + + [Fact] + public void AllowsAttributesInstanceMethod() + { + _logger.Collector.Clear(); + new NonStaticTestClass(_logger, null!).M0("arg0"); + Assert.Null(_logger.LatestRecord.Exception); + Assert.Equal("M0 arg0", _logger.LatestRecord.Message); + Assert.Equal(1, _logger.Collector.Count); + } + + [Fact] + public void RedactsArgumentsOnlyWithDataClassificationAttributes() + { + _logger.Collector.Clear(); + AttributeTestExtensions.M1(_logger, _redactorProvider, "arg0", "arg1"); + Assert.Null(_logger.LatestRecord.Exception); + Assert.Equal("M1 **** arg1", _logger.LatestRecord.Message); + Assert.Equal(1, _logger.Collector.Count); + + _logger.Collector.Clear(); + AttributeTestExtensions.M2(_logger, _redactorProvider, "arg0", "arg1"); + Assert.Null(_logger.LatestRecord.Exception); + Assert.Equal("M2 **** arg1", _logger.LatestRecord.Message); + Assert.Equal(1, _logger.Collector.Count); + } + + [Fact] + public void RedactsArgumentsWithToString() + { + _logger.Collector.Clear(); + AttributeTestExtensions.M8(_logger, _redactorProvider, 123456); + Assert.Null(_logger.LatestRecord.Exception); + Assert.Equal("M8 ******", _logger.LatestRecord.Message); + Assert.Equal(1, _logger.Collector.Count); + + _logger.Collector.Clear(); + AttributeTestExtensions.M9(_logger, _redactorProvider, new CustomToStringTestClass()); + Assert.Null(_logger.LatestRecord.Exception); + Assert.Equal("M9 ****", _logger.LatestRecord.Message); + Assert.Equal(1, _logger.Collector.Count); + } + + [Fact] + public void HandlesAvailableDataClassificationAttributes() + { + _logger.Collector.Clear(); + AttributeTestExtensions.M3(_logger, _redactorProvider, "arg0", "arg1", "arg2", "arg3"); + Assert.Null(_logger.LatestRecord.Exception); + Assert.Equal("M3 **** **** **** ****", _logger.LatestRecord.Message); + Assert.Equal(1, _logger.Collector.Count); + + _logger.Collector.Clear(); + AttributeTestExtensions.M4(_logger, _redactorProvider, "arg0", "arg1", "arg2"); + Assert.Null(_logger.LatestRecord.Exception); + Assert.Equal("M4 **** **** ****", _logger.LatestRecord.Message); + Assert.Equal(1, _logger.Collector.Count); + } + + [Fact] + public void RedactsWhenExceedsMaxLogMethodDefineArguments() + { + _logger.Collector.Clear(); + AttributeTestExtensions.M5(_logger, _redactorProvider, "arg0", "arg1", "arg2", "arg3", "arg4", "arg5", "arg6", "arg7", "arg8", "arg9", "arg10"); + Assert.Null(_logger.LatestRecord.Exception); + Assert.Equal("M5 **** **** **** **** **** **** **** **** **** **** *****", _logger.LatestRecord.Message); + Assert.Equal(1, _logger.Collector.Count); + } + + [Fact] + public void RedactsWhenDefaultLogMethodCtor() + { + _logger.Collector.Clear(); + AttributeTestExtensions.M6(_logger, LogLevel.Critical, _redactorProvider, "arg0", "arg1"); + AssertWhenDefaultLogMethodCtor(LogLevel.Critical, ("p0", "****"), ("p1", "arg1")); + + _logger.Collector.Clear(); + AttributeTestExtensions.M7(_logger, LogLevel.Warning, _redactorProvider, "arg_0", "arg_1"); + AssertWhenDefaultLogMethodCtor(LogLevel.Warning, ("p0", "*****"), ("p1", "arg_1")); + } + + [Fact] + public void RedactsWhenRedactorProviderIsAvailableInTheInstance() + { + _logger.Collector.Clear(); + new NonStaticTestClass(_logger, _redactorProvider).M1("arg0"); + Assert.Null(_logger.LatestRecord.Exception); + Assert.Equal("M1 ****", _logger.LatestRecord.Message); + Assert.Equal(1, _logger.Collector.Count); + + _logger.Collector.Clear(); + new NonStaticTestClass(_logger, _redactorProvider).M2("arg0", "arg1", "arg2"); + Assert.Null(_logger.LatestRecord.Exception); + Assert.Equal("M2 **** **** ****", _logger.LatestRecord.Message); + Assert.Equal(1, _logger.Collector.Count); + + _logger.Collector.Clear(); + new NonStaticTestClass(_logger, _redactorProvider).M3(LogLevel.Information, "arg_0"); + AssertWhenDefaultLogMethodCtor(LogLevel.Information, ("p0", "*****")); + } + + private void AssertWhenDefaultLogMethodCtor(LogLevel expectedLevel, params (string key, string value)[] expectedState) + { + Assert.Equal(1, _logger.Collector.Count); + Assert.Null(_logger.LatestRecord.Exception); + Assert.Equal(string.Empty, _logger.LatestRecord.Message); + Assert.Equal(expectedLevel, _logger.LatestRecord.Level); + Assert.NotNull(_logger.LatestRecord.StructuredState); + Assert.Equal(expectedState.Length, _logger.LatestRecord.StructuredState!.Count); + foreach ((string key, string value) in expectedState) + { + Assert.Contains(_logger.LatestRecord.StructuredState, x => x.Key == key && x.Value == value); + } + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Generated/Common/LogMethodTests.cs b/test/Generators/Microsoft.Gen.Logging/Generated/Common/LogMethodTests.cs new file mode 100644 index 0000000000..cf49704caf --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Generated/Common/LogMethodTests.cs @@ -0,0 +1,916 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Threading; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using TestClasses; +using Xunit; + +namespace Microsoft.Gen.Logging.Test; + +public class LogMethodTests +{ + [Fact] + public void BasicTests() + { + var logger = new FakeLogger(); + + NoNamespace.CouldNotOpenSocket(logger, "microsoft.com"); + Assert.Equal(LogLevel.Critical, logger.LatestRecord.Level); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("Could not open socket to `microsoft.com`", logger.LatestRecord.Message); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + Level1.OneLevelNamespace.CouldNotOpenSocket(logger, "microsoft.com"); + Assert.Equal(LogLevel.Critical, logger.LatestRecord.Level); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("Could not open socket to `microsoft.com`", logger.LatestRecord.Message); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + Level1.Level2.TwoLevelNamespace.CouldNotOpenSocket(logger, "microsoft.com"); + Assert.Equal(LogLevel.Critical, logger.LatestRecord.Level); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("Could not open socket to `microsoft.com`", logger.LatestRecord.Message); + Assert.Equal(1, logger.Collector.Count); + } + +#if ROSLYN_4_0_OR_GREATER + [Fact] + public void FileScopedNamespaceTest() + { + var logger = new FakeLogger(); + FileScopedNamespace.Log.CouldNotOpenSocket(logger, "microsoft.com"); + Assert.Equal(LogLevel.Critical, logger.LatestRecord.Level); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("Could not open socket to `microsoft.com`", logger.LatestRecord.Message); + Assert.Equal(1, logger.Collector.Count); + } +#endif + + [Fact] + public void EnableTest() + { + var logger = new FakeLogger(); + + logger.ControlLevel(LogLevel.Trace, false); + logger.ControlLevel(LogLevel.Debug, false); + logger.ControlLevel(LogLevel.Information, false); + logger.ControlLevel(LogLevel.Warning, false); + logger.ControlLevel(LogLevel.Error, false); + logger.ControlLevel(LogLevel.Critical, false); + + LevelTestExtensions.M8(logger, LogLevel.Trace); + LevelTestExtensions.M8(logger, LogLevel.Debug); + LevelTestExtensions.M8(logger, LogLevel.Information); + LevelTestExtensions.M8(logger, LogLevel.Warning); + LevelTestExtensions.M8(logger, LogLevel.Error); + LevelTestExtensions.M8(logger, LogLevel.Critical); + + Assert.Equal(0, logger.Collector.Count); + } + + [Fact] + public void OptionalArgTest() + { + var logger = new FakeLogger(); + + SignatureTestExtensions.M2(logger, "Hello"); + Assert.Equal(1, logger.Collector.Count); + Assert.Equal("Hello World", logger.LatestRecord.Message); + Assert.Equal(3, logger.LatestRecord.StructuredState!.Count); + Assert.Equal("p1", logger.LatestRecord.StructuredState[0].Key); + Assert.Equal("Hello", logger.LatestRecord.StructuredState[0].Value); + Assert.Equal("p2", logger.LatestRecord.StructuredState[1].Key); + Assert.Equal("World", logger.LatestRecord.StructuredState[1].Value); + + logger.Collector.Clear(); + SignatureTestExtensions.M2(logger, "Hello", "World"); + Assert.Equal(1, logger.Collector.Count); + Assert.Equal("Hello World", logger.LatestRecord.Message); + Assert.Equal(3, logger.LatestRecord.StructuredState!.Count); + Assert.Equal("p1", logger.LatestRecord.StructuredState[0].Key); + Assert.Equal("Hello", logger.LatestRecord.StructuredState[0].Value); + Assert.Equal("p2", logger.LatestRecord.StructuredState[1].Key); + Assert.Equal("World", logger.LatestRecord.StructuredState[1].Value); + } + + [Fact] + public void ArgTest() + { + var logger = new FakeLogger(); + + logger.Collector.Clear(); + ArgTestExtensions.Method1(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M1", logger.LatestRecord.Message); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + ArgTestExtensions.Method2(logger, "arg1"); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M2 arg1", logger.LatestRecord.Message); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + ArgTestExtensions.Method3(logger, "arg1", 2); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M3 arg1 2", logger.LatestRecord.Message); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + ArgTestExtensions.Method4(logger, new InvalidOperationException("A")); + Assert.Equal("A", logger.LatestRecord.Exception!.Message); + Assert.Equal("M4", logger.LatestRecord.Message); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + ArgTestExtensions.Method5(logger, new InvalidOperationException("A"), new InvalidOperationException("B")); + Assert.Equal("A", logger.LatestRecord.Exception!.Message); + Assert.Equal("M5 System.InvalidOperationException: B", logger.LatestRecord.Message); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + ArgTestExtensions.Method6(logger, new InvalidOperationException("A"), 2); + Assert.Equal("A", logger.LatestRecord.Exception!.Message); + Assert.Equal("M6 2", logger.LatestRecord.Message); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + ArgTestExtensions.Method7(logger, 1, new InvalidOperationException("B")); + Assert.Equal("B", logger.LatestRecord.Exception!.Message); + Assert.Equal("M7 1", logger.LatestRecord.Message); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + ArgTestExtensions.Method8(logger, 1, 2, 3, 4, 5, 6, 7); + Assert.Equal("M81234567", logger.LatestRecord.Message); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + ArgTestExtensions.Method9(logger, 1, 2, 3, 4, 5, 6, 7); + Assert.Equal("M9 1 2 3 4 5 6 7", logger.LatestRecord.Message); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + ArgTestExtensions.Method10(logger, 1); + Assert.Equal("M101", logger.LatestRecord.Message); + Assert.Equal(1, logger.Collector.Count); + } + + [Fact] + public void CollectionTest() + { + var logger = new FakeLogger(); + + logger.Collector.Clear(); + CollectionTestExtensions.M0(logger); + TestCollection(1, logger); + + logger.Collector.Clear(); + CollectionTestExtensions.M1(logger, 0); + TestCollection(2, logger); + + logger.Collector.Clear(); + CollectionTestExtensions.M2(logger, 0, 1); + TestCollection(3, logger); + + logger.Collector.Clear(); + CollectionTestExtensions.M3(logger, 0, 1, 2); + TestCollection(4, logger); + + logger.Collector.Clear(); + CollectionTestExtensions.M4(logger, 0, 1, 2, 3); + TestCollection(5, logger); + + logger.Collector.Clear(); + CollectionTestExtensions.M5(logger, 0, 1, 2, 3, 4); + TestCollection(6, logger); + + logger.Collector.Clear(); + CollectionTestExtensions.M6(logger, 0, 1, 2, 3, 4, 5); + TestCollection(7, logger); + + logger.Collector.Clear(); + CollectionTestExtensions.M7(logger, 0, 1, 2, 3, 4, 5, 6); + TestCollection(8, logger); + + logger.Collector.Clear(); + CollectionTestExtensions.M8(logger, 0, 1, 2, 3, 4, 5, 6, 7); + TestCollection(9, logger); + + logger.Collector.Clear(); + CollectionTestExtensions.M9(logger, LogLevel.Critical, 0, new ArgumentException("Foo"), 1); + TestCollection(3, logger); + } + + [Fact] + public void ConstructorVariationsTests() + { + var logger = new FakeLogger(); + + logger.Collector.Clear(); + ConstructorVariationsTestExtensions.M0(logger, "Zero"); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M0 Zero", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Debug, logger.LatestRecord.Level); + Assert.Equal(0, logger.LatestRecord.Id.Id); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + ConstructorVariationsTestExtensions.M1(logger, LogLevel.Trace, "One"); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M1 One", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Trace, logger.LatestRecord.Level); + Assert.Equal(1, logger.LatestRecord.Id.Id); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + ConstructorVariationsTestExtensions.M2(logger, "Two"); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal(string.Empty, logger.LatestRecord.Message); + Assert.Equal(LogLevel.Debug, logger.LatestRecord.Level); + Assert.Equal(2, logger.LatestRecord.Id.Id); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + ConstructorVariationsTestExtensions.M3(logger, LogLevel.Trace, "Three"); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal(string.Empty, logger.LatestRecord.Message); + Assert.Equal(LogLevel.Trace, logger.LatestRecord.Level); + Assert.Equal(3, logger.LatestRecord.Id.Id); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + ConstructorVariationsTestExtensions.M4(logger, "Four"); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M4 Four", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Debug, logger.LatestRecord.Level); + Assert.Equal(0, logger.LatestRecord.Id.Id); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + ConstructorVariationsTestExtensions.M5(logger, LogLevel.Trace, "Five"); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M5 Five", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Trace, logger.LatestRecord.Level); + Assert.Equal(0, logger.LatestRecord.Id.Id); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + ConstructorVariationsTestExtensions.M6(logger, "Six"); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal(string.Empty, logger.LatestRecord.Message); + Assert.Equal(LogLevel.Debug, logger.LatestRecord.Level); + Assert.Equal(0, logger.LatestRecord.Id.Id); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + ConstructorVariationsTestExtensions.M7(logger, LogLevel.Information, "Seven"); + Assert.Equal(1, logger.Collector.Count); + + var logRecord = logger.LatestRecord; + Assert.Null(logRecord.Exception); + Assert.Equal(string.Empty, logRecord.Message); + Assert.Equal(LogLevel.Information, logRecord.Level); + Assert.Equal(0, logRecord.Id.Id); + Assert.Equal("M7", logRecord.Id.Name); + } + + [Fact] + public void MessageTests() + { + var logger = new FakeLogger(); + + logger.Collector.Clear(); + MessageTestExtensions.M0(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal(string.Empty, logger.LatestRecord.Message); + Assert.Equal(LogLevel.Trace, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + MessageTestExtensions.M1(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal(string.Empty, logger.LatestRecord.Message); + Assert.Equal(LogLevel.Debug, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + MessageTestExtensions.M2(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal(string.Empty, logger.LatestRecord.Message); + Assert.Equal(LogLevel.Debug, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + MessageTestExtensions.M5(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("\"Hello\" World", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Debug, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + MessageTestExtensions.M6(logger, LogLevel.Trace, "p", "q"); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("\"p\" -> \"q\"", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Trace, logger.LatestRecord.Level); + Assert.Equal(6, logger.LatestRecord.Id); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + MessageTestExtensions.M7(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("\"\n\r\\", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Debug, logger.LatestRecord.Level); + Assert.Equal(7, logger.LatestRecord.Id); + Assert.Equal(1, logger.Collector.Count); + } + + [Fact] + public void InstanceTests() + { + var logger = new FakeLogger(); + var o = new TestInstances(logger); + + logger.Collector.Clear(); + o.M0(); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M0", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Error, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + o.M1("Foo"); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M1 Foo", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Trace, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + o.M2(LogLevel.Warning, "param"); + Assert.Equal(1, logger.Collector.Count); + + var logRecord = logger.LatestRecord; + Assert.Null(logRecord.Exception); + Assert.Equal(string.Empty, logRecord.Message); + Assert.Equal(LogLevel.Warning, logRecord.Level); + Assert.NotNull(logRecord.StructuredState); + Assert.Single(logRecord.StructuredState!); + Assert.Equal("p1", logRecord.StructuredState![0].Key); + Assert.Equal("param", logRecord.StructuredState[0].Value); + } + + [Fact] + public void LevelTests() + { + var logger = new FakeLogger(); + + logger.Collector.Clear(); + LevelTestExtensions.M0(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M0", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Trace, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + LevelTestExtensions.M1(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M1", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Debug, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + LevelTestExtensions.M2(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M2", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Information, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + LevelTestExtensions.M3(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M3", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Warning, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + LevelTestExtensions.M4(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M4", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Error, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + LevelTestExtensions.M5(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M5", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Critical, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + LevelTestExtensions.M6(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M6", logger.LatestRecord.Message); + Assert.Equal(LogLevel.None, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + LevelTestExtensions.M7(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M7", logger.LatestRecord.Message); + Assert.Equal((LogLevel)42, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + LevelTestExtensions.M8(logger, LogLevel.Critical); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M8", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Critical, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + LevelTestExtensions.M9(LogLevel.Trace, logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M9", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Trace, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + LevelTestExtensions.M10(logger, LogLevel.Trace); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M10 Trace", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Trace, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + LevelTestExtensions.M11(logger, LogLevel.Trace); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M11 Microsoft.Extensions.Telemetry.Testing.Logging.FakeLogger", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Trace, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + } + + [Fact] + public void LevelTests_ForNonStatic() + { + var logger = new FakeLogger(); + + var redactorProvider = new StarRedactorProvider(); + var instance = new NonStaticTestClass(logger, redactorProvider); + + instance.NoParams(); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("No params here...", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Warning, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + instance.NoParamsWithLevel(LogLevel.Information); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("No params here as well...", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Information, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + instance.NoParamsWithLevel(LogLevel.Error); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("No params here as well...", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Error, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + } + + [Fact] + public void NonStaticNullable() + { + var logger = new FakeLogger(); + var redactorProvider = new StarRedactorProvider(); + + var instance = new NonStaticNullableTestClass(logger, redactorProvider); + instance.M2("One", "Two", "Three"); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M2 *** *** *****", logger.LatestRecord.Message); + + logger.Collector.Clear(); + instance = new NonStaticNullableTestClass(null, redactorProvider); + instance.M2("One", "Two", "Three"); + Assert.Equal(0, logger.Collector.Count); + + logger.Collector.Clear(); + instance = new NonStaticNullableTestClass(logger, null); + instance.M2("One", "Two", "Three"); + Assert.Equal("M2 (null) (null) (null)", logger.LatestRecord.Message); + } + + [Fact] + public void ExceptionTests() + { + var logger = new FakeLogger(); + + logger.Collector.Clear(); + ExceptionTestExtensions.M0(logger, new ArgumentException("Foo"), new ArgumentException("Bar")); + Assert.Equal("Foo", logger.LatestRecord.Exception!.Message); + Assert.Equal("M0 System.ArgumentException: Bar", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Trace, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + ExceptionTestExtensions.M1(new ArgumentException("Foo"), logger, new ArgumentException("Bar")); + Assert.Equal("Foo", logger.LatestRecord.Exception!.Message); + Assert.Equal("M1 System.ArgumentException: Bar", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Debug, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + ExceptionTestExtensions.M2(logger, "One", new ArgumentException("Foo")); + Assert.Equal("Foo", logger.LatestRecord.Exception!.Message); + Assert.Equal("M2 One: System.ArgumentException: Foo", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Debug, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + var exception = new ArgumentException("Foo"); + ExceptionTestExtensions.M3(exception, logger, LogLevel.Error); + Assert.Equal(1, logger.Collector.Count); + Assert.NotNull(logger.LatestRecord.Exception); + Assert.Equal(exception.Message, logger.LatestRecord.Exception!.Message); + Assert.Equal(exception.GetType(), logger.LatestRecord.Exception!.GetType()); + Assert.Equal(string.Empty, logger.LatestRecord.Message); + Assert.Equal(LogLevel.Error, logger.LatestRecord.Level); + } + + [Fact] + public void EventNameTests() + { + var logger = new FakeLogger(); + + logger.Collector.Clear(); + EventNameTestExtensions.M0(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M0", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Trace, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + Assert.Equal("CustomEventName", logger.LatestRecord.Id.Name); + + logger.Collector.Clear(); + EventNameTestExtensions.M1(LogLevel.Trace, logger, "Eight"); + Assert.Equal(1, logger.Collector.Count); + + var logRecord = logger.LatestRecord; + Assert.Null(logRecord.Exception); + Assert.Equal(string.Empty, logRecord.Message); + Assert.Equal(LogLevel.Trace, logRecord.Level); + Assert.Equal(0, logRecord.Id.Id); + Assert.Equal("M1_Event", logRecord.Id.Name); + } + + [Fact] + public void NestedClassTests() + { + var logger = new FakeLogger(); + + logger.Collector.Clear(); + NestedClassTestExtensions.NestedMiddleParentClass.NestedClass.M8(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M8", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Debug, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + NonStaticNestedClassTestExtensions.NonStaticNestedMiddleParentClass.NestedClass.M9(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M9", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Debug, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + NestedStruct.Logger.M10(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M10", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Debug, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + NestedRecord.Logger.M11(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M11", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Debug, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + + logger.Collector.Clear(); + MultiLevelNestedClass.NestedStruct.NestedRecord.Logger.M12(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M12", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Debug, logger.LatestRecord.Level); + Assert.Equal(1, logger.Collector.Count); + } + + [Fact] + public void TemplateTests() + { + var logger = new FakeLogger(); + + logger.Collector.Clear(); + TemplateTestExtensions.M0(logger, 0); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M0 0", logger.LatestRecord.Message); + AssertLastState(logger, + new KeyValuePair("A1", "0"), + new KeyValuePair("{OriginalFormat}", "M0 {A1}")); + + logger.Collector.Clear(); + TemplateTestExtensions.M1(logger, 42); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M1 42 42", logger.LatestRecord.Message); + AssertLastState(logger, + new KeyValuePair("A1", "42"), + new KeyValuePair("{OriginalFormat}", "M1 {A1} {A1}")); + + logger.Collector.Clear(); + TemplateTestExtensions.M2(logger, 42, 43, 44, 45, 46, 47, 48); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M2 42 43 44 45 46 47 48", logger.LatestRecord.Message); + AssertLastState(logger, + new KeyValuePair("A1", "42"), + new KeyValuePair("a2", "43"), + new KeyValuePair("A3", "44"), + new KeyValuePair("a4", "45"), + new KeyValuePair("A5", "46"), + new KeyValuePair("a6", "47"), + new KeyValuePair("A7", "48"), + new KeyValuePair("{OriginalFormat}", "M2 {A1} {a2} {A3} {a4} {A5} {a6} {A7}")); + + logger.Collector.Clear(); + TemplateTestExtensions.M3(logger, 42, 43); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M3 43 42", logger.LatestRecord.Message); + AssertLastState(logger, + new KeyValuePair("A1", "42"), + new KeyValuePair("a2", "43"), + new KeyValuePair("{OriginalFormat}", "M3 {a2} {A1}")); + } + + [Fact] + public void StructTests() + { + var logger = new FakeLogger(); + + logger.Collector.Clear(); + StructTestExtensions.M0(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M0", logger.LatestRecord.Message); + AssertLastState(logger, + new KeyValuePair("{OriginalFormat}", "M0")); + } + + [Fact] + public void RecordTests() + { + var logger = new FakeLogger(); + + logger.Collector.Clear(); + RecordTestExtensions.M0(logger); + Assert.Null(logger.LatestRecord.Exception); + Assert.Equal("M0", logger.LatestRecord.Message); + AssertLastState(logger, + new KeyValuePair("{OriginalFormat}", "M0")); + } + + [Fact] + public void SkipEnabledCheckTests() + { + var logger = new FakeLogger(); + logger.ControlLevel(LogLevel.Information, false); + + SkipEnabledCheckTestExtensions.LoggerMethodWithFalseSkipEnabledCheck(logger); + Assert.Equal(0, logger.Collector.Count); + + SkipEnabledCheckTestExtensions.LoggerMethodWithFalseSkipEnabledCheck(logger, LogLevel.Information, "p1"); + Assert.Equal(0, logger.Collector.Count); + +#if NET6_0_OR_GREATER + SkipEnabledCheckTestExtensions.LoggerMethodWithTrueSkipEnabledCheck(logger); + Assert.Equal(1, logger.Collector.Count); +#endif + } + + [Fact] + public void InParameterTests() + { + var logger = new FakeLogger(); + + InParameterTestExtensions.S s; + InParameterTestExtensions.M0(logger, in s); + Assert.Equal(1, logger.Collector.Count); + Assert.Contains("Hello from S", logger.Collector.LatestRecord.Message); + } + + [Fact] + public void AtSymbolsTest() + { + var logger = new FakeLogger(); + + AtSymbolsTestExtensions.M0(logger, "Test"); + Assert.Equal(1, logger.Collector.Count); + + AtSymbolsTestExtensions.M1(logger, new StarRedactorProvider(), "Test"); + Assert.Equal(2, logger.Collector.Count); + } + + [Fact] + public void OverloadsTest() + { + var logger = new FakeLogger(); + + OverloadsTestExtensions.M0(logger, "Test"); + OverloadsTestExtensions.M0(logger, 42); + Assert.Equal(2, logger.Collector.Count); + } + + [Fact] + public void NullableTests() + { + var logger = new FakeLogger(); + var redactorProvider = new StarRedactorProvider(); + + NullableTestExtensions.M0(logger, null); + Assert.Equal("M0 (null)", logger.LatestRecord.Message); + + NullableTestExtensions.M1(logger, null); + Assert.Equal("M1 (null)", logger.LatestRecord.Message); + + NullableTestExtensions.M3(logger, redactorProvider, null); + Assert.Equal("M3 (null)", logger.LatestRecord.Message); + + NullableTestExtensions.M4(logger, null, null, null, null, null, null, null, null, null); + Assert.Equal("M4 (null) (null) (null) (null) (null) (null) (null) (null) (null)", logger.LatestRecord.Message); + + NullableTestExtensions.M5(logger, null, null, null, null, null, null, null, null, null); + Assert.Equal("M5 (null) (null) (null) (null) (null) (null) (null) (null) (null)", logger.LatestRecord.Message); + + logger.Collector.Clear(); + NullableTestExtensions.M6(null, null, "Nothing"); + Assert.Equal(0, logger.Collector.Count); + + NullableTestExtensions.M6(logger, null, "Nothing"); + Assert.Equal("M6 (null)", logger.LatestRecord.Message); + } + + [Fact] + public void InvariantTest() + { + var logger = new FakeLogger(); + var dt = new DateTime(2022, 5, 22); + + var oldCulture = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("fr-CA"); + + InvariantTestExtensions.M0(logger, dt); + Assert.Equal(dt.ToString(CultureInfo.InvariantCulture), logger.LatestRecord.StructuredState![0].Value); + Assert.Equal("M0 " + dt.ToString(CultureInfo.InvariantCulture), logger.LatestRecord.Message); + + Thread.CurrentThread.CurrentCulture = oldCulture; + } + + [Fact] + public void EnumerableTest() + { + var logger = new FakeLogger(); + + EnumerableTestExtensions.M10(logger, + new[] { 1, 2, 3 }, + new[] { 4, 5, 6 }, + new Dictionary + { + { "Seven", 7 }, + { "Eight", 8 }, + { "Nine", 9 } + }); + + Assert.Equal(1, logger.Collector.Count); + + Assert.Equal("p1", logger.LatestRecord.StructuredState![0].Key); + Assert.Equal("[\"1\",\"2\",\"3\"]", logger.LatestRecord.StructuredState[0].Value); + + Assert.Equal("p2", logger.LatestRecord.StructuredState[1].Key); + Assert.Equal("[\"4\",\"5\",\"6\"]", logger.LatestRecord.StructuredState[1].Value); + + Assert.Equal("p3", logger.LatestRecord.StructuredState[2].Key); + Assert.Equal("{\"Seven\"=\"7\",\"Eight\"=\"8\",\"Nine\"=\"9\"}", logger.LatestRecord.StructuredState[2].Value); + } + + [Fact] + public void NullableEnumerableTest() + { + var logger = new FakeLogger(); + + EnumerableTestExtensions.M11(logger, null); + Assert.Equal(1, logger.Collector.Count); + Assert.Equal("p1", logger.LatestRecord.StructuredState![0].Key); + Assert.Null(logger.LatestRecord.StructuredState[0].Value); + + logger.Collector.Clear(); + EnumerableTestExtensions.M11(logger, new[] { 1, 2, 3 }); + Assert.Equal(1, logger.Collector.Count); + Assert.Equal("p1", logger.LatestRecord.StructuredState![0].Key); + Assert.Equal("[\"1\",\"2\",\"3\"]", logger.LatestRecord.StructuredState[0].Value); + + logger.Collector.Clear(); + EnumerableTestExtensions.M12(logger, null); + Assert.Equal(1, logger.Collector.Count); + Assert.Equal("class", logger.LatestRecord.StructuredState![0].Key); + Assert.Null(logger.LatestRecord.StructuredState[0].Value); + + logger.Collector.Clear(); + EnumerableTestExtensions.M12(logger, new[] { 1, 2, 3 }); + Assert.Equal(1, logger.Collector.Count); + Assert.Equal("class", logger.LatestRecord.StructuredState![0].Key); + Assert.Equal("[\"1\",\"2\",\"3\"]", logger.LatestRecord.StructuredState[0].Value); + + logger.Collector.Clear(); + EnumerableTestExtensions.M13(logger, default); + Assert.Equal(1, logger.Collector.Count); + Assert.Equal("p1", logger.LatestRecord.StructuredState![0].Key); + Assert.Equal("[\"1\",\"2\",\"3\"]", logger.LatestRecord.StructuredState[0].Value); + + logger.Collector.Clear(); +#pragma warning disable SA1129 // Do not use default value type constructor + EnumerableTestExtensions.M14(logger, new StructEnumerable()); +#pragma warning restore SA1129 // Do not use default value type constructor + Assert.Equal(1, logger.Collector.Count); + Assert.Equal("p1", logger.LatestRecord.StructuredState![0].Key); + Assert.Equal("[\"1\",\"2\",\"3\"]", logger.LatestRecord.StructuredState[0].Value); + + logger.Collector.Clear(); + EnumerableTestExtensions.M14(logger, default); + Assert.Equal(1, logger.Collector.Count); + Assert.Equal("p1", logger.LatestRecord.StructuredState![0].Key); + Assert.Null(logger.LatestRecord.StructuredState[0].Value); + } + + [Fact] + public void FormattableTest() + { + var logger = new FakeLogger(); + FormattableTestExtensions.Method1(logger, new FormattableTestExtensions.Formattable()); + Assert.Equal(1, logger.Collector.Count); + Assert.Equal("p1", logger.LatestRecord.StructuredState![0].Key); + Assert.Equal("Formatted!", logger.LatestRecord.StructuredState[0].Value); + + logger.Collector.Clear(); + FormattableTestExtensions.Method2(logger, new FormattableTestExtensions.ComplexObj()); + Assert.Equal(1, logger.Collector.Count); + Assert.Equal("p1_P1", logger.LatestRecord.StructuredState![1].Key); + Assert.Equal("Formatted!", logger.LatestRecord.StructuredState[1].Value); + } + + private static void AssertLastState(FakeLogger logger, params KeyValuePair[] expected) + { + var rol = (IReadOnlyList>)logger.LatestRecord.State!; + int count = 0; + foreach (var kvp in expected) + { + Assert.Equal(kvp.Key, rol[count].Key); + Assert.Equal(kvp.Value, rol[count].Value); + count++; + } + } + + [SuppressMessage("Minor Code Smell", "S4056:Overloads with a \"CultureInfo\" or an \"IFormatProvider\" parameter should be used", Justification = "Not appropriate here")] + private static void TestCollection(int expected, FakeLogger logger) + { + var rol = (logger.LatestRecord.State as IReadOnlyList>)!; + Assert.NotNull(rol); + + Assert.Equal(expected, rol.Count); + for (int i = 0; i < expected; i++) + { + if (i != expected - 1) + { + var kvp = new KeyValuePair($"p{i}", i.ToString()); + Assert.Equal(kvp, rol[i]); + } + } + + int count = 0; + foreach (var actual in rol) + { + if (count != expected - 1) + { + var kvp = new KeyValuePair($"p{count}", count.ToString()); + Assert.Equal(kvp, actual); + } + + count++; + } + + Assert.Equal(expected, count); + _ = Assert.Throws(() => _ = rol[expected]); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Generated/Common/LogPropertiesProviderTests.cs b/test/Generators/Microsoft.Gen.Logging/Generated/Common/LogPropertiesProviderTests.cs new file mode 100644 index 0000000000..e41078a1da --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Generated/Common/LogPropertiesProviderTests.cs @@ -0,0 +1,382 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Globalization; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Microsoft.Shared.Text; +using TestClasses; +using Xunit; + +namespace Microsoft.Gen.Logging.Test; + +public class LogPropertiesProviderTests +{ + private readonly FakeLogger _logger = new(); + + internal class ClassToBeLogged + { + public string MyStringProperty { get; set; } = "TestString"; + public override string ToString() => "ClassToLog string representation"; + } + + [Fact] + public void LogsWithObject() + { + object obj = new ClassToBeLogged(); + LogPropertiesProviderWithObjectExtensions.OneParam(_logger, obj); + + Assert.Equal(1, _logger.Collector.Count); + + var latestRecord = _logger.Collector.LatestRecord; + Assert.Equal(LogLevel.Warning, latestRecord.Level); + Assert.Equal($"Custom provided properties for {obj}.", latestRecord.Message); + + var expectedState = new Dictionary + { + ["Param"] = obj.ToString(), + ["param_ToString"] = obj + " ProvidePropertiesCall", + ["{OriginalFormat}"] = "Custom provided properties for {Param}." + }; + + latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void LogsWhenNonStaticClass() + { + const string StringParamValue = "Value for a string"; + + var classToLog = new ClassToLog { MyIntProperty = 0 }; + new NonStaticTestClass(_logger, null!).LogPropertiesWithProvider(StringParamValue, classToLog); + + Assert.Equal(1, _logger.Collector.Count); + var latestRecord = _logger.Collector.LatestRecord; + Assert.Equal(LogLevel.Information, latestRecord.Level); + Assert.Equal($"LogProperties with provider: {StringParamValue}, {classToLog}", latestRecord.Message); + + var expectedState = new Dictionary + { + ["P0"] = StringParamValue, + ["P1"] = classToLog.ToString(), + ["p1_MyIntProperty"] = classToLog.MyIntProperty.ToInvariantString(), + ["p1_Custom_property_name"] = classToLog.MyStringProperty, + ["{OriginalFormat}"] = "LogProperties with provider: {P0}, {P1}" + }; + + latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void LogsWhenDefaultAttrCtorInNonStaticClass() + { + const string StringParamValue = "Value for a string"; + + var classToLog = new ClassToLog { MyIntProperty = ushort.MaxValue }; + new NonStaticTestClass(_logger, null!).DefaultAttrCtorLogPropertiesWithProvider(LogLevel.Debug, StringParamValue, classToLog); + + Assert.Equal(1, _logger.Collector.Count); + var latestRecord = _logger.Collector.LatestRecord; + Assert.Null(latestRecord.Exception); + Assert.Equal(0, latestRecord.Id.Id); + Assert.Equal(LogLevel.Debug, latestRecord.Level); + Assert.Equal(string.Empty, latestRecord.Message); + + var expectedState = new Dictionary + { + ["p0"] = StringParamValue, + ["p1_MyIntProperty"] = classToLog.MyIntProperty.ToInvariantString(), + ["p1_Custom_property_name"] = classToLog.MyStringProperty + }; + + latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void LogsWhenDefaultAttrCtorInStaticClass() + { + var classToLog = new ClassToLog { MyIntProperty = ushort.MaxValue }; + LogPropertiesProviderExtensions.DefaultAttributeCtor(_logger, LogLevel.Trace, classToLog); + + Assert.Equal(1, _logger.Collector.Count); + var latestRecord = _logger.Collector.LatestRecord; + Assert.Null(latestRecord.Exception); + Assert.Equal(0, latestRecord.Id.Id); + Assert.Equal(LogLevel.Trace, latestRecord.Level); + Assert.Equal(string.Empty, latestRecord.Message); + + var expectedState = new Dictionary + { + ["param_MyIntProperty"] = classToLog.MyIntProperty.ToInvariantString(), + ["param_Custom_property_name"] = classToLog.MyStringProperty + }; + + latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void LogsWhenOmitParamNameIsTrue() + { + var props = new LogPropertiesOmitParameterNameExtensions.MyProps + { + P0 = 42, + P1 = "foo" + }; + + LogPropertiesOmitParameterNameExtensions.M1(_logger, props); + + Assert.Equal(1, _logger.Collector.Count); + var latestRecord = _logger.Collector.LatestRecord; + Assert.Equal(LogLevel.Warning, latestRecord.Level); + Assert.Equal(string.Empty, latestRecord.Message); + + var state = latestRecord.StructuredState!; + Assert.Equal(2, state.Count); + Assert.Equal("P0", state[0].Key); + Assert.Equal(props.P0.ToString(CultureInfo.InvariantCulture), state[0].Value); + Assert.Equal("Custom_property_name", state[1].Key); + Assert.Equal(props.P1, state[1].Value); + } + + [Fact] + public void LogsWhenOmitParamNameIsTrueWithDefaultAttrCtor() + { + var props = new LogPropertiesOmitParameterNameExtensions.MyProps + { + P0 = 42, + P1 = "foo" + }; + + LogPropertiesOmitParameterNameExtensions.M3(_logger, LogLevel.Error, props); + + Assert.Equal(1, _logger.Collector.Count); + var latestRecord = _logger.Collector.LatestRecord; + Assert.Null(latestRecord.Exception); + Assert.Equal(0, latestRecord.Id.Id); + Assert.Equal(LogLevel.Error, latestRecord.Level); + Assert.Equal(string.Empty, latestRecord.Message); + + var state = latestRecord.StructuredState!; + Assert.Equal(2, state.Count); + Assert.Equal("P0", state[0].Key); + Assert.Equal(props.P0.ToString(CultureInfo.InvariantCulture), state[0].Value); + Assert.Equal("Custom_property_name", state[1].Key); + Assert.Equal(props.P1, state[1].Value); + } + + [Fact] + public void LogsWithNullObject() + { + LogPropertiesProviderWithObjectExtensions.OneParam(_logger, null!); + + Assert.Equal(1, _logger.Collector.Count); + + var latestRecord = _logger.Collector.LatestRecord; + Assert.Equal(LogLevel.Warning, latestRecord.Level); + Assert.Equal("Custom provided properties for (null).", latestRecord.Message); + + var expectedState = new Dictionary + { + ["Param"] = null, + ["{OriginalFormat}"] = "Custom provided properties for {Param}." + }; + + latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void LogsWhenNullStronglyTypedObject() + { + LogPropertiesProviderExtensions.LogMethodCustomPropsProvider(_logger, null!); + + Assert.Equal(1, _logger.Collector.Count); + var latestRecord = _logger.Collector.LatestRecord; + Assert.Equal(LogLevel.Warning, latestRecord.Level); + Assert.Equal("Custom provided properties for (null).", latestRecord.Message); + + var expectedState = new Dictionary + { + ["Param"] = null, + ["{OriginalFormat}"] = "Custom provided properties for {Param}." + }; + + latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void LogsWhenNonNullStronglyTypedObject() + { + var classToLog = new ClassToLog { MyIntProperty = 0 }; + LogPropertiesProviderExtensions.LogMethodCustomPropsProvider(_logger, classToLog); + + Assert.Equal(1, _logger.Collector.Count); + var latestRecord = _logger.Collector.LatestRecord; + Assert.Equal(LogLevel.Warning, latestRecord.Level); + Assert.Equal($"Custom provided properties for {classToLog}.", latestRecord.Message); + + var expectedState = new Dictionary + { + ["Param"] = classToLog.ToString(), + ["param_MyIntProperty"] = classToLog.MyIntProperty.ToInvariantString(), + ["param_Custom_property_name"] = classToLog.MyStringProperty, + ["{OriginalFormat}"] = "Custom provided properties for {Param}." + }; + + latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void LogsWhenStruct() + { + var structToLog = new StructToLog { MyIntProperty = 0 }; + LogPropertiesProviderExtensions.LogMethodCustomPropsProviderStruct(_logger, structToLog); + + Assert.Equal(1, _logger.Collector.Count); + var latestRecord = _logger.Collector.LatestRecord; + Assert.Equal(LogLevel.Debug, latestRecord.Level); + Assert.Equal($"Custom provided properties for struct.", latestRecord.Message); + + var expectedState = new Dictionary + { + ["param_MyIntProperty"] = structToLog.MyIntProperty.ToInvariantString(), + ["param_Custom_property_name"] = structToLog.MyStringProperty, + ["{OriginalFormat}"] = "Custom provided properties for struct." + }; + + latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void LogsWhenInterface() + { + IInterfaceToLog interfaceToLog = new InterfaceImpl { MyIntProperty = 0 }; + LogPropertiesProviderExtensions.LogMethodCustomPropsProviderInterface(_logger, interfaceToLog); + + Assert.Equal(1, _logger.Collector.Count); + var latestRecord = _logger.Collector.LatestRecord; + Assert.Equal(LogLevel.Information, latestRecord.Level); + Assert.Equal($"Custom provided properties for interface.", latestRecord.Message); + + var expectedState = new Dictionary + { + ["param_MyIntProperty"] = interfaceToLog.MyIntProperty.ToInvariantString(), + ["param_Custom_property_name"] = interfaceToLog.MyStringProperty, + ["{OriginalFormat}"] = "Custom provided properties for interface." + }; + + latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void LogsWhenProviderCombinedWithLogProperties() + { + var classToLog = new ClassToLog { MyIntProperty = 0 }; + LogPropertiesProviderExtensions.LogMethodCombinePropsProvider(_logger, classToLog, classToLog); + + Assert.Equal(1, _logger.Collector.Count); + var latestRecord = _logger.Collector.LatestRecord; + Assert.Equal(LogLevel.Warning, latestRecord.Level); + Assert.Equal("No params.", latestRecord.Message); + + var expectedState = new Dictionary + { + ["param1_MyIntProperty"] = classToLog.MyIntProperty.ToInvariantString(), + ["param1_MyStringProperty"] = classToLog.MyStringProperty, + ["param2_MyIntProperty"] = classToLog.MyIntProperty.ToInvariantString(), + ["param2_Custom_property_name"] = classToLog.MyStringProperty, + ["{OriginalFormat}"] = "No params." + }; + + latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void LogsTwoStronglyTypedParams() + { + const string StringParamValue = "Value for a string"; + + var classToLog1 = new ClassToLog { MyIntProperty = 1 }; + var classToLog2 = new ClassToLog { MyIntProperty = -1 }; + LogPropertiesProviderExtensions.LogMethodCustomPropsProviderTwoParams(_logger, StringParamValue, classToLog1, classToLog2); + + Assert.Equal(1, _logger.Collector.Count); + var latestRecord = _logger.Collector.LatestRecord; + Assert.Equal(LogLevel.Warning, latestRecord.Level); + Assert.Equal($"Custom provided properties for both complex params and {StringParamValue}.", latestRecord.Message); + + var expectedState = new Dictionary + { + ["StringParam"] = StringParamValue, + ["param_MyIntProperty"] = classToLog1.MyIntProperty.ToInvariantString(), + ["param_Custom_property_name"] = classToLog1.MyStringProperty, + ["param2_Another_property_name"] = classToLog2.MyStringProperty.ToUpperInvariant(), + ["param2_MyIntProperty_test"] = classToLog2.MyIntProperty.ToInvariantString(), + ["{OriginalFormat}"] = "Custom provided properties for both complex params and {StringParam}." + }; + + latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + + // Changing object and logging again to test that IResettable for props provider works correctly: + classToLog1.MyIntProperty = int.MaxValue; + classToLog2.MyIntProperty = int.MinValue; + LogPropertiesProviderExtensions.LogMethodCustomPropsProviderTwoParams(_logger, StringParamValue, classToLog1, classToLog2); + + Assert.Equal(2, _logger.Collector.Count); + expectedState["param_MyIntProperty"] = classToLog1.MyIntProperty.ToInvariantString(); + expectedState["param2_MyIntProperty_test"] = classToLog2.MyIntProperty.ToInvariantString(); + _logger.Collector.LatestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void LogsTwoObjectParams() + { + const string StringParamValue = "ValueForAString"; + + object obj1 = new ClassToBeLogged(); + object obj2 = new ClassToBeLogged(); + LogPropertiesProviderWithObjectExtensions.TwoParams(_logger, StringParamValue, obj1, obj2); + + Assert.Equal(1, _logger.Collector.Count); + + var latestRecord = _logger.Collector.LatestRecord; + Assert.Equal(LogLevel.Warning, latestRecord.Level); + Assert.Equal($"Custom provided properties for both complex params and {StringParamValue}.", latestRecord.Message); + + var expectedState = new Dictionary + { + ["StringParam"] = StringParamValue, + ["param_ToString"] = obj1 + " ProvidePropertiesCall", + ["param2_Type"] = obj2.GetType().ToString(), + ["param2_ToString"] = obj2 + " ProvideOtherPropertiesCall", + ["{OriginalFormat}"] = "Custom provided properties for both complex params and {StringParam}." + }; + + latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void LogsTwoNullObjectParams() + { + const string StringParamValue = "ValueForAString"; + + LogPropertiesProviderWithObjectExtensions.TwoParams(_logger, StringParamValue, null!, null!); + + Assert.Equal(1, _logger.Collector.Count); + + var latestRecord = _logger.Collector.LatestRecord; + Assert.Equal(LogLevel.Warning, latestRecord.Level); + Assert.Equal($"Custom provided properties for both complex params and {StringParamValue}.", latestRecord.Message); + + var expectedState = new Dictionary + { + ["StringParam"] = StringParamValue, + ["param2_Type"] = null, + ["param2_ToString"] = " ProvideOtherPropertiesCall", + ["{OriginalFormat}"] = "Custom provided properties for both complex params and {StringParam}." + }; + + latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Generated/Common/LogPropertiesRedactionTests.cs b/test/Generators/Microsoft.Gen.Logging/Generated/Common/LogPropertiesRedactionTests.cs new file mode 100644 index 0000000000..dd8ef12979 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Generated/Common/LogPropertiesRedactionTests.cs @@ -0,0 +1,162 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Globalization; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using TestClasses; +using Xunit; + +using static TestClasses.LogPropertiesRedactionExtensions; + +namespace Microsoft.Gen.Logging.Test; + +public class LogPropertiesRedactionTests +{ + private readonly FakeLogger _logger = new(); + private readonly StarRedactorProvider _redactorProvider = new(); + + [Fact] + public void RedactsWhenRedactorProviderIsAvailableInTheInstance() + { + var instance = new NonStaticTestClass(_logger, _redactorProvider); + var classToRedact = new MyBaseClassToRedact(); + + instance.LogPropertiesWithRedaction("arg0", classToRedact); + + Assert.Equal(1, _logger.Collector.Count); + Assert.Null(_logger.LatestRecord.Exception); + Assert.Equal("LogProperties with redaction: ****", _logger.LatestRecord.Message); + + var expectedState = new Dictionary + { + ["P0"] = "****", + ["p1_StringPropertyBase"] = new('*', classToRedact.StringPropertyBase.Length), + ["{OriginalFormat}"] = "LogProperties with redaction: {P0}" + }; + + _logger.Collector.LatestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void RedactsWhenDefaultAttrCtorAndRedactorProviderIsInTheInstance() + { + var instance = new NonStaticTestClass(_logger, _redactorProvider); + var classToRedact = new MyBaseClassToRedact(); + + instance.DefaultAttrCtorLogPropertiesWithRedaction(LogLevel.Information, "arg0", classToRedact); + + Assert.Equal(1, _logger.Collector.Count); + Assert.Null(_logger.LatestRecord.Exception); + Assert.Equal(LogLevel.Information, _logger.LatestRecord.Level); + Assert.Equal(string.Empty, _logger.LatestRecord.Message); + + var expectedState = new Dictionary + { + ["p0"] = "****", + ["p1_StringPropertyBase"] = new('*', classToRedact.StringPropertyBase.Length), + }; + + _logger.Collector.LatestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void RedactsWhenLogMethodIsStaticNoParams() + { + var classToRedact = new ClassToRedact(); + + LogNoParams(_logger, _redactorProvider, classToRedact); + + Assert.Equal(1, _logger.Collector.Count); + Assert.Null(_logger.LatestRecord.Exception); + Assert.Equal("No template params", _logger.LatestRecord.Message); + + var expectedState = new Dictionary + { + ["classToLog_StringProperty"] = new('*', classToRedact.StringProperty.Length), + ["classToLog_StringPropertyBase"] = new('*', classToRedact.StringPropertyBase.Length), + ["classToLog_SimplifiedNullableIntProperty"] = classToRedact.SimplifiedNullableIntProperty.ToString(CultureInfo.InvariantCulture), + ["classToLog_GetOnlyProperty"] = new('*', classToRedact.GetOnlyProperty.Length), + ["classToLog_TransitiveProp_TransitiveNumberProp"] = classToRedact.TransitiveProp.TransitiveNumberProp.ToString(CultureInfo.InvariantCulture), + ["classToLog_TransitiveProp_TransitiveStringProp"] = new('*', classToRedact.TransitiveProp.TransitiveStringProp.Length), + + ["classToLog_NoRedactionProp"] = classToRedact.NoRedactionProp, + ["{OriginalFormat}"] = "No template params" + }; + + _logger.Collector.LatestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void RedactsWhenDefaultAttrCtorAndIsStaticNoParams() + { + var classToRedact = new ClassToRedact(); + + LogNoParamsDefaultCtor(_logger, LogLevel.Warning, _redactorProvider, classToRedact); + + Assert.Equal(1, _logger.Collector.Count); + Assert.Null(_logger.LatestRecord.Exception); + Assert.Equal(LogLevel.Warning, _logger.LatestRecord.Level); + Assert.Equal(string.Empty, _logger.LatestRecord.Message); + + var expectedState = new Dictionary + { + ["classToLog_StringProperty"] = new('*', classToRedact.StringProperty.Length), + ["classToLog_StringPropertyBase"] = new('*', classToRedact.StringPropertyBase.Length), + ["classToLog_SimplifiedNullableIntProperty"] = classToRedact.SimplifiedNullableIntProperty.ToString(CultureInfo.InvariantCulture), + ["classToLog_GetOnlyProperty"] = new('*', classToRedact.GetOnlyProperty.Length), + ["classToLog_TransitiveProp_TransitiveNumberProp"] = classToRedact.TransitiveProp.TransitiveNumberProp.ToString(CultureInfo.InvariantCulture), + ["classToLog_TransitiveProp_TransitiveStringProp"] = new('*', classToRedact.TransitiveProp.TransitiveStringProp.Length), + + ["classToLog_NoRedactionProp"] = classToRedact.NoRedactionProp, + }; + + _logger.Collector.LatestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void RedactsWhenLogMethodIsStaticTwoParams() + { + var classToRedact = new MyTransitiveClass(); + + LogTwoParams(_logger, _redactorProvider, "string_prop", classToRedact); + + Assert.Equal(1, _logger.Collector.Count); + Assert.Null(_logger.LatestRecord.Exception); + Assert.Equal("Only *********** as param", _logger.LatestRecord.Message); + + var expectedState = new Dictionary + { + ["StringProperty"] = "***********", + ["complexParam_TransitiveNumberProp"] = classToRedact.TransitiveNumberProp.ToString(CultureInfo.InvariantCulture), + ["complexParam_TransitiveStringProp"] = new('*', classToRedact.TransitiveStringProp.Length), + ["{OriginalFormat}"] = "Only {StringProperty} as param" + }; + + _logger.Collector.LatestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void RedactsWhenDefaultAttrCtorAndIsStaticTwoParams() + { + var classToRedact = new MyTransitiveClass(); + + LogTwoParamsDefaultCtor(_logger, _redactorProvider, LogLevel.None, "string_prop", classToRedact); + + Assert.Equal(1, _logger.Collector.Count); + Assert.Null(_logger.LatestRecord.Exception); + Assert.Equal(LogLevel.None, _logger.LatestRecord.Level); + Assert.Equal(string.Empty, _logger.LatestRecord.Message); + + var expectedState = new Dictionary + { + ["stringProperty"] = "***********", + ["complexParam_TransitiveNumberProp"] = classToRedact.TransitiveNumberProp.ToString(CultureInfo.InvariantCulture), + ["complexParam_TransitiveStringProp"] = new('*', classToRedact.TransitiveStringProp.Length), + }; + + _logger.Collector.LatestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Generated/Common/LogPropertiesTests.cs b/test/Generators/Microsoft.Gen.Logging/Generated/Common/LogPropertiesTests.cs new file mode 100644 index 0000000000..afb33d8b55 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Generated/Common/LogPropertiesTests.cs @@ -0,0 +1,487 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using System.Numerics; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Microsoft.Shared.Text; +using TestClasses; +using Xunit; + +using static TestClasses.LogPropertiesExtensions; + +namespace Microsoft.Gen.Logging.Test; + +public class LogPropertiesTests +{ + private const string StringProperty = "microsoft.com"; + private readonly FakeLogger _logger = new(); + + [Fact] + public void LogPropertiesEnumerables() + { + var props = new LogPropertiesSimpleExtensions.MyProps + { + P5 = new[] { 1, 2, 3 }, + P6 = new[] { 4, 5, 6 }, + P7 = new Dictionary + { + { "Seven", 7 }, + { "Eight", 8 }, + { "Nine", 9 } + } + }; + + LogPropertiesSimpleExtensions.LogFunc(_logger, "Hello", props); + + Assert.Equal(1, _logger.Collector.Count); + + Assert.Equal("myProps_P5", _logger.LatestRecord.StructuredState![7].Key); + Assert.Equal("[\"1\",\"2\",\"3\"]", _logger.LatestRecord.StructuredState[7].Value); + + Assert.Equal("myProps_P6", _logger.LatestRecord.StructuredState[8].Key); + Assert.Equal("[\"4\",\"5\",\"6\"]", _logger.LatestRecord.StructuredState[8].Value); + + Assert.Equal("myProps_P7", _logger.LatestRecord.StructuredState[9].Key); + Assert.Equal("{\"Seven\"=\"7\",\"Eight\"=\"8\",\"Nine\"=\"9\"}", _logger.LatestRecord.StructuredState[9].Value); + } + + [Fact] + public void LogPropertiesOmitParamName() + { + var props = new LogPropertiesOmitParameterNameExtensions.MyProps + { + P0 = 42, + P1 = "foo" + }; + + LogPropertiesOmitParameterNameExtensions.M0(_logger, props); + + var state = _logger.LatestRecord.StructuredState!; + Assert.Equal(2, state.Count); + Assert.Equal("P0", state[0].Key); + Assert.Equal(props.P0.ToString(CultureInfo.InvariantCulture), state[0].Value); + Assert.Equal("P1", state[1].Key); + Assert.Equal(props.P1, state[1].Value); + } + + [Fact] + public void LogPropertiesOmitParamNameDefaultAttrCtor() + { + var props = new LogPropertiesOmitParameterNameExtensions.MyProps + { + P0 = 42, + P1 = "foo" + }; + + LogPropertiesOmitParameterNameExtensions.M2(_logger, LogLevel.Critical, props); + + var state = _logger.LatestRecord.StructuredState!; + Assert.Equal(2, state.Count); + Assert.Equal("P0", state[0].Key); + Assert.Equal(props.P0.ToString(CultureInfo.InvariantCulture), state[0].Value); + Assert.Equal("P1", state[1].Key); + Assert.Equal(props.P1, state[1].Value); + } + + [Fact] + public void LogPropertiesSpecialTypes() + { + var props = new LogPropertiesSpecialTypesExtensions.MyProps + { + P0 = DateTime.Now, + P1 = DateTimeOffset.Now, + P2 = new TimeSpan(1234), + P3 = Guid.NewGuid(), + P4 = new Version(1, 2, 3, 4), + P5 = new Uri("https://www.microsoft.com"), + P6 = IPAddress.Parse("192.168.10.1"), + P7 = new IPEndPoint(IPAddress.Parse("192.168.10.1"), 42), + P8 = new IPEndPoint(IPAddress.Parse("192.168.10.1"), 42), + P9 = new DnsEndPoint("microsoft.com", 42), + P10 = new BigInteger(3.1415), + P11 = new Complex(1.2, 3.4), + P12 = new Matrix3x2(1, 2, 3, 4, 5, 6), + P13 = new Matrix4x4(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16), + P14 = new Plane(1, 2, 3, 4), + P15 = new Quaternion(1, 2, 3, 4), + P16 = new Vector2(1), + P17 = new Vector3(1, 2, 3), + P18 = new Vector4(1, 2, 3, 4), + +#if NET6_0_OR_GREATER + P19 = new TimeOnly(123), + P20 = new DateOnly(2022, 6, 21), +#endif + }; + + LogPropertiesSpecialTypesExtensions.M0(_logger, props); + var state = _logger.LatestRecord.StructuredState!; + +#if NET6_0_OR_GREATER + Assert.Equal(21, state.Count); +#else + Assert.Equal(19, state.Count); +#endif + + Assert.Equal("p_P0", state[0].Key); + Assert.Equal(props.P0.ToString(CultureInfo.InvariantCulture), state[0].Value); + Assert.Equal("p_P1", state[1].Key); + Assert.Equal(props.P1.ToString(CultureInfo.InvariantCulture), state[1].Value); + Assert.Equal("p_P2", state[2].Key); + Assert.Equal(props.P2.ToString(null, CultureInfo.InvariantCulture), state[2].Value); + Assert.Equal("p_P3", state[3].Key); + Assert.Equal(props.P3.ToString(), state[3].Value); + Assert.Equal("p_P4", state[4].Key); + Assert.Equal(props.P4.ToString(), state[4].Value); + Assert.Equal("p_P5", state[5].Key); + Assert.Equal(props.P5.ToString(), state[5].Value); + Assert.Equal("p_P6", state[6].Key); + Assert.Equal(props.P6.ToString(), state[6].Value); + Assert.Equal("p_P7", state[7].Key); + Assert.Equal(props.P7.ToString(), state[7].Value); + Assert.Equal("p_P8", state[8].Key); + Assert.Equal(props.P8.ToString(), state[8].Value); + Assert.Equal("p_P9", state[9].Key); + Assert.Equal(props.P9.ToString(), state[9].Value); + Assert.Equal("p_P10", state[10].Key); + Assert.Equal(props.P10.ToString(CultureInfo.InvariantCulture), state[10].Value); + Assert.Equal("p_P11", state[11].Key); + Assert.Equal(props.P11.ToString(CultureInfo.InvariantCulture), state[11].Value); + Assert.Equal("p_P12", state[12].Key); + Assert.Equal(props.P12.ToString(), state[12].Value); + Assert.Equal("p_P13", state[13].Key); + Assert.Equal(props.P13.ToString(), state[13].Value); + Assert.Equal("p_P14", state[14].Key); + Assert.Equal(props.P14.ToString(), state[14].Value); + Assert.Equal("p_P15", state[15].Key); + Assert.Equal(props.P15.ToString(), state[15].Value); + Assert.Equal("p_P16", state[16].Key); + Assert.Equal(props.P16.ToString(), state[16].Value); + Assert.Equal("p_P17", state[17].Key); + Assert.Equal(props.P17.ToString(), state[17].Value); + Assert.Equal("p_P18", state[18].Key); + Assert.Equal(props.P18.ToString(), state[18].Value); + +#if NET6_0_OR_GREATER + Assert.Equal("p_P19", state[19].Key); + Assert.Equal(props.P19.ToString(CultureInfo.InvariantCulture), state[19].Value); + Assert.Equal("p_P20", state[20].Key); + Assert.Equal(props.P20.ToString(CultureInfo.InvariantCulture), state[20].Value); +#endif + + } + + [Fact] + public void LogPropertiesNullHandling() + { + var provider = new StarRedactorProvider(); + + var props = new LogPropertiesNullHandlingExtensions.MyProps + { + P0 = null!, + P1 = null, + P2 = 2, + P3 = null, + }; + + LogPropertiesNullHandlingExtensions.M0(_logger, provider, props); + Assert.Equal(5, _logger.LatestRecord.StructuredState!.Count); + Assert.Equal("p_P0", _logger.LatestRecord.StructuredState[0].Key); + Assert.Null(_logger.LatestRecord.StructuredState[0].Value); + Assert.Equal("p_P1", _logger.LatestRecord.StructuredState[1].Key); + Assert.Null(_logger.LatestRecord.StructuredState[1].Value); + Assert.Equal("p_P2", _logger.LatestRecord.StructuredState[2].Key); + Assert.Equal(props.P2.ToString(null, CultureInfo.InvariantCulture), _logger.LatestRecord.StructuredState[2].Value); + Assert.Equal("p_P3", _logger.LatestRecord.StructuredState[3].Key); + Assert.Null(_logger.LatestRecord.StructuredState[3].Value); + Assert.Equal("p_P4", _logger.LatestRecord.StructuredState[4].Key); + Assert.Null(_logger.LatestRecord.StructuredState[4].Value); + Assert.Equal(1, _logger.Collector.Count); + + _logger.Collector.Clear(); + LogPropertiesNullHandlingExtensions.M1(_logger, provider, props); + Assert.Equal(1, _logger.LatestRecord.StructuredState!.Count); + Assert.Equal("p_P2", _logger.LatestRecord.StructuredState[0].Key); + Assert.Equal(props.P2.ToString(null, CultureInfo.InvariantCulture), _logger.LatestRecord.StructuredState[0].Value); + Assert.Equal(1, _logger.Collector.Count); + } + + [Fact] + public void LogPropertiesTest() + { + var classToLog = new MyDerivedClass(double.Epsilon) + { + StringProperty = "test Abc", + SimplifiedNullableIntProperty = null, + ExplicitNullableIntProperty = 2, + StringPropertyBase = "Base Abc", + SetOnlyProperty = DateTime.MinValue, + NonVirtualPropertyBase = "NonVirtualPropertyBase", + TransitivePropertyArray = new[] { 1, 2, 3 }, + TransitiveProperty = new MyTransitiveDerivedClass + { + TransitiveStringProp = "Transitive string", + TransitiveVirtualProp = int.MaxValue, + InnerTransitiveProperty = new LeafTransitiveDerivedClass(), + TransitiveDerivedProp = byte.MaxValue + }, + AnotherTransitiveProperty = null, + VirtualInterimProperty = -1, + PropertyOfGenerics = new GenericClass { GenericProp = 1 }, + InterimProperty = short.MaxValue + }; + + LogFunc(_logger, StringProperty, classToLog); + Assert.Equal(1, _logger.Collector.Count); + Assert.Equal(LogLevel.Debug, _logger.Collector.LatestRecord.Level); + + var expectedState = new Dictionary + { + ["classToLog_StringProperty_1"] = "microsoft.com", + ["classToLog_StringProperty"] = classToLog.StringProperty, + ["classToLog_SimplifiedNullableIntProperty"] = null, + ["classToLog_ExplicitNullableIntProperty"] = classToLog.ExplicitNullableIntProperty.ToString(), + ["classToLog_GetOnlyProperty"] = classToLog.GetOnlyProperty.ToString(CultureInfo.InvariantCulture), + ["classToLog_VirtualPropertyBase"] = classToLog.VirtualPropertyBase, + ["classToLog_NonVirtualPropertyBase"] = classToLog.NonVirtualPropertyBase, + ["classToLog_TransitivePropertyArray"] = LogMethodHelper.Stringify(classToLog.TransitivePropertyArray), + ["classToLog_TransitiveProperty_TransitiveNumberProp"] + = classToLog.TransitiveProperty.TransitiveNumberProp.ToString(CultureInfo.InvariantCulture), + + ["classToLog_TransitiveProperty_TransitiveStringProp"] = classToLog.TransitiveProperty.TransitiveStringProp, + ["classToLog_TransitiveProperty_InnerTransitiveProperty_IntegerProperty"] + = classToLog.TransitiveProperty.InnerTransitiveProperty.IntegerProperty.ToString(CultureInfo.InvariantCulture), + ["classToLog_TransitiveProperty_InnerTransitiveProperty_DateTimeProperty"] + = classToLog.TransitiveProperty.InnerTransitiveProperty.DateTimeProperty.ToString(CultureInfo.InvariantCulture), + + ["classToLog_AnotherTransitiveProperty_IntegerProperty"] = null, // Since AnotherTransitiveProperty is null + ["classToLog_StringPropertyBase"] = classToLog.StringPropertyBase, + ["classToLog_VirtualInterimProperty"] = classToLog.VirtualInterimProperty.ToInvariantString(), + ["classToLog_InterimProperty"] = classToLog.InterimProperty.ToString(CultureInfo.InvariantCulture), + ["classToLog_TransitiveProperty_TransitiveDerivedProp"] = classToLog.TransitiveProperty.TransitiveDerivedProp.ToInvariantString(), + ["classToLog_TransitiveProperty_TransitiveVirtualProp"] = classToLog.TransitiveProperty.TransitiveVirtualProp.ToInvariantString(), + ["classToLog_TransitiveProperty_TransitiveGenericProp_GenericProp"] + = classToLog.TransitiveProperty.TransitiveGenericProp.GenericProp, + + ["classToLog_PropertyOfGenerics_GenericProp"] = classToLog.PropertyOfGenerics.GenericProp.ToInvariantString(), + ["classToLog_CustomStructProperty_LongProperty"] = classToLog.CustomStructProperty.LongProperty.ToInvariantString(), + ["classToLog_CustomStructProperty_TransitiveStructProperty_DateTimeOffsetProperty"] + = classToLog.CustomStructProperty.TransitiveStructProperty.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture), + + ["classToLog_CustomStructProperty_NullableTransitiveStructProperty_DateTimeOffsetProperty"] + = classToLog.CustomStructProperty.NullableTransitiveStructProperty?.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture), + + ["classToLog_CustomStructProperty_NullableTransitiveStructProperty2_DateTimeOffsetProperty"] + = classToLog.CustomStructProperty.NullableTransitiveStructProperty2?.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture), + + ["classToLog_CustomStructNullableProperty_LongProperty"] = classToLog.CustomStructNullableProperty?.LongProperty.ToInvariantString(), + ["classToLog_CustomStructNullableProperty_TransitiveStructProperty_DateTimeOffsetProperty"] + = classToLog.CustomStructNullableProperty?.TransitiveStructProperty.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture), + + ["classToLog_CustomStructNullableProperty_NullableTransitiveStructProperty_DateTimeOffsetProperty"] + = classToLog.CustomStructNullableProperty?.NullableTransitiveStructProperty?.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture), + + ["classToLog_CustomStructNullableProperty_NullableTransitiveStructProperty2_DateTimeOffsetProperty"] + = classToLog.CustomStructNullableProperty?.NullableTransitiveStructProperty2?.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture), + + ["classToLog_CustomStructNullableProperty2_LongProperty"] = classToLog.CustomStructNullableProperty2?.LongProperty.ToInvariantString(), + ["classToLog_CustomStructNullableProperty2_TransitiveStructProperty_DateTimeOffsetProperty"] + = classToLog.CustomStructNullableProperty2?.TransitiveStructProperty.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture), + + ["classToLog_CustomStructNullableProperty2_NullableTransitiveStructProperty_DateTimeOffsetProperty"] + = classToLog.CustomStructNullableProperty2?.NullableTransitiveStructProperty?.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture), + + ["classToLog_CustomStructNullableProperty2_NullableTransitiveStructProperty2_DateTimeOffsetProperty"] + = classToLog.CustomStructNullableProperty2?.NullableTransitiveStructProperty2?.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture), + + ["{OriginalFormat}"] = "Only {classToLog_StringProperty_1} as param" + }; + + _logger.Collector.LatestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void LogPropertiesInTemplateTest() + { + var classToLog = new ClassAsParam { MyProperty = 0 }; + LogMethodTwoParams(_logger, StringProperty, classToLog); + Assert.Equal(1, _logger.Collector.Count); + Assert.Equal(LogLevel.Information, _logger.Collector.LatestRecord.Level); + Assert.Equal($"Both {StringProperty} and {classToLog} as params", _logger.Collector.LatestRecord.Message); + + var expectedState = new Dictionary + { + ["StringProperty"] = StringProperty, + ["ComplexParam"] = classToLog.ToString(), + ["complexParam_MyProperty"] = classToLog.MyProperty.ToInvariantString(), + ["{OriginalFormat}"] = "Both {StringProperty} and {ComplexParam} as params" + }; + + _logger.Collector.LatestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void LogPropertiesNonStaticClassTest() + { + const string StringParamValue = "Value for a string"; + + var classToLog = new ClassToLog { MyIntProperty = 0 }; + new NonStaticTestClass(_logger, null!).LogProperties(StringParamValue, classToLog); + + Assert.Equal(1, _logger.Collector.Count); + var latestRecord = _logger.Collector.LatestRecord; + Assert.Equal(LogLevel.Information, latestRecord.Level); + Assert.Equal($"LogProperties: {StringParamValue}", latestRecord.Message); + + var expectedState = new Dictionary + { + ["P0"] = StringParamValue, + ["p1_MyIntProperty"] = classToLog.MyIntProperty.ToInvariantString(), + ["p1_MyStringProperty"] = classToLog.MyStringProperty, + ["{OriginalFormat}"] = "LogProperties: {P0}" + }; + + latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void LogPropertyTestCustomStruct() + { + var transitiveStruct = new MyTransitiveStruct { DateTimeOffsetProperty = DateTimeOffset.MinValue }; + var structToLog = new MyCustomStruct { LongProperty = 1L, NullableTransitiveStructProperty2 = transitiveStruct }; + LogMethodStruct(_logger, structToLog); + + Assert.Equal(1, _logger.Collector.Count); + var latestRecord = _logger.Collector.LatestRecord; + Assert.Null(latestRecord.Exception); + Assert.Equal("Testing non-nullable struct here...", latestRecord.Message); + + var expectedState = new Dictionary + { + ["structParam_LongProperty"] = structToLog.LongProperty.ToInvariantString(), + ["structParam_TransitiveStructProperty_DateTimeOffsetProperty"] + = structToLog.TransitiveStructProperty.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture), + + ["structParam_NullableTransitiveStructProperty_DateTimeOffsetProperty"] + = structToLog.NullableTransitiveStructProperty?.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture), + + ["structParam_NullableTransitiveStructProperty2_DateTimeOffsetProperty"] + = structToLog.NullableTransitiveStructProperty2.Value.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture), + + ["{OriginalFormat}"] = "Testing non-nullable struct here..." + }; + + latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void LogPropertyTestCustomNullableStruct() + { + var transitiveStruct = new MyTransitiveStruct { DateTimeOffsetProperty = DateTimeOffset.MinValue }; + MyCustomStruct? structToLog = new MyCustomStruct { LongProperty = 1L, NullableTransitiveStructProperty = transitiveStruct }; + LogMethodNullableStruct(_logger, in structToLog); + + Assert.Equal(1, _logger.Collector.Count); + var latestRecord = _logger.Collector.LatestRecord; + Assert.Null(latestRecord.Exception); + Assert.Equal("Testing nullable struct here...", latestRecord.Message); + + var expectedState = new Dictionary + { + ["structParam_LongProperty"] = structToLog.Value.LongProperty.ToInvariantString(), + ["structParam_TransitiveStructProperty_DateTimeOffsetProperty"] + = structToLog.Value.TransitiveStructProperty.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture), + + ["structParam_NullableTransitiveStructProperty_DateTimeOffsetProperty"] + = structToLog.Value.NullableTransitiveStructProperty.Value.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture), + + ["structParam_NullableTransitiveStructProperty2_DateTimeOffsetProperty"] + = structToLog.Value.NullableTransitiveStructProperty2?.DateTimeOffsetProperty.ToString(CultureInfo.InvariantCulture), + + ["{OriginalFormat}"] = "Testing nullable struct here..." + }; + + latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void LogPropertyTestCustomExplicitNullableStruct() + { + LogMethodExplicitNullableStruct(_logger, null); + + Assert.Equal(1, _logger.Collector.Count); + + var latestRecord = _logger.Collector.LatestRecord; + Assert.Null(latestRecord.Exception); + Assert.Equal("Testing explicit nullable struct here...", latestRecord.Message); + + var expectedState = new Dictionary + { + ["structParam_LongProperty"] = null, + ["structParam_TransitiveStructProperty_DateTimeOffsetProperty"] = null, + ["structParam_NullableTransitiveStructProperty_DateTimeOffsetProperty"] = null, + ["structParam_NullableTransitiveStructProperty2_DateTimeOffsetProperty"] = null, + ["{OriginalFormat}"] = "Testing explicit nullable struct here..." + }; + + latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void LogPropertiesDefaultAttrCtor() + { + var classToLog = new ClassAsParam { MyProperty = 0 }; + + LogMethodDefaultAttrCtor(_logger, LogLevel.Critical, classToLog); + + Assert.Equal(1, _logger.Collector.Count); + var latestRecord = _logger.Collector.LatestRecord; + Assert.Null(latestRecord.Exception); + Assert.Equal(0, latestRecord.Id.Id); + Assert.Equal(LogLevel.Critical, latestRecord.Level); + Assert.Equal(string.Empty, latestRecord.Message); + + var expectedState = new Dictionary + { + ["complexParam_MyProperty"] = classToLog.MyProperty.ToInvariantString() + }; + + latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } + + [Fact] + public void LogPropertiesInterfaceArgument() + { + var classToLog = new MyInterfaceImpl + { + IntProperty = 100, + ClassStringProperty = "test string", // won't get logged + TransitiveProp = new LeafTransitiveBaseClass { IntegerProperty = 500 } + }; + + LogMethodInterfaceArg(_logger, classToLog); + + Assert.Equal(1, _logger.Collector.Count); + var latestRecord = _logger.Collector.LatestRecord; + Assert.Null(latestRecord.Exception); + Assert.Equal(5, latestRecord.Id.Id); + Assert.Equal(LogLevel.Information, latestRecord.Level); + Assert.Equal("Testing interface-typed argument here...", latestRecord.Message); + + var expectedState = new Dictionary + { + ["complexParam_IntProperty"] = classToLog.IntProperty.ToInvariantString(), + ["complexParam_TransitiveProp_IntegerProperty"] = classToLog.TransitiveProp.IntegerProperty.ToInvariantString(), + ["{OriginalFormat}"] = "Testing interface-typed argument here..." + }; + + latestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Generated/Common/StarRedactor.cs b/test/Generators/Microsoft.Gen.Logging/Generated/Common/StarRedactor.cs new file mode 100644 index 0000000000..7bc3e9708b --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Generated/Common/StarRedactor.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Compliance.Redaction; + +namespace TestClasses +{ + internal class StarRedactor : Redactor + { + public override int GetRedactedLength(ReadOnlySpan source) + { + return source!.ToString()!.Length; + } + + public override int Redact(ReadOnlySpan source, Span destination) + { + var len = source!.ToString()!.Length; + var redacted = new string('*', len); + redacted.AsSpan().CopyTo(destination); + return len; + } + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Generated/Common/StarRedactorProvider.cs b/test/Generators/Microsoft.Gen.Logging/Generated/Common/StarRedactorProvider.cs new file mode 100644 index 0000000000..a314fb2e50 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Generated/Common/StarRedactorProvider.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; + +namespace TestClasses +{ + internal class StarRedactorProvider : IRedactorProvider + { + public Redactor GetRedactor(DataClassification dataClass) => new StarRedactor(); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Generated/Directory.Build.props b/test/Generators/Microsoft.Gen.Logging/Generated/Directory.Build.props new file mode 100644 index 0000000000..b5ba031317 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Generated/Directory.Build.props @@ -0,0 +1,29 @@ + + + + + Microsoft.Gen.Logging.Test + Tests for code generated by Gen.Logging. + + + + $(NetCoreTargetFrameworks) + $(NetCoreTargetFrameworks)$(ConditionalNet462) + true + true + true + $(NoWarn);IDE0161;S1144 + + + + + + + + + + + + + + diff --git a/test/Generators/Microsoft.Gen.Logging/Generated/Roslyn3.8/Microsoft.Gen.Logging.Roslyn3.8.Generated.Tests.csproj b/test/Generators/Microsoft.Gen.Logging/Generated/Roslyn3.8/Microsoft.Gen.Logging.Roslyn3.8.Generated.Tests.csproj new file mode 100644 index 0000000000..eac2eac217 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Generated/Roslyn3.8/Microsoft.Gen.Logging.Roslyn3.8.Generated.Tests.csproj @@ -0,0 +1,5 @@ + + + 3.8 + + diff --git a/test/Generators/Microsoft.Gen.Logging/Generated/Roslyn4.0/Microsoft.Gen.Logging.Roslyn4.0.Generated.Tests.csproj b/test/Generators/Microsoft.Gen.Logging/Generated/Roslyn4.0/Microsoft.Gen.Logging.Roslyn4.0.Generated.Tests.csproj new file mode 100644 index 0000000000..0cd2e6b265 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Generated/Roslyn4.0/Microsoft.Gen.Logging.Roslyn4.0.Generated.Tests.csproj @@ -0,0 +1,6 @@ + + + 4.0 + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/ArgTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/ArgTestExtensions.cs new file mode 100644 index 0000000000..936b6ac507 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/ArgTestExtensions.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class ArgTestExtensions + { + [LogMethod(0, LogLevel.Error, "M1")] + public static partial void Method1(ILogger logger); + + [LogMethod(1, LogLevel.Error, "M2 {p1}")] + public static partial void Method2(ILogger logger, string p1); + + [LogMethod(2, LogLevel.Error, "M3 {p1} {p2}")] + public static partial void Method3(ILogger logger, string p1, int p2); + + [LogMethod(3, LogLevel.Error, "M4")] + public static partial void Method4(ILogger logger, InvalidOperationException p1); + + [LogMethod(4, LogLevel.Error, "M5 {p2}")] + public static partial void Method5(ILogger logger, System.InvalidOperationException p1, System.InvalidOperationException p2); + + [LogMethod(5, LogLevel.Error, "M6 {p2}")] + public static partial void Method6(ILogger logger, System.InvalidOperationException p1, int p2); + + [LogMethod(6, LogLevel.Error, "M7 {p1}")] + public static partial void Method7(ILogger logger, int p1, System.InvalidOperationException p2); + +#pragma warning disable S107 // Methods should not have too many parameters + [LogMethod(7, LogLevel.Error, "M8{p1}{p2}{p3}{p4}{p5}{p6}{p7}")] + public static partial void Method8(ILogger logger, int p1, int p2, int p3, int p4, int p5, int p6, int p7); + + [LogMethod(8, LogLevel.Error, "M9 {p1} {p2} {p3} {p4} {p5} {p6} {p7}")] + public static partial void Method9(ILogger logger, int p1, int p2, int p3, int p4, int p5, int p6, int p7); +#pragma warning restore S107 // Methods should not have too many parameters + + [LogMethod(9, LogLevel.Error, "M10{p1}")] + public static partial void Method10(ILogger logger, int p1); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/AtSymbolsTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/AtSymbolsTestExtensions.cs new file mode 100644 index 0000000000..178adefefb --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/AtSymbolsTestExtensions.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class AtSymbolsTestExtensions + { + [LogMethod(0, LogLevel.Information, "M0 {@event}")] + internal static partial void M0(ILogger logger, string @event); + + [LogMethod(1, LogLevel.Information, "M1 {@event}")] + internal static partial void M1(ILogger logger, IRedactorProvider redactorProvider, [PrivateData] string @event); + + [LogMethod(int.MaxValue, "M2 {Event}")] + internal static partial void M2(ILogger logger, LogLevel level, string @event); + + // And support with property logging + [LogMethod(3, "M3")] + internal static partial void M3(ILogger logger, LogLevel level, [LogProperties] ClassToLog @event); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/AttributeTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/AttributeTestExtensions.cs new file mode 100644 index 0000000000..354c212791 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/AttributeTestExtensions.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class AttributeTestExtensions + { + [LogMethod(0, LogLevel.Debug, "M0 {p0}")] + public static partial void M0(ILogger logger, [In] string p0); + + [LogMethod(1, LogLevel.Debug, "M1 {p0} {p1}")] + public static partial void M1(ILogger logger, IRedactorProvider redactorProvider, [PrivateData] string p0, string p1); + + [LogMethod(2, LogLevel.Debug, "M2 {p0} {p1}")] + public static partial void M2(ILogger logger, IRedactorProvider redactorProvider, [PrivateData] string p0, [In] string p1); + + [LogMethod(3, LogLevel.Debug, "M3 {p0} {p1} {p2} {p3}")] + public static partial void M3( + ILogger logger, + IRedactorProvider redactorProvider, + [PrivateData] string p0, + [PrivateData] string p1, + [PrivateData] string p2, + [PrivateData] string p3); + + [LogMethod(4, LogLevel.Debug, "M4 {p0} {p1} {p2}")] + public static partial void M4( + ILogger logger, + IRedactorProvider redactorProvider, + [PrivateData] string p0, + [PrivateData] string p1, + [PrivateData] string p2); + + [LogMethod(5, LogLevel.Debug, "M5 {p0} {p1} {p2} {p3} {p4} {p5} {p6} {p7} {p8} {p9} {p10}")] + [SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", Justification = "Testing.")] + public static partial void M5( + ILogger logger, + IRedactorProvider redactorProvider, + [PrivateData] string p0, + [PrivateData] string p1, + [PrivateData] string p2, + [PrivateData] string p3, + [PrivateData] string p4, + [PrivateData] string p5, + [PrivateData] string p6, + [PrivateData] string p7, + [PublicData] string p8, + [PublicData] string p9, + [PublicData] string p10); + + // Parameterless ctor: + [LogMethod] + public static partial void M6(ILogger logger, LogLevel level, IRedactorProvider redactorProvider, + [PrivateData] string p0, string p1); + + [LogMethod] + public static partial void M7(ILogger logger, LogLevel level, IRedactorProvider redactorProvider, + [PrivateData] string p0, string p1); + + [LogMethod(8, LogLevel.Debug, "M8 {p0}")] + public static partial void M8(ILogger logger, IRedactorProvider redactorProvider, [PrivateData] int p0); + + [LogMethod(9, LogLevel.Debug, "M9 {p0}")] + public static partial void M9(ILogger logger, IRedactorProvider redactorProvider, [PrivateData] CustomToStringTestClass p0); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/CollectionTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/CollectionTestExtensions.cs new file mode 100644 index 0000000000..af74de1c62 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/CollectionTestExtensions.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class CollectionTestExtensions + { + [LogMethod(0, LogLevel.Error, "M0")] + public static partial void M0(ILogger logger); + + [LogMethod(1, LogLevel.Error, "M1{p0}")] + public static partial void M1(ILogger logger, int p0); + + [LogMethod(2, LogLevel.Error, "M2{p0}{p1}")] + public static partial void M2(ILogger logger, int p0, int p1); + + [LogMethod(3, LogLevel.Error, "M3{p0}{p1}{p2}")] + public static partial void M3(ILogger logger, int p0, int p1, int p2); + + [LogMethod(4, LogLevel.Error, "M4{p0}{p1}{p2}{p3}")] + public static partial void M4(ILogger logger, int p0, int p1, int p2, int p3); + + [LogMethod(5, LogLevel.Error, "M5{p0}{p1}{p2}{p3}{p4}")] + public static partial void M5(ILogger logger, int p0, int p1, int p2, int p3, int p4); + + [LogMethod(6, LogLevel.Error, "M6{p0}{p1}{p2}{p3}{p4}{p5}")] + public static partial void M6(ILogger logger, int p0, int p1, int p2, int p3, int p4, int p5); + +#pragma warning disable S107 // Methods should not have too many parameters + [LogMethod(7, LogLevel.Error, "M7{p0}{p1}{p2}{p3}{p4}{p5}{p6}")] + public static partial void M7(ILogger logger, int p0, int p1, int p2, int p3, int p4, int p5, int p6); + + [LogMethod(8, LogLevel.Error, "M8{p0}{p1}{p2}{p3}{p4}{p5}{p6}{p7}")] + public static partial void M8(ILogger logger, int p0, int p1, int p2, int p3, int p4, int p5, int p6, int p7); +#pragma warning restore S107 // Methods should not have too many parameters + + [LogMethod(9, "M8{p0}{p1}")] + public static partial void M9(ILogger logger, LogLevel level, int p0, System.Exception ex, int p1); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/ConstraintsTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/ConstraintsTestExtensions.cs new file mode 100644 index 0000000000..a09f48e071 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/ConstraintsTestExtensions.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +#pragma warning disable SA1402 // File may only contain a single type +#pragma warning disable S1186 // Methods should not be empty + +namespace TestClasses +{ + internal static partial class ConstraintsTestExtensions + where T : class + { + [LogMethod(0, LogLevel.Debug, "M0{p0}")] + public static partial void M0(ILogger logger, int p0); + + public static void Foo(T _) + { + } + } + + internal static partial class ConstraintsTestExtensions1 + where T : struct + { + [LogMethod(0, LogLevel.Debug, "M0{p0}")] + public static partial void M0(ILogger logger, int p0); + + public static void Foo(T _) + { + } + } + + internal static partial class ConstraintsTestExtensions2 + where T : unmanaged + { + [LogMethod(0, LogLevel.Debug, "M0{p0}")] + public static partial void M0(ILogger logger, int p0); + + public static void Foo(T _) + { + } + } + + internal static partial class ConstraintsTestExtensions3 + where T : new() + { + [LogMethod(0, LogLevel.Debug, "M0{p0}")] + public static partial void M0(ILogger logger, int p0); + + public static void Foo(T _) + { + } + } + + internal static partial class ConstraintsTestExtensions4 + where T : Attribute + { + [LogMethod(0, LogLevel.Debug, "M0{p0}")] + public static partial void M0(ILogger logger, int p0); + + public static void Foo(T _) + { + } + } + + internal static partial class ConstraintsTestExtensions5 + where T : notnull + { + [LogMethod(0, LogLevel.Debug, "M0{p0}")] + public static partial void M0(ILogger logger, int p0); + + public static void Foo(T _) + { + } + } + + internal static partial class ConstraintsTestExtensionsMultiple + where T : class, new() + { + [LogMethod(0, LogLevel.Debug, "M0{p0}")] + public static partial void M0(ILogger logger, int p0); + + public static void Foo(T _) + { + } + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/ConstructorVariationsTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/ConstructorVariationsTestExtensions.cs new file mode 100644 index 0000000000..c319e7c014 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/ConstructorVariationsTestExtensions.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class ConstructorVariationsTestExtensions + { + [LogMethod(0, LogLevel.Debug, "M0 {p0}")] + public static partial void M0(ILogger logger, string p0); + + [LogMethod(1, "M1 {p0}")] + public static partial void M1(ILogger logger, LogLevel level, string p0); + + [LogMethod(2, LogLevel.Debug)] + public static partial void M2(ILogger logger, string p0); + + [LogMethod(3)] + public static partial void M3(ILogger logger, LogLevel level, string p0); + + [LogMethod(LogLevel.Debug, "M4 {p0}")] + public static partial void M4(ILogger logger, string p0); + + [LogMethod("M5 {p0}")] + public static partial void M5(ILogger logger, LogLevel level, string p0); + + [LogMethod(LogLevel.Debug)] + public static partial void M6(ILogger logger, string p0); + + [LogMethod] + public static partial void M7(ILogger logger, LogLevel level, string p0); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/CustomToStringTestClass.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/CustomToStringTestClass.cs new file mode 100644 index 0000000000..ce3cfa26e1 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/CustomToStringTestClass.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace TestClasses +{ + public class CustomToStringTestClass + { + public override string ToString() => "Test"; + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/EnumerableTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/EnumerableTestExtensions.cs new file mode 100644 index 0000000000..ff648f1a9f --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/EnumerableTestExtensions.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class EnumerableTestExtensions + { + [LogMethod(0, LogLevel.Error, "M0")] + public static partial void M0(ILogger logger); + + [LogMethod(1, LogLevel.Error, "M1{p0}")] + public static partial void M1(ILogger logger, IEnumerable p0); + + [LogMethod(2, LogLevel.Error, "M2{p0}{p1}")] + public static partial void M2(ILogger logger, int p0, IEnumerable p1); + + [LogMethod(3, LogLevel.Error, "M3{p0}{p1}{p2}")] + public static partial void M3(ILogger logger, int p0, IEnumerable p1, int p2); + + [LogMethod(4, LogLevel.Error, "M4{p0}{p1}{p2}{p3}")] + public static partial void M4(ILogger logger, int p0, IEnumerable p1, int p2, int p3); + + [LogMethod(5, LogLevel.Error, "M5{p0}{p1}{p2}{p3}{p4}")] + public static partial void M5(ILogger logger, int p0, IEnumerable p1, int p2, int p3, int p4); + + [LogMethod(6, LogLevel.Error, "M6{p0}{p1}{p2}{p3}{p4}{p5}")] + public static partial void M6(ILogger logger, int p0, IEnumerable p1, int p2, int p3, int p4, int p5); + +#pragma warning disable S107 // Methods should not have too many parameters + + [LogMethod(7, LogLevel.Error, "M7{p0}{p1}{p2}{p3}{p4}{p5}{p6}")] + public static partial void M7(ILogger logger, int p0, IEnumerable p1, int p2, int p3, int p4, int p5, int p6); + + [LogMethod(8, LogLevel.Error, "M8{p0}{p1}{p2}{p3}{p4}{p5}{p6}{p7}")] + public static partial void M8(ILogger logger, int p0, IEnumerable p1, int p2, int p3, int p4, int p5, int p6, int p7); + + [LogMethod(9, LogLevel.Error, "M9{p0}{p1}{p2}{p3}{p4}{p5}{p6}{p7}{p8}")] + public static partial void M9(ILogger logger, int p0, IEnumerable p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8); + + [LogMethod(10, LogLevel.Error, "M10{p1}{p2}{p3}")] + public static partial void M10(ILogger logger, IEnumerable p1, int[] p2, Dictionary p3); + + [LogMethod(11, LogLevel.Error, "M11{p1}")] + public static partial void M11(ILogger logger, IEnumerable? p1); + + [LogMethod(12, LogLevel.Error, "M12{@class}")] + public static partial void M12(ILogger logger, IEnumerable? @class); + + [LogMethod(13, LogLevel.Error, "M13{p1}")] + public static partial void M13(ILogger logger, StructEnumerable p1); + + [LogMethod(14, LogLevel.Error, "M14{p1}")] + public static partial void M14(ILogger logger, StructEnumerable? p1); + } + + public readonly struct StructEnumerable : IEnumerable + { + private static readonly List _numbers = new() { 1, 2, 3 }; + public IEnumerator GetEnumerator() => _numbers.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _numbers.GetEnumerator(); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/EventNameTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/EventNameTestExtensions.cs new file mode 100644 index 0000000000..b8ffe6321e --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/EventNameTestExtensions.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class EventNameTestExtensions + { + [LogMethod(0, LogLevel.Trace, "M0", EventName = "CustomEventName")] + public static partial void M0(ILogger logger); + + [LogMethod(EventName = "M1_Event")] + public static partial void M1(LogLevel level, ILogger logger, string p0); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/ExceptionTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/ExceptionTestExtensions.cs new file mode 100644 index 0000000000..cca3a0cca0 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/ExceptionTestExtensions.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class ExceptionTestExtensions + { + [LogMethod(0, LogLevel.Trace, "M0 {ex2}")] + public static partial void M0(ILogger logger, Exception ex1, Exception ex2); + + [LogMethod(1, LogLevel.Debug, "M1 {ex2}")] + public static partial void M1(Exception ex1, ILogger logger, Exception ex2); + +#pragma warning disable R9G012 // Don't include a template for ex in the logging message + [LogMethod(2, LogLevel.Debug, "M2 {arg1}: {ex}")] + public static partial void M2(ILogger logger, string arg1, Exception ex); +#pragma warning restore R9G012 + + [LogMethod] + public static partial void M3(Exception ex, ILogger logger, LogLevel level); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/FormattableTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/FormattableTestExtensions.cs new file mode 100644 index 0000000000..b84b8a2297 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/FormattableTestExtensions.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class FormattableTestExtensions + { + [LogMethod(0, LogLevel.Error, "Method1 {p1}")] + public static partial void Method1(ILogger logger, Formattable p1); + + [LogMethod(1, LogLevel.Error, "Method2")] + public static partial void Method2(ILogger logger, [LogProperties] ComplexObj p1); + + internal class Formattable : IFormattable + { + public string ToString(string? format, IFormatProvider? formatProvider) + { + return "Formatted!"; + } + } + + internal class ComplexObj + { + public Formattable P1 { get; } = new Formattable(); + } + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/InParameterTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/InParameterTestExtensions.cs new file mode 100644 index 0000000000..373b2c1f22 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/InParameterTestExtensions.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class InParameterTestExtensions + { + internal struct S + { + public override string ToString() => "Hello from S"; + } + + [LogMethod(0, LogLevel.Information, "M0 {s}")] + internal static partial void M0(ILogger logger, in S s); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/InvariantTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/InvariantTestExtensions.cs new file mode 100644 index 0000000000..cedb60da43 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/InvariantTestExtensions.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class InvariantTestExtensions + { + [LogMethod(0, LogLevel.Debug, "M0 {p0}")] + public static partial void M0(ILogger logger, DateTime p0); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/LevelTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/LevelTestExtensions.cs new file mode 100644 index 0000000000..ef948c3306 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/LevelTestExtensions.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class LevelTestExtensions + { + [LogMethod(0, LogLevel.Trace, "M0")] + public static partial void M0(ILogger logger); + + [LogMethod(1, LogLevel.Debug, "M1")] + public static partial void M1(ILogger logger); + + [LogMethod(2, LogLevel.Information, "M2")] + public static partial void M2(ILogger logger); + + [LogMethod(3, LogLevel.Warning, "M3")] + public static partial void M3(ILogger logger); + + [LogMethod(4, LogLevel.Error, "M4")] + public static partial void M4(ILogger logger); + + [LogMethod(5, LogLevel.Critical, "M5")] + public static partial void M5(ILogger logger); + + [LogMethod(6, LogLevel.None, "M6")] + public static partial void M6(ILogger logger); + + [LogMethod(7, (LogLevel)42, "M7")] + public static partial void M7(ILogger logger); + + [LogMethod(8, "M8")] + public static partial void M8(ILogger logger, LogLevel level); + + [LogMethod(9, "M9")] + public static partial void M9(LogLevel level, ILogger logger); + +#pragma warning disable R9G001 // Don't include a template for level in the logging message + [LogMethod(10, "M10 {level}")] + public static partial void M10(ILogger logger, LogLevel level); +#pragma warning restore R9G001 + +#pragma warning disable R9G017 // Don't include a template for logger in the logging message + [LogMethod(11, "M11 {logger}")] + public static partial void M11(ILogger logger, LogLevel level); +#pragma warning restore R9G017 + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesExtensions.cs new file mode 100644 index 0000000000..54b17987e9 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesExtensions.cs @@ -0,0 +1,226 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Test code")] + [SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Test code")] + [SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "Test code")] + [SuppressMessage("Major Code Smell", "S2376:Write-only properties should not be used", Justification = "Test code")] + internal static partial class LogPropertiesExtensions + { + public delegate void TestDelegate(); + + internal class MyBaseClass + { + public virtual string VirtualPropertyBase => "Hello from MyBaseClass!"; // Not supposed to be logged (overridden in both MyDerivedClass and MyInterimClass) + + public string? NonVirtualPropertyBase { get; set; } + + public string? StringPropertyBase { get; set; } + + protected int ProtectedPopertyBase { get; set; } // Not supposed to be logged (protected) + + private int PrivatePopertyBase { get; set; } // Not supposed to be logged (private) + + public Action? ActionBase { get; set; } // Not supposed to be logged (delegate type) + + private Func? FuncBase { get; set; } // Not supposed to be logged (delegate type) + + private TestDelegate? DelegateBase { get; set; } // Not supposed to be logged (delegate type) + } + + internal class MyInterimClass : MyBaseClass + { + public virtual int VirtualInterimProperty { get; set; } // Not supposed to be logged (overridden in MyDerivedClass) + + public long InterimProperty { get; set; } + + public override string VirtualPropertyBase => "Hello from MyInterimClass!"; // Not supposed to be logged (overridden in MyDerivedClass) + } + + public class MyTransitiveBaseClass + { + public decimal TransitiveNumberProp { get; set; } = decimal.One; + + public string? TransitiveStringProp { get; set; } + + public virtual int TransitiveVirtualProp { get; set; } // Not supposed to be logged (overridden in MyTransitiveDerivedClass) + + public GenericClass TransitiveGenericProp => new() + { + GenericProp = "Hello from MyTransitiveBaseClass!" + }; + + internal double TransitiveInternalProp { get; set; } // Not supposed to be logged (internal) + + public double TransitiveField = double.PositiveInfinity; // Not supposed to be logged (field) + + private static decimal PrivateProperty => decimal.MinusOne; // Not supposed to be logged (private & static) + + public LeafTransitiveDerivedClass? InnerTransitiveProperty { get; set; } + } + + public class MyTransitiveDerivedClass : MyTransitiveBaseClass + { + public override int TransitiveVirtualProp { get; set; } // Overrides MyTransitiveBaseClass.TransitiveVirtualProp + + public int TransitiveDerivedProp { get; set; } + } + + public class LeafTransitiveBaseClass + { + public int IntegerProperty { get; set; } = int.MaxValue; + } + + public class LeafTransitiveDerivedClass : LeafTransitiveBaseClass + { + public DateTime DateTimeProperty { get; set; } = DateTime.MaxValue; + } + + public class GenericClass + { + public T? GenericProp { get; set; } + } + + public struct MyCustomStruct + { + public long LongProperty { get; set; } = long.MaxValue; + +#pragma warning disable CA1805 // Do not initialize unnecessarily + public MyTransitiveStruct TransitiveStructProperty { get; set; } = default; + public MyTransitiveStruct? NullableTransitiveStructProperty { get; set; } = default; +#pragma warning restore CA1805 // Do not initialize unnecessarily + + [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1125:Use shorthand for nullable types", Justification = "Testing Nullable")] + public Nullable NullableTransitiveStructProperty2 { get; set; } = default; + + public MyCustomStruct(object _) + { + } + } + + public struct MyTransitiveStruct + { + public DateTimeOffset DateTimeOffsetProperty { get; set; } = DateTimeOffset.UtcNow; + + public MyTransitiveStruct(object _) + { + } + } + + internal class MyDerivedClass : MyInterimClass + { + public static int StaticNumberProperty { get; set; } = ushort.MaxValue; // Not supposed to be logged (static) + + public string? StringProperty { get; set; } + + public int? SimplifiedNullableIntProperty { get; set; } + + [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1125:Use shorthand for nullable types", Justification = "Test code")] + public Nullable ExplicitNullableIntProperty { get; set; } + + // Not supposed to be logged (private getter) + public string PrivateGetStringProperty { private get; set; } = nameof(PrivateGetStringProperty); + + public DateTime GetOnlyProperty => DateTime.MaxValue; + + public DateTime SetOnlyProperty // Not supposed to be logged (write-only property) + { + set => _ = value; // No-op + } + + private static decimal PrivateProperty => decimal.MinusOne; // Not supposed to be logged (private & static) + + internal string InternalProperty { get; set; } = nameof(InternalProperty); // Not supposed to be logged (internal) + + public string PublicField = nameof(PublicField); // Not supposed to be logged (field) + + private readonly double _privateField; // Not supposed to be logged (private & field) + + public override string VirtualPropertyBase => "Hello from MyDerivedClass!"; // Overrides MyBaseClass.VirtualPropertyBase + + public override int VirtualInterimProperty // Overrides MyInterimClass.VirtualInterimProperty + { + get => base.VirtualInterimProperty + 10; + set => base.VirtualInterimProperty = value - 10; + } + + public int[] TransitivePropertyArray { get; set; } = Array.Empty(); + + public MyTransitiveDerivedClass? TransitiveProperty { get; set; } + + public LeafTransitiveBaseClass? AnotherTransitiveProperty { get; set; } + + public GenericClass? PropertyOfGenerics { get; set; } + + public MyCustomStruct CustomStructProperty { get; set; } + + public MyCustomStruct? CustomStructNullableProperty { get; set; } + + [LogPropertyIgnore] + public int IgnoredProp { get; set; } + + [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1125:Use shorthand for nullable types", Justification = "Testing Nullable")] + public Nullable CustomStructNullableProperty2 { get; set; } + + public MyDerivedClass(double privateFieldValue) + { + _privateField = privateFieldValue; + } + } + + internal interface IMyInterface + { + public int IntProperty { get; set; } + + public LeafTransitiveBaseClass? TransitiveProp { get; set; } + } + + internal sealed class MyInterfaceImpl : IMyInterface + { + public int IntProperty { get; set; } + + public string? ClassStringProperty { get; set; } + + public LeafTransitiveBaseClass? TransitiveProp { get; set; } + } + + [LogMethod(0, LogLevel.Debug, "Only {classToLog_StringProperty_1} as param")] + public static partial void LogFunc(ILogger logger, string classToLog_StringProperty_1, [LogProperties] MyDerivedClass classToLog); + + internal class ClassAsParam + { + public int MyProperty { get; set; } + + public override string ToString() + => DateTime + .Parse("2021-11-15", CultureInfo.InvariantCulture) + .ToString("D", CultureInfo.InvariantCulture); + } + + [LogMethod(1, LogLevel.Information, "Both {StringProperty} and {ComplexParam} as params")] + public static partial void LogMethodTwoParams(ILogger logger, string StringProperty, [LogProperties] ClassAsParam? complexParam); + + [LogMethod(2, LogLevel.Information, "Testing non-nullable struct here...")] + public static partial void LogMethodStruct(ILogger logger, [LogProperties] MyCustomStruct structParam); + + [LogMethod(3, LogLevel.Information, "Testing nullable struct here...")] + public static partial void LogMethodNullableStruct(ILogger logger, [LogProperties] in MyCustomStruct? structParam); + + [LogMethod(4, LogLevel.Information, "Testing explicit nullable struct here...")] + public static partial void LogMethodExplicitNullableStruct(ILogger logger, [LogProperties] Nullable structParam); + + [LogMethod] + public static partial void LogMethodDefaultAttrCtor(ILogger logger, LogLevel level, [LogProperties] ClassAsParam? complexParam); + + [LogMethod(5, LogLevel.Information, "Testing interface-typed argument here...")] + public static partial void LogMethodInterfaceArg(ILogger logger, [LogProperties] IMyInterface complexParam); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesNullHandlingExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesNullHandlingExtensions.cs new file mode 100644 index 0000000000..7dcaace41f --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesNullHandlingExtensions.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class LogPropertiesNullHandlingExtensions + { + internal class MyProps + { + public string P0 { get; set; } = string.Empty; + public string? P1 { get; set; } + public int P2 { get; set; } + public int? P3 { get; set; } + + [PrivateData] + public string? P4 { get; set; } + } + + [LogMethod(LogLevel.Debug)] + public static partial void M0(ILogger logger, IRedactorProvider provider, [LogProperties] MyProps p); + + [LogMethod(LogLevel.Debug)] + public static partial void M1(ILogger logger, IRedactorProvider provider, [LogProperties(SkipNullProperties = true)] MyProps p); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesOmitParameterNameExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesOmitParameterNameExtensions.cs new file mode 100644 index 0000000000..ee23bc32d7 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesOmitParameterNameExtensions.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class LogPropertiesOmitParameterNameExtensions + { + internal class MyProps + { + public int P0 { get; set; } + + public string? P1 { get; set; } + } + + [LogMethod(0, LogLevel.Debug)] + public static partial void M0(ILogger logger, [LogProperties(OmitParameterName = true)] MyProps p); + + [LogMethod(1, LogLevel.Warning)] + public static partial void M1( + ILogger logger, + [LogProperties(typeof(MyPropsProvider), nameof(MyPropsProvider.ProvideProperties), OmitParameterName = true)] MyProps p); + + [LogMethod] + internal static partial void M2( + ILogger logger, + LogLevel level, + [LogProperties(OmitParameterName = true)] MyProps param); + + [LogMethod] + internal static partial void M3( + ILogger logger, + LogLevel level, + [LogProperties(typeof(MyPropsProvider), nameof(MyPropsProvider.ProvideProperties), OmitParameterName = true)] MyProps p); + + internal static class MyPropsProvider + { + public static void ProvideProperties(ILogPropertyCollector list, MyProps? param) + { + list.Add(nameof(MyProps.P0), param?.P0); + list.Add("Custom_property_name", param?.P1); + } + } + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesProviderExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesProviderExtensions.cs new file mode 100644 index 0000000000..cbe7f7a500 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesProviderExtensions.cs @@ -0,0 +1,117 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ +#pragma warning disable SA1402 // File may only contain a single type + + internal static partial class LogPropertiesProviderExtensions + { + [LogMethod(int.MaxValue, LogLevel.Warning, "Custom provided properties for {Param}.")] + internal static partial void LogMethodCustomPropsProvider( + ILogger logger, + [LogProperties(typeof(CustomProvider), nameof(CustomProvider.ProvideProperties))] ClassToLog param); + + [LogMethod(LogLevel.Debug, "Custom provided properties for struct.")] + internal static partial void LogMethodCustomPropsProviderStruct( + ILogger logger, + [LogProperties(typeof(CustomProvider), nameof(CustomProvider.ProvideForStruct))] StructToLog param); + + [LogMethod(LogLevel.Information, "Custom provided properties for interface.")] + internal static partial void LogMethodCustomPropsProviderInterface( + ILogger logger, + [LogProperties(typeof(CustomProvider), nameof(CustomProvider.ProvideForInterface))] IInterfaceToLog param); + + [LogMethod(int.MinValue, LogLevel.Warning, "Custom provided properties for both complex params and {StringParam}.")] + internal static partial void LogMethodCustomPropsProviderTwoParams( + ILogger logger, + string stringParam, + [LogProperties(typeof(CustomProvider), nameof(CustomProvider.ProvideProperties))] ClassToLog param, + [LogProperties(typeof(CustomProvider), nameof(CustomProvider.ProvideOtherProperties))] ClassToLog param2); + + [LogMethod(1, LogLevel.Warning, "No params.")] + internal static partial void LogMethodCombinePropsProvider( + ILogger logger, + [LogProperties] ClassToLog param1, + [LogProperties(typeof(CustomProvider), nameof(CustomProvider.ProvideProperties))] ClassToLog param2); + + [LogMethod] + internal static partial void DefaultAttributeCtor( + ILogger logger, + LogLevel level, + [LogProperties(typeof(CustomProvider), nameof(CustomProvider.ProvideProperties))] ClassToLog param); + } + + internal static class CustomProvider + { + public static void ProvideProperties(ILogPropertyCollector list, ClassToLog? param) + { + // This condition is here only for testing purposes: + if (param is null) + { + return; + } + + list.Add(nameof(ClassToLog.MyIntProperty), param.MyIntProperty); + list.Add("Custom_property_name", param.MyStringProperty); + } + + public static void ProvideOtherProperties(ILogPropertyCollector list, ClassToLog? param) + { + list.Add("Another_property_name", param?.MyStringProperty?.ToUpperInvariant()); + list.Add(nameof(ClassToLog.MyIntProperty) + "_test", param?.MyIntProperty); + } + + public static void ProvideForStruct(ILogPropertyCollector list, StructToLog param) + { + list.Add(nameof(ClassToLog.MyIntProperty), param.MyIntProperty); + list.Add("Custom_property_name", param.MyStringProperty); + } + + public static void ProvideForInterface(ILogPropertyCollector list, IInterfaceToLog param) + { + list.Add(nameof(ClassToLog.MyIntProperty), param.MyIntProperty); + list.Add("Custom_property_name", param.MyStringProperty); + } + } + + internal sealed class ClassToLog + { + public int MyIntProperty { get; set; } + + public string MyStringProperty { get; set; } = "Test string"; + + public override string ToString() => "Custom string representation"; + } + + internal struct StructToLog + { + public StructToLog() + { + MyStringProperty = "Test string from struct"; + } + + public int MyIntProperty { get; set; } + + public string MyStringProperty { get; set; } + + public override string ToString() => "Custom struct string representation"; + } + + internal interface IInterfaceToLog + { + int MyIntProperty { get; set; } + + string MyStringProperty { get; set; } + } + + internal sealed class InterfaceImpl : IInterfaceToLog + { + public int MyIntProperty { get; set; } + + string IInterfaceToLog.MyStringProperty { get; set; } = "Test string from interface implementation"; + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesProviderWithObjectExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesProviderWithObjectExtensions.cs new file mode 100644 index 0000000000..ed52ac4d54 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesProviderWithObjectExtensions.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ +#pragma warning disable SA1402 // File may only contain a single type + + internal static partial class LogPropertiesProviderWithObjectExtensions + { + [LogMethod(int.MaxValue, LogLevel.Warning, "Custom provided properties for {Param}.")] + internal static partial void OneParam( + ILogger logger, + [LogProperties(typeof(CustomProviderWithObject), nameof(CustomProviderWithObject.ProvideProperties))] object param); + + [LogMethod(int.MinValue, LogLevel.Warning, "Custom provided properties for both complex params and {StringParam}.")] + internal static partial void TwoParams( + ILogger logger, + string stringParam, + [LogProperties(typeof(CustomProviderWithObject), nameof(CustomProviderWithObject.ProvideProperties))] object param, + [LogProperties(typeof(CustomProviderWithObject), nameof(CustomProviderWithObject.ProvideOtherProperties))] object param2); + } + + internal static class CustomProviderWithObject + { + public static void ProvideProperties(ILogPropertyCollector list, object? param) + { + // This condition is here only for testing purposes: + if (param is null) + { + return; + } + + list.Add(nameof(object.ToString), param + " ProvidePropertiesCall"); + } + + public static void ProvideOtherProperties(ILogPropertyCollector list, object? param) + { + list.Add(nameof(object.ToString), param + " ProvideOtherPropertiesCall"); + list.Add("Type", param?.GetType()); + } + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesRedactionExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesRedactionExtensions.cs new file mode 100644 index 0000000000..2e03edea1e --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesRedactionExtensions.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Test code")] + internal static partial class LogPropertiesRedactionExtensions + { + internal class MyBaseClassToRedact + { + [PrivateData] + public string StringPropertyBase { get; set; } = "StringPropertyBase"; + } + + internal class MyInterimClassToRedact : MyBaseClassToRedact + { + public string NoRedactionProp { get; set; } = "No redaction"; + } + + internal class ClassToRedact : MyInterimClassToRedact + { + [PublicData] + public string StringProperty { get; set; } = "StringProperty"; + + public int SimplifiedNullableIntProperty { get; set; } = int.MinValue; + + [PrivateData] + public string GetOnlyProperty => "GetOnlyProperty"; + + public MyTransitiveClass TransitiveProp { get; set; } = new(); + } + + public class MyTransitiveClass + { + public int TransitiveNumberProp { get; set; } = int.MaxValue; + + [PrivateData] + public string TransitiveStringProp { get; set; } = "TransitiveStringProp"; + } + + [LogMethod(1, LogLevel.Debug, "No template params")] + public static partial void LogNoParams(ILogger logger, IRedactorProvider redactionProvider, [LogProperties] ClassToRedact classToLog); + + [LogMethod(2, LogLevel.Information, "Only {StringProperty} as param")] + public static partial void LogTwoParams( + ILogger logger, IRedactorProvider redactionProvider, + [PrivateData] string stringProperty, [LogProperties] MyTransitiveClass? complexParam); + + // Default ctors: + [LogMethod] + public static partial void LogNoParamsDefaultCtor(ILogger logger, LogLevel level, + IRedactorProvider redactionProvider, [LogProperties] ClassToRedact classToLog); + + [LogMethod] + public static partial void LogTwoParamsDefaultCtor( + ILogger logger, IRedactorProvider redactionProvider, LogLevel level, + [PrivateData] string stringProperty, [LogProperties] MyTransitiveClass? complexParam); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesSimpleExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesSimpleExtensions.cs new file mode 100644 index 0000000000..6f425d1dc6 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesSimpleExtensions.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class LogPropertiesSimpleExtensions + { + internal class MyProps + { + public int P1 { get; set; } + public int? P2 { get; set; } + public string P3 { get; set; } = string.Empty; + public string? P4 { get; set; } +#pragma warning disable IDE1006 + public string? @class { get; set; } +#pragma warning restore IDE1006 + public IEnumerable? P5 { get; set; } + public int[]? P6 { get; set; } + public IDictionary? P7 { get; set; } + } + + [LogMethod(0, LogLevel.Debug, "{p0}")] + public static partial void LogFunc(ILogger logger, string p0, [LogProperties] MyProps myProps); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesSpecialTypesExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesSpecialTypesExtensions.cs new file mode 100644 index 0000000000..be79c3f954 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/LogPropertiesSpecialTypesExtensions.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Numerics; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class LogPropertiesSpecialTypesExtensions + { + internal class MyProps + { + public DateTime P0 { get; set; } + public DateTimeOffset P1 { get; set; } + public TimeSpan P2 { get; set; } + public Guid P3 { get; set; } + public Version? P4 { get; set; } + public Uri? P5 { get; set; } + public IPAddress? P6 { get; set; } + public EndPoint? P7 { get; set; } + public IPEndPoint? P8 { get; set; } + public DnsEndPoint? P9 { get; set; } + public BigInteger P10 { get; set; } + public Complex P11 { get; set; } + public Matrix3x2 P12 { get; set; } + public Matrix4x4 P13 { get; set; } + public Plane P14 { get; set; } + public Quaternion P15 { get; set; } + public Vector2 P16 { get; set; } + public Vector3 P17 { get; set; } + public Vector4 P18 { get; set; } + +#if NET6_0_OR_GREATER + public TimeOnly P19 { get; set; } + public DateOnly P20 { get; set; } +#endif + } + + [LogMethod(0, LogLevel.Debug)] + public static partial void M0(ILogger logger, [LogProperties] MyProps p); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/MessageTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/MessageTestExtensions.cs new file mode 100644 index 0000000000..2ef3d07dac --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/MessageTestExtensions.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class MessageTestExtensions + { + [LogMethod(0, LogLevel.Trace, null!)] + public static partial void M0(ILogger logger); + + [LogMethod(1, LogLevel.Debug, "")] + public static partial void M1(ILogger logger); + + [LogMethod(2, LogLevel.Debug)] + public static partial void M2(ILogger logger); + +#if false +#pragma warning disable R9G014 +#pragma warning disable R9G005 + + // These are disabled due to https://github.com/dotnet/roslyn/issues/52527 + // + // These are handled fine by the logger generator and generate warnings as expected. Unfortunately, the above warning suppression is + // not being observed by the C# compiler at the moment, so having these here causes build warnings. + + [LogMethod(2, LogLevel.Trace)] + public static partial void M2(ILogger logger, string p1, string p2); + + [LogMethod(3, LogLevel.Debug, "")] + public static partial void M3(ILogger logger, string p1, int p2); + + [LogMethod(4, LogLevel.Debug, "{p1}")] + public static partial void M4(ILogger logger, string p1, int p2, int p3); + +#pragma warning restore R9G014 +#pragma warning restore R9G005 + +#endif + + [LogMethod(5, LogLevel.Debug, "\"Hello\" World")] + public static partial void M5(ILogger logger); + + [LogMethod(6, "\"{Value1}\" -> \"{Value2}\"")] + public static partial void M6(ILogger logger, LogLevel logLevel, string value1, string value2); + + [LogMethod(7, LogLevel.Debug, "\"\n\r\\")] + public static partial void M7(ILogger logger); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/MiscTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/MiscTestExtensions.cs new file mode 100644 index 0000000000..0dced6a43f --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/MiscTestExtensions.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +#pragma warning disable SA1402 // File may only contain a single type +#pragma warning disable SA1403 // File may only contain a single namespace + +// Used to test use outside of a namespace +internal static partial class NoNamespace +{ + [LogMethod(0, LogLevel.Critical, "Could not open socket to `{hostName}`")] + public static partial void CouldNotOpenSocket(ILogger logger, string hostName); +} + +namespace Level1 +{ + // used to test use inside a one-level namespace + internal static partial class OneLevelNamespace + { + [LogMethod(0, LogLevel.Critical, "Could not open socket to `{hostName}`")] + public static partial void CouldNotOpenSocket(ILogger logger, string hostName); + } +} + +namespace Level1 +{ + namespace Level2 + { + // used to test use inside a two-level namespace + internal static partial class TwoLevelNamespace + { + [LogMethod(0, LogLevel.Critical, "Could not open socket to `{hostName}`")] + public static partial void CouldNotOpenSocket(ILogger logger, string hostName); + } + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/NamespaceTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/NamespaceTestExtensions.cs new file mode 100644 index 0000000000..ba4a16cb1c --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/NamespaceTestExtensions.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if ROSLYN_4_0_OR_GREATER + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace FileScopedNamespace; + +internal static partial class Log +{ + [LogMethod(1, LogLevel.Critical, "Could not open socket to `{hostName}`")] + public static partial void CouldNotOpenSocket(ILogger logger, string hostName); +} + +#endif diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/NestedClassTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/NestedClassTestExtensions.cs new file mode 100644 index 0000000000..c5070930fd --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/NestedClassTestExtensions.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +#pragma warning disable SA1402 // File may only contain a single type +#pragma warning disable CA1822 // Mark members as static +#pragma warning disable IDE0065 // Misplaced using directive + +namespace TestClasses +{ + using Alien; + + internal static partial class NestedClassTestExtensions + where T : Abc + { + internal static partial class NestedMiddleParentClass + { + internal static partial class NestedClass + { + [LogMethod(8, LogLevel.Debug, "M8")] + public static partial void M8(ILogger logger); + } + } + + public static T Foo(T x) => x; + } + + internal partial class NonStaticNestedClassTestExtensions + where T : Abc + { + internal partial class NonStaticNestedMiddleParentClass + { + internal static partial class NestedClass + { + [LogMethod(9, LogLevel.Debug, "M9")] + public static partial void M9(ILogger logger); + } + + public int Bar() => 42; + } + + public static T Foo(T x) => x; + } + + public partial struct NestedStruct + { + internal static partial class Logger + { + [LogMethod(10, LogLevel.Debug, "M10")] + public static partial void M10(ILogger logger); + } + } + + public partial record NestedRecord(string Name, string Address) + { + internal static partial class Logger + { + [LogMethod(11, LogLevel.Debug, "M11")] + public static partial void M11(ILogger logger); + } + } + + public static partial class MultiLevelNestedClass + { + public partial struct NestedStruct + { + internal partial record NestedRecord(string Name, string Address) + { + internal static partial class Logger + { + [LogMethod(12, LogLevel.Debug, "M12")] + public static partial void M12(ILogger logger); + } + } + } + } +} + +#pragma warning disable SA1403 + +namespace Alien +{ + public class Abc + { + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/NonStaticNullableTestClass.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/NonStaticNullableTestClass.cs new file mode 100644 index 0000000000..a594fa0d8a --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/NonStaticNullableTestClass.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + public partial class NonStaticNullableTestClass + { + private readonly ILogger? _logger; + private readonly IRedactorProvider? _redactorProvider; + + public NonStaticNullableTestClass(ILogger? logger, IRedactorProvider? redactorProvider) + { + _logger = logger; + _redactorProvider = redactorProvider; + } + + [LogMethod(2, LogLevel.Debug, "M2 {p0} {p1} {p2}")] + public partial void M2([PrivateData] string p0, [PrivateData] string p1, [PrivateData] string p2); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/NonStaticTestClass.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/NonStaticTestClass.cs new file mode 100644 index 0000000000..4d38312cec --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/NonStaticTestClass.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + public partial class NonStaticTestClass + { + private readonly ILogger _logger; + private readonly IRedactorProvider _redactorProvider; + + public NonStaticTestClass(ILogger logger, IRedactorProvider redactorProvider) + { + _logger = logger; + _redactorProvider = redactorProvider; + } + + [LogMethod(0, LogLevel.Debug, "M0 {p0}")] + public partial void M0([In] string p0); + + [LogMethod(1, LogLevel.Debug, "M1 {p0}")] + public partial void M1([PrivateData] string p0); + + [LogMethod(2, LogLevel.Debug, "M2 {p0} {p1} {p2}")] + public partial void M2([PrivateData] string p0, [PrivateData] string p1, [PrivateData] string p2); + + [LogMethod] + public partial void M3(LogLevel level, [PrivateData] string p0); + + [LogMethod(4, LogLevel.Information, "LogProperties: {P0}")] + internal partial void LogProperties(string p0, [LogProperties] ClassToLog p1); + + [LogMethod(5, LogLevel.Information, "LogProperties with provider: {P0}, {P1}")] + internal partial void LogPropertiesWithProvider( + string p0, + [LogProperties(typeof(CustomProvider), nameof(CustomProvider.ProvideProperties))] ClassToLog p1); + + [LogMethod(6, LogLevel.Information, "LogProperties with redaction: {P0}")] + internal partial void LogPropertiesWithRedaction( + [PrivateData] string p0, + [LogProperties] LogPropertiesRedactionExtensions.MyBaseClassToRedact p1); + + [LogMethod] + internal partial void DefaultAttrCtorLogPropertiesWithProvider( + LogLevel level, + string p0, + [LogProperties(typeof(CustomProvider), nameof(CustomProvider.ProvideProperties))] ClassToLog p1); + + [LogMethod] + internal partial void DefaultAttrCtorLogPropertiesWithRedaction( + LogLevel level, + [PrivateData] string p0, + [LogProperties] LogPropertiesRedactionExtensions.MyBaseClassToRedact p1); + + [LogMethod(7, LogLevel.Warning, "No params here...")] + public partial void NoParams(); + + [LogMethod(8, "No params here as well...")] + public partial void NoParamsWithLevel(LogLevel level); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/NullableTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/NullableTestExtensions.cs new file mode 100644 index 0000000000..866311add3 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/NullableTestExtensions.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class NullableTestExtensions + { + [LogMethod(0, LogLevel.Debug, "M0 {p0}")] + internal static partial void M0(ILogger logger, string? p0); + + [LogMethod(1, LogLevel.Debug, "M1 {p0}")] + internal static partial void M1(ILogger logger, int? p0); + + [LogMethod(3, LogLevel.Debug, "M3 {p0}")] + internal static partial void M3(ILogger logger, IRedactorProvider redactorProvider, [PrivateData] string? p0); + +#pragma warning disable S107 // Methods should not have too many parameters + [LogMethod(4, LogLevel.Debug, "M4 {p0} {p1} {p2} {p3} {p4} {p5} {p6} {p7} {p8}")] + internal static partial void M4(ILogger logger, int? p0, int? p1, int? p2, int? p3, int? p4, int? p5, int? p6, int? p7, int? p8); + + [LogMethod(5, LogLevel.Debug, "M5 {p0} {p1} {p2} {p3} {p4} {p5} {p6} {p7} {p8}")] + internal static partial void M5(ILogger logger, string? p0, string? p1, string? p2, string? p3, string? p4, string? p5, string? p6, string? p7, string? p8); +#pragma warning restore S107 // Methods should not have too many parameters + + [LogMethod(6, LogLevel.Debug, "M6 {p0}")] + internal static partial void M6(ILogger? logger, IRedactorProvider? redactorProvider, [PrivateData] string? p0); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/OverloadsTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/OverloadsTestExtensions.cs new file mode 100644 index 0000000000..4946858b42 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/OverloadsTestExtensions.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class OverloadsTestExtensions + { + [LogMethod(0, LogLevel.Information, "M0 {v}", EventName = "One")] + internal static partial void M0(ILogger logger, int v); + + [LogMethod(1, LogLevel.Information, "M0 {v}", EventName = "Two")] + internal static partial void M0(ILogger logger, string v); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/RecordTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/RecordTestExtensions.cs new file mode 100644 index 0000000000..2c634cbf56 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/RecordTestExtensions.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal partial record RecordTestExtensions(string Name, string Address) + { + [LogMethod(12, LogLevel.Debug, "M0")] + public static partial void M0(ILogger logger); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/SignatureTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/SignatureTestExtensions.cs new file mode 100644 index 0000000000..f7ade2f288 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/SignatureTestExtensions.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + // test that particular method signature variations are generated correctly + internal static partial class SignatureTestExtensions + { + // extension method + [LogMethod(eventId: 10, level: LogLevel.Critical, message: "Message11")] + internal static partial void M11(this ILogger logger); + + // optional parameter + [LogMethod(1, LogLevel.Debug, "{p1} {p2}")] + internal static partial void M2(ILogger logger, string p1, string p2 = "World"); + } + + // test that particular method signature variations are generated correctly + internal partial class SignatureTestExtensions + where T : class + { + public static void Combo(ILogger logger, ILogger logger2) + { + M1(logger); + M2(logger); + M3(logger); + M4(logger2); + M5(logger, new[] { "A" }); + M6(logger); + M8(logger); + M9(logger); + M10(logger, null); + M11(logger, "A", LogLevel.Debug, "B"); + } + + // normal public method + [LogMethod(0, LogLevel.Critical, "Message1")] + public static partial void M1(ILogger logger); + + // internal method + [LogMethod(1, LogLevel.Critical, "Message2")] + internal static partial void M2(ILogger logger); + + // private method + [LogMethod(2, LogLevel.Critical, "Message3")] + private static partial void M3(ILogger logger); + + // generic ILogger + [LogMethod(3, LogLevel.Critical, "Message4")] + private static partial void M4(ILogger logger); + + // random type method parameter + [LogMethod(4, LogLevel.Critical, "Message5 {items}")] + private static partial void M5(ILogger logger, System.Collections.IEnumerable items); + + // line feeds and quotes in the message string + [LogMethod(5, LogLevel.Critical, "Message6\n\"\r")] + private static partial void M6(ILogger logger); + + // generic parameter + [LogMethod(6, LogLevel.Critical, "Message7 {p1}\n\"\r")] + private static partial void M7(ILogger logger, T p1); + + // normal public method + [LogMethod(7, LogLevel.Critical, "Message8")] + private protected static partial void M8(ILogger logger); + + // internal method + [LogMethod(8, LogLevel.Critical, "Message9")] + protected internal static partial void M9(ILogger logger); + + // nullable parameter + [LogMethod(9, LogLevel.Critical, "Message10 {optional}")] + internal static partial void M10(ILogger logger, string? optional); + + // dynamic log level + [LogMethod(10, "Message11 {p1} {p2}")] + internal static partial void M11(ILogger logger, string p1, LogLevel level, string p2); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/SkipEnabledCheckTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/SkipEnabledCheckTestExtensions.cs new file mode 100644 index 0000000000..1565fbf966 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/SkipEnabledCheckTestExtensions.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class SkipEnabledCheckTestExtensions + { + [LogMethod(0, LogLevel.Information, "M0", SkipEnabledCheck = true)] + internal static partial void LoggerMethodWithTrueSkipEnabledCheck(ILogger logger); + + [LogMethod(1, LogLevel.Information, "M1", SkipEnabledCheck = false)] + internal static partial void LoggerMethodWithFalseSkipEnabledCheck(ILogger logger); + + // Default ctor: + [LogMethod(SkipEnabledCheck = false)] + internal static partial void LoggerMethodWithFalseSkipEnabledCheck(ILogger logger, LogLevel level, string p1); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/StructTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/StructTestExtensions.cs new file mode 100644 index 0000000000..17007b2cd9 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/StructTestExtensions.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal readonly partial struct StructTestExtensions + { + [LogMethod(0, LogLevel.Trace, "M0")] + public static partial void M0(ILogger logger); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/TemplateTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/TemplateTestExtensions.cs new file mode 100644 index 0000000000..98727f2627 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/TemplateTestExtensions.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + internal static partial class TemplateTestExtensions + { + [LogMethod(0, LogLevel.Error, "M0 {A1}")] + public static partial void M0(ILogger logger, int a1); + + [LogMethod(1, LogLevel.Error, "M1 {A1} {A1}")] + public static partial void M1(ILogger logger, int a1); + +#pragma warning disable S107 // Methods should not have too many parameters + [LogMethod(2, LogLevel.Error, "M2 {A1} {a2} {A3} {a4} {A5} {a6} {A7}")] + public static partial void M2(ILogger logger, int a1, int a2, int a3, int a4, int a5, int a6, int a7); +#pragma warning restore S107 // Methods should not have too many parameters + + [LogMethod(3, LogLevel.Error, "M3 {a2} {A1}")] + public static partial void M3(ILogger logger, int a1, int a2); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/TestInstances.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/TestInstances.cs new file mode 100644 index 0000000000..61b2c833ea --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/TestInstances.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace TestClasses +{ + public partial class TestInstances + { +#pragma warning disable IDE0052 + private readonly ILogger _myLogger; +#pragma warning restore IDE0052 + + public TestInstances(ILogger logger) + { + _myLogger = logger; + } + + [LogMethod(0, LogLevel.Error, "M0")] + public partial void M0(); + + [LogMethod(1, LogLevel.Trace, "M1 {p1}")] + public partial void M1(string p1); + + [LogMethod] + public partial void M2(LogLevel level, string p1); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Unit/Common/AttributeParserTests.cs b/test/Generators/Microsoft.Gen.Logging/Unit/Common/AttributeParserTests.cs new file mode 100644 index 0000000000..1a4fa5a0a9 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Unit/Common/AttributeParserTests.cs @@ -0,0 +1,310 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Extensions.Telemetry.Logging; +using Microsoft.Gen.Logging.Parsing; +using Microsoft.Gen.Shared; +using Xunit; + +namespace Microsoft.Gen.Logging.Test; + +public class AttributeParserTests +{ + [Fact] + public async Task RandomAttribute() + { + var diagnostics = await RunGenerator(@" + internal static partial class C + { + [LogMethod(0, LogLevel.Debug, ""M {p0}"")] + static partial void M(ILogger logger, [Test] string p0); + } + "); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task LegacyAttribute() + { + var diagnostics = await RunGenerator(@" + internal static partial class C + { +#pragma warning disable CS0618 + + [LoggerMessage(0, LogLevel.Debug, ""M {p0}"")] + static partial void M(ILogger logger, [Test] string p0); + } + "); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task DataClassificationAttributeFullName() + { + var diagnostics = await RunGenerator(@" + internal static partial class C + { + [LogMethod(0, LogLevel.Debug, ""M {p0}"")] + static partial void M(ILogger logger, IRedactorProvider provider, [PrivateDataAttribute] string p0); + } + "); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task DataClassificationAttributeShortName() + { + var diagnostics = await RunGenerator(@" + internal static partial class C + { + [LogMethod(0, LogLevel.Debug, ""M {p0}"")] + static partial void M(ILogger logger, IRedactorProvider provider, [PrivateData] string p0); + } + "); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task MultipleAttributesOnDifferentTopics() + { + var diagnostics = await RunGenerator(@" + internal static partial class C + { + [LogMethod(0, LogLevel.Debug, ""M {p0}"")] + static partial void M(ILogger logger, IRedactorProvider provider, [Test][PrivateData] string p0); + } + "); + + Assert.Empty(diagnostics); + } + + [Theory] + [InlineData("[PrivateData] string")] + public async Task RedactorProviderIsInTheInstance(string type) + { + var diagnostics = await RunGenerator(@$" + internal partial class TestInstance + {{ + private readonly ILogger _logger; + private readonly IRedactorProvider _redactorProvider; + + public TestInstance(ILogger logger, IRedactorProvider redactorProvider) + {{ + _logger = logger; + _redactorProvider = redactorProvider; + }} + + [LogMethod(0, LogLevel.Debug, ""M0 {{p0}}"")] + public partial void M0({type} p0); + }}"); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task DataClassOnAllTypes() + { + var diagnostics = await RunGenerator(@" + internal static partial class C + { + [LogMethod(0, LogLevel.Debug, ""M {p0}"")] + static partial void M(ILogger logger, IRedactorProvider provider, [PrivateData] int p0); + } + "); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task MultipleDataClassificationAttributes() + { + var diagnostics = await RunGenerator(@" + internal static partial class C + { + [LogMethod(0, LogLevel.Debug, ""M {p0}"")] + static partial void M(ILogger logger, IRedactorProvider provider, [PrivateData][PrivateData] string p0); + } + "); + + Assert.Contains(diagnostics, d => d.Id == DiagDescriptors.MultipleDataClassificationAttributes.Id); + } + + [Fact] + public async Task MissingLogger() + { + var diagnostics = await RunGenerator(@" + internal static partial class C + { + [LogMethod(0, LogLevel.Debug, ""M {p0}"")] + static partial void M(IRedactorProvider provider, [PrivateData] string p0); + } + "); + + _ = Assert.Single(diagnostics); + Assert.Equal(DiagDescriptors.MissingLoggerArgument.Id, diagnostics[0].Id); + } + + [Theory] + [InlineData("[PrivateData] string")] + public async Task MissingRedactorProviderStaticMethod(string type) + { + var diagnostics = await RunGenerator(@$" + internal static partial class C + {{ + [LogMethod(0, LogLevel.Debug, ""M {{p0}}"")] + static partial void M(ILogger logger, {type} p0); + }}"); + + _ = Assert.Single(diagnostics); + Assert.Equal(DiagDescriptors.MissingRedactorProviderArgument.Id, diagnostics[0].Id); + } + + [Theory] + [InlineData("[PrivateData] string")] + public async Task MissingRedactorProviderInstanceMethod(string type) + { + var diagnostics = await RunGenerator(@$" + internal partial class TestInstance + {{ + private readonly ILogger _logger; + + public TestInstance(ILogger logger) + {{ + _logger = logger; + }} + + [LogMethod(0, LogLevel.Debug, ""M0 {{p0}}"")] + public partial void M0({type} p0); + }}"); + + _ = Assert.Single(diagnostics); + Assert.Equal(DiagDescriptors.MissingRedactorProviderField.Id, diagnostics[0].Id); + } + + [Fact] + public async Task MultipleRedactorProviders() + { + var diagnostics = await RunGenerator(@" + internal partial class TestInstance + { + private readonly ILogger _logger; + private readonly IRedactorProvider _redactorProvider; + private readonly IRedactorProvider _redactorProvider2; + + public TestInstance(ILogger logger, IRedactorProvider redactorProvider) + { + _logger = logger; + _redactorProvider = redactorProvider; + } + + [LogMethod(0, LogLevel.Debug, ""M0 {p0}"")] + public partial void M0([PrivateData] string p0); + } + "); + + _ = Assert.Single(diagnostics); + Assert.Equal(DiagDescriptors.MultipleRedactorProviderFields.Id, diagnostics[0].Id); + } + + [Fact] + public async Task MissingDataClassificationAttributeInStatic() + { + var diagnostics = await RunGenerator(@" + internal static partial class C + { + [LogMethod(0, LogLevel.Debug, ""M {p0}"")] + static partial void M(ILogger logger, IRedactorProvider provider, string p0); + } + "); + + _ = Assert.Single(diagnostics); + Assert.Equal(DiagDescriptors.MissingDataClassificationArgument.Id, diagnostics[0].Id); + } + + [Fact] + public async Task NotMissingDataClassificationAttributeInStatic() + { + var diagnostics = await RunGenerator(@" + internal static partial class C + { + [LogMethod(0, LogLevel.Debug, ""M {p0}"")] + static partial void M(ILogger logger, IRedactorProvider provider, [PublicData] string p0); + } + "); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task MissingDataClassificationAttributeInInstance() + { + var diagnostics = await RunGenerator(@" + partial class C + { + private ILogger _logger; + + [LogMethod(0, LogLevel.Debug, ""M {p0}"")] + partial void M(IRedactorProvider provider, string p0); + }"); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal(DiagDescriptors.MissingDataClassificationArgument.Id, diagnostic.Id); + } + + [Theory] + [InlineData("[PrivateData] string")] + public async Task MethodNotStatic(string type) + { + var diagnostics = await RunGenerator(@$" + internal partial class C + {{ + [LogMethod(0, LogLevel.Debug, ""M {{p0}}"")] + partial void M(ILogger logger, IRedactorProvider provider, {type} p0); + }}"); + + _ = Assert.Single(diagnostics); + Assert.Equal(DiagDescriptors.LoggingMethodShouldBeStatic.Id, diagnostics[0].Id); + } + + private static async Task> RunGenerator(string code) + { + var text = $@" + namespace Test {{ + using Microsoft.Extensions.Compliance.Classification; + using Microsoft.Extensions.Compliance.Testing; + using Microsoft.Extensions.Compliance.Redaction; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Telemetry.Logging; + {code} + }} + "; + + var loggerAssembly = Assembly.GetAssembly(typeof(ILogger)); + var logMethodAssembly = Assembly.GetAssembly(typeof(LogMethodAttribute)); + var enrichmentAssembly = Assembly.GetAssembly(typeof(IEnrichmentPropertyBag)); + var dataClassificationAssembly = Assembly.GetAssembly(typeof(DataClassification)); + var simpleDataClassificationAssembly = Assembly.GetAssembly(typeof(PrivateDataAttribute)); + var redactorProviderAssembly = Assembly.GetAssembly(typeof(IRedactorProvider)); + var refs = new[] { loggerAssembly!, logMethodAssembly!, enrichmentAssembly!, dataClassificationAssembly!, simpleDataClassificationAssembly!, redactorProviderAssembly! }; + + var (d, _) = await RoslynTestUtils.RunGenerator( + new Generator(), + refs, + new[] { text }).ConfigureAwait(false); + + return d; + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Unit/Common/DiagDescriptorsTests.cs b/test/Generators/Microsoft.Gen.Logging/Unit/Common/DiagDescriptorsTests.cs new file mode 100644 index 0000000000..0668929309 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Unit/Common/DiagDescriptorsTests.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.Gen.Logging.Parsing; +using Xunit; + +namespace Microsoft.Gen.Logging.Test; + +public class DiagDescriptorsTests +{ + public static IEnumerable DiagDescriptorsData() + { + var type = typeof(DiagDescriptors); + foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.GetProperty)) + { + var value = property.GetValue(type, null); + yield return new[] { value }; + } + } + + [Theory] + [MemberData(nameof(DiagDescriptorsData))] + public void ShouldContainValidLinkAndBeEnabled(DiagnosticDescriptor descriptor) + { + Assert.True(descriptor.IsEnabledByDefault, descriptor.Id + " should be enabled by default"); + Assert.EndsWith("/" + descriptor.Id, descriptor.HelpLinkUri, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Unit/Common/EmitterTests.cs b/test/Generators/Microsoft.Gen.Logging/Unit/Common/EmitterTests.cs new file mode 100644 index 0000000000..05f3a71f6f --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Unit/Common/EmitterTests.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Extensions.Telemetry.Logging; +using Microsoft.Gen.Logging.Parsing; +using Microsoft.Gen.Shared; +using Xunit; + +namespace Microsoft.Gen.Logging.Test; + +public class EmitterTests +{ + [Fact] + public async Task TestEmitter() + { + var sources = new List(); + foreach (var file in Directory.GetFiles("TestClasses")) + { +#if !ROSLYN_4_0_OR_GREATER + if (file.EndsWith("NamespaceTestExtensions.cs")) + { + continue; + } +#endif + + sources.Add(File.ReadAllText(file)); + } + +#if NET6_0_OR_GREATER + var symbols = new[] { "NET7_0_OR_GREATER", "NET6_0_OR_GREATER", "NET5_0_OR_GREATER" }; +#else + var symbols = new[] { "NET5_0_OR_GREATER" }; +#endif + + var (d, r) = await RoslynTestUtils.RunGenerator( + new Generator(), + new[] + { + Assembly.GetAssembly(typeof(ILogger))!, + Assembly.GetAssembly(typeof(LogMethodAttribute))!, + Assembly.GetAssembly(typeof(IEnrichmentPropertyBag))!, + Assembly.GetAssembly(typeof(DataClassification))!, + Assembly.GetAssembly(typeof(IRedactorProvider))!, + Assembly.GetAssembly(typeof(PrivateDataAttribute))!, + }, + sources, + symbols) + .ConfigureAwait(false); + + // we need this "Where()" hack because Roslyn 4.0 doesn't recognize #pragma warning disable for generator-produced warnings + Assert.Empty(d.Where(diag + => diag.Id != DiagDescriptors.ShouldntMentionExceptionInMessage.Id + && diag.Id != DiagDescriptors.ShouldntMentionLoggerInMessage.Id + && diag.Id != DiagDescriptors.ShouldntMentionLogLevelInMessage.Id)); + + _ = Assert.Single(r); + + var golden = File.ReadAllText($"GoldenFiles/Microsoft.Gen.Logging/Microsoft.Gen.Logging.Generator/Logging.g.cs"); + var result = r[0].SourceText.ToString(); + Assert.Equal(golden, result); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Unit/Common/EmitterUtilsTests.cs b/test/Generators/Microsoft.Gen.Logging/Unit/Common/EmitterUtilsTests.cs new file mode 100644 index 0000000000..2ef14f1cc0 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Unit/Common/EmitterUtilsTests.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Gen.Logging.Emission; +using Microsoft.Gen.Logging.Model; +using Xunit; + +namespace Microsoft.Gen.Logging.Test; + +public class EmitterUtilsTests +{ + [Theory] + [InlineData("\n", "\\n")] + [InlineData("\r", "\\r")] + [InlineData("\"", "\\\"")] + [InlineData("\\", "\\\\")] + [InlineData("\n\r\"", "\\n\\r\\\"")] + [InlineData("no special chars...", "no special chars...")] + [InlineData("special \n chars \r within \n\n a \"string\"", "special \\n chars \\r within \\n\\n a \\\"string\\\"")] + public void ShouldEscapeMessageStringCorrectly(string input, string expected) + { + Assert.Equal(expected, Emitter.EscapeMessageString(input)); + } + + [Theory] + [InlineData("\n", "\\n")] + [InlineData("\r", "\\r")] + [InlineData("<", "<")] + [InlineData(">", ">")] + [InlineData("no special chars...", "no special chars...")] + [InlineData("special \n chars \r within \n\n a \"string\"", "special \\n chars \\r within \\n\\n a \"string\"")] + public void ShouldEscapeMessageStringForXmlDocumentationCorrectly(string input, string expected) + { + Assert.Equal(expected, Emitter.EscapeMessageStringForXmlDocumentation(input)); + } + + [Fact] + public void ShouldGetLogPropertiesDataClassesCorrectly() + { + var publicDataFullName = typeof(PublicDataAttribute).FullName!; + + var lm = new LoggingMethod(); + lm.AllParameters.Add(new LoggingMethodParameter + { + LogPropertiesProvider = new LoggingPropertyProvider(string.Empty, string.Empty), + PropertiesToLog = new List { new LoggingProperty("a", "b", publicDataFullName, false, false, false, false, false, false, Array.Empty()) } + }); + + lm.AllParameters.Add(new LoggingMethodParameter + { + LogPropertiesProvider = null, + PropertiesToLog = new List { new LoggingProperty("c", "d", publicDataFullName, false, false, false, false, false, false, Array.Empty()) } + }); + + var result = Emitter.GetLogPropertiesAttributes(lm); + Assert.Collection(result, x => Assert.Equal(publicDataFullName, x)); + } + + [Fact] + public void ShouldNotFindLogLevelIfNoneAvailable() + { + var lm = new LoggingMethod + { + Level = null + }; + + Assert.Empty(Emitter.GetLoggerMethodLogLevel(lm)); + } + + [Fact] + public void ShouldFindLogLevelFromParameter() + { + const string ParamName = "Test name"; + + var lm = new LoggingMethod + { + Level = null + }; + + lm.AllParameters.Add(new LoggingMethodParameter + { + IsLogLevel = true, + Name = ParamName + }); + + Assert.Equal(ParamName, Emitter.GetLoggerMethodLogLevel(lm)); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Unit/Common/LogParserUtilitiesTests.cs b/test/Generators/Microsoft.Gen.Logging/Unit/Common/LogParserUtilitiesTests.cs new file mode 100644 index 0000000000..11300b92e8 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Unit/Common/LogParserUtilitiesTests.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Immutable; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.Gen.Logging.Model; +using Microsoft.Gen.Logging.Parsing; +using Microsoft.Gen.Shared; +using Moq; +using Moq.Protected; +using Xunit; + +namespace Microsoft.Gen.Logging.Test; + +public class LogParserUtilitiesTests +{ + [Theory] + [InlineData(false, false, false, true)] + [InlineData(false, false, true, false)] + [InlineData(false, true, false, false)] + [InlineData(true, false, false, false)] + public void ShouldSkipLoggingMethodWhenParameterIsSpecial(bool isLogger, bool isRedactorProvider, bool isException, bool isLogLevel) + { + const string ParamName = "param name"; + + var paramSymbolMock = new Mock(); + paramSymbolMock.SetupGet(x => x.Name) + .Returns(ParamName); + + var loggerParameter = new LoggingMethodParameter + { + IsLogger = isLogger, + IsRedactorProvider = isRedactorProvider, + IsException = isException, + IsLogLevel = isLogLevel + }; + + var diagMock = new Mock>(); + var result = LogParserUtilities.ProcessLogPropertiesForParameter(null!, null!, loggerParameter, paramSymbolMock.Object, null!, diagMock.Object, null!, CancellationToken.None); + + Assert.True(result == LogPropertiesProcessingResult.Fail); + diagMock.Verify( + x => x(It.IsAny(), It.IsAny(), It.Is(p => p != null && p.Length == 1 && Equals(p[0], ParamName))), + Times.Once); + + diagMock.VerifyNoOtherCalls(); + } + + [Fact] + public void ShouldSkipLoggingMethodWhenParameterTypeIsSpecial() + { + const string ParamType = "param type"; + + var paramTypeMock = new Mock(); + paramTypeMock.SetupGet(x => x.Kind).Returns(SymbolKind.NamedType); + paramTypeMock.SetupGet(x => x.TypeKind).Returns(TypeKind.Class); + paramTypeMock.SetupGet(x => x.SpecialType).Returns(SpecialType.System_Array); + paramTypeMock.SetupGet(x => x.OriginalDefinition).Returns(paramTypeMock.Object); + paramTypeMock.Setup(x => x.ToDisplayString(It.IsAny())) + .Returns(ParamType); + + var paramSymbolMock = new Mock(); + paramSymbolMock.SetupGet(x => x.Type) + .Returns(paramTypeMock.Object); + + var diagMock = new Mock>(); + var result = LogParserUtilities.ProcessLogPropertiesForParameter(null!, null!, new LoggingMethodParameter(), paramSymbolMock.Object, null!, diagMock.Object, null!, CancellationToken.None); + + Assert.True(result == LogPropertiesProcessingResult.Fail); + diagMock.Verify( + x => x(It.IsAny(), It.IsAny(), It.Is(p => p != null && p.Length == 1 && Equals(p[0], ParamType))), + Times.Once); + + diagMock.VerifyNoOtherCalls(); + } + + [Fact] + public void ShouldDetectNullableOfT() + { + var typeSymbolMock = new Mock(); + typeSymbolMock.SetupGet(x => x.SpecialType).Returns(SpecialType.System_Nullable_T); + var result = typeSymbolMock.Object.IsNullableOfT(); + Assert.True(result); + + var anotherTypeSymbolMock = new Mock(); + anotherTypeSymbolMock.SetupGet(x => x.SpecialType).Returns(SpecialType.None); + anotherTypeSymbolMock.SetupGet(x => x.OriginalDefinition).Returns(typeSymbolMock.Object); + result = typeSymbolMock.Object.IsNullableOfT(); + Assert.True(result); + } + + [Fact] + public void ShouldNotDetectNullableOfT() + { + var typeSymbolMock = new Mock(); + typeSymbolMock.SetupGet(x => x.SpecialType).Returns(SpecialType.None); + typeSymbolMock.SetupGet(x => x.OriginalDefinition).Returns(typeSymbolMock.Object); + var result = typeSymbolMock.Object.IsNullableOfT(); + Assert.False(result); + } + + [Fact] + public void ShouldGet_DataClassificationAttr_WhenAttrClassIsNull() + { + var attributeMock = new Mock(); + attributeMock + .Protected() + .SetupGet("CommonAttributeClass") + .Returns((INamedTypeSymbol?)null); + + var symbolMock = new Mock(); + symbolMock.Setup(x => x.GetAttributes()) + .Returns(new[] { attributeMock.Object }.ToImmutableArray()); + + var result = LogParserUtilities.GetDataClassificationAttributes(symbolMock.Object, null!); + Assert.Empty(result); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Unit/Common/LoggingMethodParameterTests.cs b/test/Generators/Microsoft.Gen.Logging/Unit/Common/LoggingMethodParameterTests.cs new file mode 100644 index 0000000000..babea28f9a --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Unit/Common/LoggingMethodParameterTests.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Gen.Logging.Model; +using Xunit; + +namespace Microsoft.Gen.Logging.Test; + +public class LoggingMethodParameterTests +{ + [Fact] + public void Fields_Should_BeInitialized() + { + var instance = new LoggingMethodParameter(); + Assert.Empty(instance.Name); + Assert.Empty(instance.Type); + } + + [Theory] + [InlineData(false, false, true, false, false)] + [InlineData(false, true, true, false, true)] + [InlineData(true, false, true, true, false)] + [InlineData(true, true, true, true, true)] + public void ShouldGetLogMethodParameterInfoCorrectly( + bool addPropertiesToLog, + bool setLogPropertiesProvider, + bool expectedParamIsInTemplate, + bool expectedParamHasProperties, + bool expectedParamHasPropsProvider) + { + const string PrivateDataAttributeType = "Microsoft.Extensions.Compliance.Testing.PrivateDataAtribute"; + + var lp = new LoggingMethodParameter + { + LogPropertiesProvider = setLogPropertiesProvider + ? new LoggingPropertyProvider(string.Empty, string.Empty) + : null + }; + + if (addPropertiesToLog) + { + lp.PropertiesToLog.Add(new LoggingProperty(string.Empty, string.Empty, PrivateDataAttributeType, false, false, false, false, false, false, Array.Empty())); + } + + Assert.Equal(expectedParamIsInTemplate, lp.IsNormalParameter); + Assert.Equal(expectedParamHasProperties, lp.HasProperties); + Assert.Equal(expectedParamHasPropsProvider, lp.HasPropsProvider); + } + + [Fact] + public void Misc() + { + var lp = new LoggingMethodParameter + { + Name = "Foo", + NeedsAtSign = false, + }; + + Assert.Equal(lp.Name, lp.NameWithAt); + lp.NeedsAtSign = true; + Assert.Equal("@" + lp.Name, lp.NameWithAt); + + lp.Type = "Foo"; + lp.IsReference = false; + lp.IsNullable = true; + Assert.Equal(lp.Type, lp.PotentiallyNullableType); + + lp.IsReference = false; + lp.IsNullable = false; + Assert.Equal(lp.Type, lp.PotentiallyNullableType); + + lp.IsReference = true; + lp.IsNullable = false; + Assert.Equal(lp.Type + "?", lp.PotentiallyNullableType); + + lp.IsReference = true; + lp.IsNullable = true; + Assert.Equal(lp.Type, lp.PotentiallyNullableType); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Unit/Common/LoggingMethodTests.cs b/test/Generators/Microsoft.Gen.Logging/Unit/Common/LoggingMethodTests.cs new file mode 100644 index 0000000000..69ca8089c7 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Unit/Common/LoggingMethodTests.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Gen.Logging.Model; +using Xunit; + +namespace Microsoft.Gen.Logging.Test; + +public class LoggingMethodTests +{ + [Fact] + public void Fields_Should_BeInitialized() + { + var instance = new LoggingMethod(); + Assert.Empty(instance.Name); + Assert.Empty(instance.Message); + Assert.Empty(instance.Modifiers); + Assert.Equal("_logger", instance.LoggerField); + Assert.Equal("_redactorProvider", instance.RedactorProviderField); + } + + [Fact] + public void ShouldReturnParameterNameIfNotFoundInMap() + { + var p = new LoggingMethodParameter { Name = "paramName" }; + var method = new LoggingMethod(); + Assert.Equal(p.Name, method.GetParameterNameInTemplate(p)); + } + + [Fact] + public void ShouldReturnNameForParameterFromMap() + { + var p = new LoggingMethodParameter { Name = "paramName" }; + var method = new LoggingMethod(); + method.TemplateMap[p.Name] = "Name from the map"; + + Assert.Equal(method.TemplateMap[p.Name], method.GetParameterNameInTemplate(p)); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Unit/Common/LoggingTypeTests.cs b/test/Generators/Microsoft.Gen.Logging/Unit/Common/LoggingTypeTests.cs new file mode 100644 index 0000000000..970986af7c --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Unit/Common/LoggingTypeTests.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Gen.Logging.Model; +using Xunit; + +namespace Microsoft.Gen.Logging.Test; + +public class LoggingTypeTests +{ + [Fact] + public void Fields_Should_BeInitialized() + { + var instance = new LoggingType(); + Assert.Empty(instance.Name); + Assert.Empty(instance.Namespace); + Assert.Empty(instance.Keyword); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Unit/Common/ParserTests.LogProperties.cs b/test/Generators/Microsoft.Gen.Logging/Unit/Common/ParserTests.LogProperties.cs new file mode 100644 index 0000000000..2cb9f43d58 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Unit/Common/ParserTests.LogProperties.cs @@ -0,0 +1,718 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using Microsoft.Gen.Logging.Parsing; +using Xunit; + +namespace Microsoft.Gen.Logging.Test; + +public partial class ParserTests +{ + [Fact] + public static async Task LogPropertiesOmitParamName_DetectsNameCollision() + { + const string Source = @" + class MyType + { + public int p0 { get; } + } + + partial class C + { + [LogMethod(0, LogLevel.Debug, ""Parameterless..."")] + static partial void M0(ILogger logger, [LogProperties(OmitParameterName = true)] MyType /*0+*/p0/*-0*/); + }"; + + await RunGenerator(Source, DiagDescriptors.LogPropertiesNameCollision); + } + + [Fact] + public static async Task LogProperties_AllowsDefaultLogMethodCtor() + { + await RunGenerator(@" + class MyType + { + public int p0 { get; } + } + + partial class C + { + [LogMethod] + static partial void M0(ILogger logger, LogLevel level, [LogProperties] MyType p0); + }"); + } + + [Theory] + [InlineData("LogLevel")] + [InlineData("System.Exception")] + public async Task LogPropertiesInvalidUsage(string annotation) + { + // We don't check [LogProperties] on ILogger here since it produces a lot of errors apart from R9G027 + string source = @$" + partial class C + {{ + [LogMethod(0, LogLevel.Debug, ""Parameterless..."")] + static partial void M(ILogger logger, [LogProperties] {annotation} /*0+*/param/*-0*/); + }}"; + + await RunGenerator(source, DiagDescriptors.LogPropertiesInvalidUsage); + } + + [Theory] + [InlineData("MyClass")] + [InlineData("MyStruct")] + [InlineData("MyInterface")] + public async Task LogPropertiesValidUsage(string parameterType) + { + await RunGenerator(@$" + class MyClass + {{ + public string Property {{ get; set; }} + }} + + struct MyStruct + {{ + public string Property {{ get; set; }} + }} + + struct MyInterface + {{ + public string Property {{ get; set; }} + }} + + partial class C + {{ + [LogMethod(0, LogLevel.Debug, ""Parameter {{P1}}"")] + static partial void M(ILogger logger, [LogProperties] {parameterType} p1); + }}"); + } + + [Theory] + [InlineData("MyClass")] + [InlineData("MyStruct")] + [InlineData("MyInterface")] + public async Task LogPropertiesParameterSkipped(string parameterType) + { + string source = @$" + class MyClass {{ }} + + struct MyStruct {{ }} + + struct MyInterface {{ }} + + partial class C + {{ + [LogMethod(0, LogLevel.Debug, ""Empty template"")] + static partial void M(ILogger logger, [LogProperties] {parameterType} /*0+*/param/*-0*/); + }}"; + + await RunGenerator(source, DiagDescriptors.LogPropertiesParameterSkipped); + } + + [Fact] + public async Task LogPropertiesParameterNotSkipped() + { + await RunGenerator(@" + class MyClass + { + public int A { get; set; } + } + + partial class C + { + [LogMethod(0, LogLevel.Debug, ""{Param} is here"")] + static partial void M(ILogger logger, [LogProperties] MyClass param); + }"); + } + + [Fact] + public async Task LogPropertiesPointlessUsage() + { + const string Source = @" + class MyClass + { + private int A { get; set; } + protected double B { get; set; } + } + + partial class C + { + [LogMethod(0, LogLevel.Debug, ""{param}"")] + static partial void M(ILogger logger, [LogProperties] MyClass /*0+*/param/*-0*/); + }"; + + await RunGenerator(Source, DiagDescriptors.LogPropertiesParameterSkipped); + } + + [Fact] + public async Task SimpleNameCollision() + { + const string Source = @" + partial class C + { + [LogMethod(0, LogLevel.Debug, ""{Param} {Param}"")] + static partial void M(ILogger logger, string param, string /*0+*/Param/*-0*/); + }"; + + await RunGenerator(Source, DiagDescriptors.LogPropertiesNameCollision); + } + + [Fact] + public async Task LogPropertiesNameCollision() + { + const string Source = @" + class MyClass + { + public int A { get; set; } + } + + partial class C + { + [LogMethod(0, LogLevel.Debug, ""{param_A}"")] + static partial void M(ILogger logger, string param_A, [LogProperties] MyClass /*0+*/param/*-0*/); + }"; + + await RunGenerator(Source, DiagDescriptors.LogPropertiesNameCollision); + } + + [Fact] + public async Task LogPropertiesTransitiveNameCollision() + { + const string Source = @" + public class MyClass + { + public int Transitive_Prop { get; set; } + + public MyTransitiveClass Transitive { get; set; } + } + + public class MyTransitiveClass + { + public int Prop { get; set; } + } + + partial class C + { + [LogMethod(0, LogLevel.Debug, ""No params..."")] + static partial void M(ILogger logger, [LogProperties] MyClass /*0+*/param/*-0*/); + }"; + + await RunGenerator(Source, DiagDescriptors.LogPropertiesNameCollision); + } + + [Fact] + public async Task LogPropertiesProviderTypeNotFound() + { + await RunGenerator(@" + class MyClass + { + public string Property { get; set; } + } + + partial class C + { + [LogMethod(0, LogLevel.Debug, ""Parameter"")] + static partial void M(ILogger logger, [LogProperties(typeof(XXX), """")] MyClass p1); + }"); + } + + [Theory] + [InlineData("null")] + [InlineData("\"\"")] + [InlineData("\"Error\"")] + [InlineData("\"Prop\"")] + [InlineData("\"Field\"")] + [InlineData("\"Const\"")] + public async Task LogPropertiesProviderMethodNotFound(string methodName) + { + string source = @$" + class MyClass + {{ + public string Property {{ get; set; }} + }} + + static class Provider + {{ + public static string Prop {{ get; set; }} + public static string Field; + public static const string Const = ""test""; + }} + + partial class C + {{ + [LogMethod(0, LogLevel.Debug, ""Parameter"")] + static partial void M(ILogger logger, [/*0+*/LogProperties(typeof(Provider), {methodName})/*-0*/] MyClass p1); + }}"; + + await RunGenerator(source, DiagDescriptors.LogPropertiesProviderMethodNotFound); + } + + [Fact] + public async Task LogPropertiesProviderMethodNotFound2() + { + const string Source = @" + class MyClass + { + public string Property { get; set; } + } + + static class Provider + { + public static void Provide1(ILogPropertyCollector props, MyClass? value) + { + } + + public static void Provide2(ILogPropertyCollector props, MyClass? value, int a) + { + } + } + + partial class C + { + [LogMethod(0, LogLevel.Debug, ""Parameter"")] + static partial void M(ILogger logger, [/*0+*/LogProperties(typeof(Provider), nameof(Provider.Provide))/*-0*/] MyClass p1); + }"; + + await RunGenerator(Source, DiagDescriptors.LogPropertiesProviderMethodNotFound); + } + + [Fact] + public async Task LogPropertiesProviderMethodIsGeneric() + { + const string Source = @" + class MyClass + { + public string Property { get; set; } + } + + static class Provider + { + public static void Provide(ILogPropertyCollector props, MyClass? value) + { + } + } + + partial class C + { + [LogMethod(0, LogLevel.Debug, ""Parameter"")] + static partial void M(ILogger logger, [/*0+*/LogProperties(typeof(Provider), nameof(Provider.Provide))/*-0*/] MyClass p1); + }"; + + await RunGenerator(Source, DiagDescriptors.LogPropertiesProviderMethodInvalidSignature); + } + + [Fact] + public async Task LogPropertiesProvider_UsingInterfacesAndBaseClassAndNullableAndOptional() + { + const string Source = @" + interface IFoo + { + } + + class BaseClass + { + } + + class MyClass : BaseClass, IFoo + { + } + + static class Provider + { + public static void Provide1(ILogPropertyCollector props, MyClass? value) {} + public static void Provide2(ILogPropertyCollector props, BaseClass value) {} + public static void Provide3(ILogPropertyCollector props, IFoo value) {} + public static void Provide4(ILogPropertyCollector props, MyClass value, object o = null) {} + public static void Provide5(ILogPropertyCollector props, MyClass value) {} + } + + partial class C + { + [LogMethod(LogLevel.Debug)] + static partial void M1(ILogger logger, [LogProperties(typeof(Provider), nameof(Provider.Provide1))] MyClass p1); + + [LogMethod(LogLevel.Debug)] + static partial void M2(ILogger logger, [LogProperties(typeof(Provider), nameof(Provider.Provide2))] MyClass p1); + + [LogMethod(LogLevel.Debug)] + static partial void M3(ILogger logger, [LogProperties(typeof(Provider), nameof(Provider.Provide3))] MyClass p1); + + [LogMethod(LogLevel.Debug)] + static partial void M4(ILogger logger, [LogProperties(typeof(Provider), nameof(Provider.Provide4))] MyClass p1); + + [LogMethod(LogLevel.Debug)] + static partial void M5(ILogger logger, [/*0+*/LogProperties(typeof(Provider), nameof(Provider.Provide5))/*-0*/] MyClass? p1); + }"; + + await RunGenerator(Source, DiagDescriptors.LogPropertiesProviderMethodInvalidSignature); + } + + [Theory] + [InlineData("")] + [InlineData("ILogPropertyCollector props")] + [InlineData("ILogPropertyCollector props, MyClass? value, int a")] + public async Task LogPropertiesProviderMethodParamsCount(string paramsList) + { + string source = @$" + class MyClass + {{ + public string Property {{ get; set; }} + }} + + static class Provider + {{ + public static void Provide({paramsList}) + {{ + }} + }} + + partial class C + {{ + [LogMethod(0, LogLevel.Debug, ""Parameter"")] + static partial void M(ILogger logger, [/*0+*/LogProperties(typeof(Provider), nameof(Provider.Provide))/*-0*/] MyClass p1); + }}"; + + await RunGenerator(source, DiagDescriptors.LogPropertiesProviderMethodInvalidSignature); + } + + [Theory] + [CombinatorialData] + public async Task LogPropertiesProviderMethodParamsRefKind( + [CombinatorialValues("ref", "out", "in", "")] string listModifier, + [CombinatorialValues("ref", "out", "in", "")] string valueModifier) + { + if (listModifier == string.Empty && valueModifier == string.Empty) + { + return; + } + + string source = @$" + class MyClass + {{ + public string Property {{ get; set; }} + }} + + static class Provider + {{ + public static void Provide({listModifier} ILogPropertyCollector props, {valueModifier} MyClass? value) + {{ + }} + }} + + partial class C + {{ + [LogMethod(0, LogLevel.Debug, ""Parameter"")] + static partial void M(ILogger logger, [/*0+*/LogProperties(typeof(Provider), nameof(Provider.Provide))/*-0*/] MyClass p1); + }}"; + + await RunGenerator(source, DiagDescriptors.LogPropertiesProviderMethodInvalidSignature); + } + + [Theory] + [CombinatorialData] + public async Task LogPropertiesProviderMethodParamsInvalidType( + [CombinatorialValues("ILogPropertyCollector", "MyClass?", "int", "object", "string", "DateTime")] string listType, + [CombinatorialValues("ILogPropertyCollector", "int", "string", "DateTime")] string valueType) + { + string source = @$" + class MyClass + {{ + public string Property {{ get; set; }} + }} + + static class Provider + {{ + public static void Provide({listType} props, {valueType} value) + {{ + }} + }} + + partial class C + {{ + [LogMethod(0, LogLevel.Debug, ""Parameter"")] + static partial void M(ILogger logger, [/*0+*/LogProperties(typeof(Provider), nameof(Provider.Provide))/*-0*/] MyClass p1); + }}"; + + await RunGenerator(source, DiagDescriptors.LogPropertiesProviderMethodInvalidSignature); + } + + [Theory] + [InlineData("private")] + [InlineData("")] + public async Task LogPropertiesProviderMethodIsInaccessible(string methodModifier) + { + string source = @$" + class MyClass + {{ + public string Property {{ get; set; }} + }} + + static class Provider + {{ + {methodModifier} static void Provide(ILogPropertyCollector props, MyClass? value) + {{ + return 0; + }} + }} + + partial class C + {{ + [LogMethod(0, LogLevel.Debug, ""Parameter"")] + static partial void M(ILogger logger, [/*0+*/LogProperties(typeof(Provider), nameof(Provider.Provide))/*-0*/] MyClass p1); + }}"; + + await RunGenerator(source, DiagDescriptors.LogPropertiesProviderMethodInaccessible); + } + + [Theory] + [InlineData("int")] + [InlineData("int?")] + [InlineData("System.Int32")] + [InlineData("System.Int32?")] + [InlineData("bool")] + [InlineData("bool?")] + [InlineData("System.Boolean")] + [InlineData("System.Boolean?")] + [InlineData("byte")] + [InlineData("byte?")] + [InlineData("char?")] + [InlineData("string")] + [InlineData("string?")] + [InlineData("double?")] + [InlineData("decimal?")] + [InlineData("object")] + [InlineData("object?")] + [InlineData("System.Object")] + [InlineData("System.Object?")] + [InlineData("int[]")] + [InlineData("int?[]")] + [InlineData("int[]?")] + [InlineData("int?[]?")] + [InlineData("object[]")] + [InlineData("object[]?")] + [InlineData("System.Array")] + [InlineData("System.DateTime")] + [InlineData("System.DateTimeOffset")] + [InlineData("System.TimeSpan")] + [InlineData("System.Guid")] + [InlineData("System.DateTime?")] + [InlineData("System.DateTimeOffset?")] + [InlineData("System.TimeSpan?")] + [InlineData("System.Guid?")] + [InlineData("System.IDisposable")] + [InlineData("System.Action")] + [InlineData("System.Action")] + [InlineData("System.Func")] + [InlineData("System.Nullable")] + [InlineData("System.Nullable")] + [InlineData("System.Nullable")] + [InlineData("System.Nullable")] + [InlineData("System.Nullable")] + [InlineData("System.Nullable")] + [InlineData("System.Nullable")] + [InlineData("System.Nullable")] + public async Task IneligibleTypeForPropertiesLogging(string type) + { + string source = @$" + partial class C + {{ + [LogMethod(0, LogLevel.Debug, ""No params..."")] + static partial void M(ILogger logger, [LogProperties] {type} /*0+*/test/*-0*/); + }}"; + + await RunGenerator(source, DiagDescriptors.InvalidTypeToLogProperties); + } + + [Theory] + [InlineData("ClassA")] // Type self-reference + [InlineData("ClassC")] // One-level cycle + [InlineData("ClassB")] // Two-level cycle + [InlineData("StructA")] // Custom struct + [InlineData("StructA?")] // Nullable struct + [InlineData("System.Nullable")] // Explicit nullable struct + public async Task PropertyToLogWithTypeCycle(string propertyType) + { + string source = @$" + public class ClassA + {{ + public {propertyType} Prop {{ get; set; }} + }} + + public class ClassB + {{ + public ClassC Prop {{ get; set; }} + }} + + public class ClassC + {{ + public ClassA Prop {{ get; set; }} + }} + + public struct StructA + {{ + public ClassA Prop {{ get; set; }} + }} + + partial class LoggerClass + {{ + [LogMethod(0, LogLevel.Debug, ""No params..."")] + static partial void M(ILogger logger, [LogProperties] ClassA /*0+*/test/*-0*/); + }}"; + + await RunGenerator(source, DiagDescriptors.LogPropertiesCycleDetected); + } + + [Fact] + public async Task PropertyHiddenInDerivedClass() + { + const string Source = @" + public class BaseType + { + public int Prop { get; set; } + } + + public class DerivedType : BaseType + { + public new string Prop { get; set; } + } + + partial class LoggerClass + { + [LogMethod(0, LogLevel.Debug, ""No params..."")] + static partial void M(ILogger logger, [LogProperties] DerivedType /*0+*/test/*-0*/); + }"; + + await RunGenerator(Source, DiagDescriptors.LogPropertiesHiddenPropertyDetected); + } + + [Fact] + public async Task MultipleDataClassificationAttributes() + { + const string Source = @" + using Microsoft.Extensions.Compliance.Redaction; + using Microsoft.Extensions.Compliance.Testing; + + class MyClass + { + [PrivateData] + [PrivateData] + public string? /*0+*/A/*-0*/ { get; set; } + } + + internal static partial class C + { + [LogMethod(0, LogLevel.Debug, ""Only {A}"")] + static partial void M(ILogger logger, IRedactorProvider provider, [FeedbackData] string a, [LogProperties] MyClass param); + }"; + + await RunGenerator(Source, DiagDescriptors.MultipleDataClassificationAttributes); + } + + [Theory] + [InlineData("[PrivateData]", "string")] + [InlineData("[PrivateData]", "int")] + public async Task MissingRedactorProvider(string attribute, string type) + { + string source = @$" + using Microsoft.Extensions.Compliance.Redaction; + using Microsoft.Extensions.Compliance.Testing; + + class MyClass + {{ + {attribute} + public {type} A {{ get; set; }} + }} + + internal static partial class C + {{ + [LogMethod(0, LogLevel.Debug, ""Template..."")] + static partial void M/*0+*/(ILogger logger, [LogProperties] MyClass param)/*-0*/; + }}"; + + await RunGenerator(source, DiagDescriptors.MissingRedactorProviderArgument); + } + + [Theory] + [InlineData("object")] + [InlineData("object?")] + [InlineData("System.Object")] + [InlineData("System.Object?")] + [InlineData("MyClass")] + [InlineData("MyStruct")] + [InlineData("MyInterface")] + public async Task TypeIsEligibleForLogPropertiesProvider(string type) + { + string source = @$" + internal static partial class C + {{ + class MyClass {{ }} + + struct MyStruct {{ }} + + struct MyInterface {{ }} + + public static void Provide(ILogPropertyCollector props, {type} value) + {{ + }} + + [LogMethod(0, LogLevel.Debug, ""No params..."")] + static partial void M(ILogger logger, [LogProperties(typeof(C), nameof(C.Provide))] {type} test); + }}"; + + await RunGenerator(source); + } + + [Theory] + [InlineData("int")] + [InlineData("int?")] + [InlineData("System.Int32")] + [InlineData("System.Int32?")] + [InlineData("bool")] + [InlineData("bool?")] + [InlineData("System.Boolean")] + [InlineData("System.Boolean?")] + [InlineData("byte")] + [InlineData("byte?")] + [InlineData("char?")] + [InlineData("string")] + [InlineData("string?")] + [InlineData("double?")] + [InlineData("decimal?")] + [InlineData("int[]")] + [InlineData("int?[]")] + [InlineData("int[]?")] + [InlineData("int?[]?")] + [InlineData("object[]")] + [InlineData("object[]?")] + [InlineData("System.Array")] + [InlineData("System.DateTime")] + [InlineData("System.DateTimeOffset")] + [InlineData("System.DateTime?")] + [InlineData("System.DateTimeOffset?")] + [InlineData("System.IDisposable")] + [InlineData("System.Action")] + [InlineData("System.Action")] + [InlineData("System.Func")] + [InlineData("System.Nullable")] + [InlineData("System.Nullable")] + [InlineData("System.Nullable")] + [InlineData("System.Nullable")] + [InlineData("System.Nullable")] + [InlineData("System.Nullable")] + public async Task IneligibleTypeForLogPropertiesProvider(string type) + { + string source = @$" + internal static partial class C + {{ + public static void Provide(ILogPropertyCollector props, {type} value) + {{ + }} + + [LogMethod(0, LogLevel.Debug, ""No params..."")] + static partial void M(ILogger logger, [LogProperties(typeof(C), nameof(C.Provide))] {type} /*0+*/test/*-0*/); + }}"; + + await RunGenerator(source, DiagDescriptors.InvalidTypeToLogProperties); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Unit/Common/ParserTests.cs b/test/Generators/Microsoft.Gen.Logging/Unit/Common/ParserTests.cs new file mode 100644 index 0000000000..1ba08eba6a --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Unit/Common/ParserTests.cs @@ -0,0 +1,786 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Extensions.Telemetry.Logging; +using Microsoft.Gen.Logging.Parsing; +using Microsoft.Gen.Shared; +using Xunit; + +namespace Microsoft.Gen.Logging.Test; + +public partial class ParserTests +{ + [Fact] + public async Task NullableStructEnumerable() + { + await RunGenerator(@" + using System.Collections.Generic; + + namespace TestClasses + { + public readonly struct StructEnumerable : IEnumerable + { + private static readonly List _numbers = new() { 1, 2, 3 }; + public IEnumerator GetEnumerator() => _numbers.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _numbers.GetEnumerator(); + } + + internal static partial class NullableTestExtensions + { + /// + /// A comment! + /// + [LogMethod(13, LogLevel.Error, ""M13{p1}"")] + public static partial void M13(ILogger logger, StructEnumerable p1); + + [LogMethod(14, LogLevel.Error, ""M14{p1}"")] + public static partial void M14(ILogger logger, StructEnumerable? p1); + } + }"); + } + + [Fact] + public async Task NullableLogger() + { + await RunGenerator(@" + namespace TestClasses + { + internal static partial class NullableTestExtensions + { + [LogMethod(6, LogLevel.Debug, ""M6 {p0}"")] + internal static partial void M6(ILogger? logger, string p0); + } + }"); + } + + [Fact] + public async Task MissingAttributeValue() + { + await RunGenerator(@" + internal static partial class C + { + [LogMethod(0, LogLevel.Debug, ""M0 {p0}"", EventName = )] + static partial void M0(ILogger logger, string p0); + + [LogMethod(1, LogLevel.Debug, ""M0 {p0}"", SkipEnabledChecks = )] + static partial void M1(ILogger logger, string p0); + } + "); + } + + [Fact] + public async Task WithNullLevel_GeneratorWontFail() + { + await RunGenerator(@" + partial class C + { + [LogMethod(0, null, ""This is a message with {foo}"")] + static partial void M1(ILogger logger, string foo); + } + "); + } + + [Fact] + public async Task WithNullEventId_GeneratorWontFail() + { + await RunGenerator(@" + partial class C + { + [LogMethod(null, LogLevel.Debug, ""This is a message with {foo}"")] + static partial void M1(ILogger logger, string foo); + } + "); + } + + [Fact] + public async Task WithNullMessage_GeneratorWontFail() + { + await RunGenerator(@" + partial class C + { + [LogMethod(0, LogLevel.Debug, null)] + static partial void M1(ILogger logger, string foo); + } + "); + } + + [Fact] + public async Task ParameterlessConstructor() + { + await RunGenerator(@" + partial class C + { + [LogMethod()] + static partial void M1(ILogger logger, LogLevel level, string foo); + + [LogMethod] + static partial void M2(ILogger logger, LogLevel level, string foo); + + [LogMethod(SkipEnabledChecks = true)] + static partial void M3(ILogger logger, LogLevel level, string foo); + }"); + } + + [Fact] + public async Task InvalidMethodName() + { + const string Source = @" + partial class C + { + [LogMethod(0, LogLevel.Debug, ""M1"")] + static partial void /*0+*/__M1/*-0*/(ILogger logger); + } + "; + + await RunGenerator(Source, DiagDescriptors.InvalidLoggingMethodName); + } + + [Fact] + public async Task MissingLogLevel() + { + const string Source = @" + partial class C + { + /*0+*/[LogMethod(0, ""M1"")] + static partial void M1(ILogger logger);/*-0*/ + } + "; + + await RunGenerator(Source, DiagDescriptors.MissingLogLevel); + } + + [Fact] + public async Task MissingLogLevel_WhenDefaultCtor() + { + const string Source = @" + partial class C + { + /*0+*/[LogMethod] + static partial void M1(ILogger logger);/*-0*/ + } + "; + + await RunGenerator(Source, DiagDescriptors.MissingLogLevel); + } + + [Fact] + public async Task InvalidMethodBody() + { + const string Source = @" + partial class C + { + static partial void M1(ILogger logger); + + [LogMethod(0, LogLevel.Debug, ""M1"")] + static partial void M1(ILogger logger) + /*0+*/{ + }/*-0*/ + } + "; + + await RunGenerator(Source, DiagDescriptors.LoggingMethodHasBody); + } + + [Fact] + public async Task MissingTemplate() + { + const string Source = @" + partial class C + { + [LogMethod(0, LogLevel.Debug, ""This is a message without foo"")] + static partial void M1(ILogger logger, string /*0+*/foo/*-0*/); + } + "; + + await RunGenerator(Source, DiagDescriptors.ArgumentHasNoCorrespondingTemplate); + } + + [Fact] + public async Task MissingArgument() + { + const string Source = @" + partial class C + { + [/*0+*/LogMethod(0, LogLevel.Debug, ""{foo}"")/*-0*/] + static partial void M1(ILogger logger); + } + "; + + await RunGenerator(Source, DiagDescriptors.TemplateHasNoCorrespondingArgument); + } + + [Fact] + public async Task NeedlessQualifierInMessage() + { + const string Source = @" + partial class C + { + [/*0+*/LogMethod(0, LogLevel.Information, ""INFO: this is an informative message"")/*-0*/] + static partial void M1(ILogger logger); + } + "; + + await RunGenerator(Source, DiagDescriptors.RedundantQualifierInMessage); + } + + [Fact] + public async Task NeedlessExceptionInMessage() + { + const string Source = @" + partial class C + { + [/*0+*/LogMethod(0, LogLevel.Debug, ""M1 {ex} {ex2}"")/*-0*/] + static partial void M1(ILogger logger, System.Exception ex, System.Exception ex2); + } + "; + + await RunGenerator(Source, DiagDescriptors.ShouldntMentionExceptionInMessage); + } + + [Fact] + public async Task NeedlessLogLevelInMessage() + { + const string Source = @" + partial class C + { + [/*0+*/LogMethod(0, ""M1 {l1} {l2}"")/*-0*/] + static partial void M1(ILogger logger, LogLevel l1, LogLevel l2); + } + "; + + await RunGenerator(Source, DiagDescriptors.ShouldntMentionLogLevelInMessage); + } + + [Fact] + public async Task NeedlessLoggerInMessage() + { + const string Source = @" + partial class C + { + [/*0+*/LogMethod(0, LogLevel.Debug, ""M1 {logger}"")/*-0*/] + static partial void M1(ILogger logger); + } + "; + + await RunGenerator(Source, DiagDescriptors.ShouldntMentionLoggerInMessage); + } + + [Fact] + public async Task FileScopedNamespace() + { + await RunGenerator(@" + namespace Test; + partial class C + { + [LogMethod(0, LogLevel.Debug, ""{P1}"")] + static partial void M1(ILogger logger, int p1); + }", inNamespace: false); + } + + [Theory] + [InlineData("_foo")] + [InlineData("__foo")] + [InlineData("@_foo", "_foo")] + public async Task InvalidParameterName(string name, string? template = null) + { + string source = @$" + partial class C + {{ + [LogMethod(0, LogLevel.Debug, ""M1 {{{template ?? name}}}"")] + static partial void M1(ILogger logger, string /*0+*/{name}/*-0*/); + }} + "; + + await RunGenerator(source, DiagDescriptors.InvalidLoggingMethodParameterName); + } + + [Fact] + public async Task MissingExceptionType() + { + const string Source = @" + namespace System + { + public class Object {} + public class Void {} + public class String {} + public struct DateTime {} + public class Attribute {} + } + namespace System.Collections + { + public interface IEnumerable {} + } + namespace Microsoft.Extensions.Logging + { + public enum LogLevel {} + public interface ILogger {} + } + namespace Microsoft.Extensions.Telemetry.Logging + { + public class LogMethodAttribute : System.Attribute {} + } + partial class C + { + [Microsoft.Extensions.Telemetry.Logging.LogMethodAttribute()] + public static partial void Something(this Microsoft.Extensions.Logging.ILogger logger); + }"; + + await RunGenerator(Source, DiagDescriptors.MissingRequiredType, false, includeBaseReferences: false, includeLoggingReferences: false); + } + + [Fact] + public async Task MissingEnrichmentPropertyBagTypes() + { + const string Source = @" + namespace Microsoft.Extensions.Logging + { + public enum LogLevel {} + public interface ILogger {} + } + namespace Microsoft.Extensions.Telemetry.Logging + { + public class LogMethodAttribute : System.Attribute {} + public class LogPropertiesAttribute : System.Attribute {} + public class LogPropertyIgnoreAttribute : System.Attribute {} + public class ILogPropertyCollector {} + public class LogMethodHelper { } + } + partial class C + { + [Microsoft.Extensions.Telemetry.Logging.LogMethodAttribute] + public static partial void Something(this Microsoft.Extensions.Logging.ILogger logger, Microsoft.Extensions.Logging.LogLevel level, string foo); + }"; + + await RunGenerator(Source, DiagDescriptors.MissingRequiredType, wrap: false, includeLoggingReferences: false); + } + + [Fact] + public async Task MissingLogMethodAttributeType() + { + await RunGenerator(@" + partial class C + { + } + ", includeLoggingReferences: false); + } + + [Fact] + public async Task MissingILoggerType() + { + await RunGenerator(@" + namespace Microsoft.Extensions.Telemetry.Logging + { + public sealed class LogMethodAttribute : System.Attribute {} + } + partial class C + { + } + ", includeLoggingReferences: false); + } + + [Fact] + public async Task MissingLogLevelType() + { + await RunGenerator(@" + namespace Microsoft.Extensions.Telemetry.Logging + { + public sealed class LogMethodAttribute : System.Attribute {} + } + namespace Microsoft.Extensions.Logging + { + public interface ILogger {} + } + partial class C + { + } + ", includeLoggingReferences: false); + } + + [Fact] + public async Task EventIdReuse() + { + const string Source = @" + partial class MyClass + { + [LogMethod(0, LogLevel.Debug, ""M1"")] + static partial void M1(ILogger logger); + + [/*0+*/LogMethod(0, LogLevel.Debug, ""M1"")/*-0*/] + static partial void M2(ILogger logger); + } + "; + + await RunGenerator(Source, DiagDescriptors.ShouldntReuseEventIds); + } + + [Fact] + public async Task EventNameReuse() + { + const string Source = @" + partial class MyClass + { + [LogMethod(0, LogLevel.Debug, ""M1"", EventName = ""Dog"")] + static partial void M1(ILogger logger); + + [/*0+*/LogMethod(1, LogLevel.Debug, ""M1"", EventName = ""Dog"")/*-0*/] + static partial void M2(ILogger logger); + } + "; + + await RunGenerator(Source, DiagDescriptors.ShouldntReuseEventNames); + } + + [Fact] + public async Task MethodReturnType() + { + const string Source = @" + partial class C + { + [LogMethod(0, LogLevel.Debug, ""M1"")] + public static partial /*0+*/int/*-0*/ M1(ILogger logger); + + public static partial int M1(ILogger logger) { return 0; } + } + "; + + await RunGenerator(Source, DiagDescriptors.LoggingMethodMustReturnVoid); + } + + [Fact] + public async Task MissingILogger() + { + const string Source = @" + partial class C + { + [LogMethod(0, LogLevel.Debug, ""M1 {p1}"")] + static partial void M1/*0+*/(int p1)/*-0*/; + } + "; + + await RunGenerator(Source, DiagDescriptors.MissingLoggerArgument); + } + + [Fact] + public async Task NotStatic() + { + const string Source = @" + partial class C + { + [LogMethod(0, LogLevel.Debug, ""M1"")] + partial void /*0+*/M1/*-0*/(ILogger logger); + } + "; + + await RunGenerator(Source, DiagDescriptors.LoggingMethodShouldBeStatic); + } + + [Fact] + public async Task NoILoggerField() + { + const string Source = @" + partial class C + { + [LogMethod(0, LogLevel.Debug, ""M1"")] + public partial void /*0+*/M1/*-0*/(); + } + "; + + await RunGenerator(Source, DiagDescriptors.MissingLoggerField); + } + + [Fact] + public async Task NoILoggerFieldWithRedactionProvider() + { + const string Source = @" + using Microsoft.Extensions.Compliance.Redaction; + using Microsoft.Extensions.Compliance.Testing; + + internal partial class C + { + [LogMethod(0, LogLevel.Debug, ""M {p0}"")] + partial void /*0+*/M/*-0*/(IRedactorProvider provider, [PrivateData] string p0); + }"; + + await RunGenerator(Source, DiagDescriptors.MissingLoggerField); + } + + [Fact] + public async Task MultipleILoggerFields() + { + const string Source = @" + partial class C + { + public ILogger _logger1; + public ILogger /*0+*/_logger2/*-0*/; + + [LogMethod(0, LogLevel.Debug, ""M1"")] + public partial void M1(); + } + "; + + await RunGenerator(Source, DiagDescriptors.MultipleLoggerFields); + } + + [Fact] + public async Task InstanceEmptyLoggingMethod() + { + const string Source = @" + using Microsoft.Extensions.Compliance.Redaction; + + partial class C + { + public ILogger _logger; + + [LogMethod] + public partial void /*0+*/M1/*-0*/(LogLevel level); + + [LogMethod(LogLevel.Debug)] + public partial void /*1+*/M2/*-1*/(); + + [LogMethod] + public partial void /*2+*/M3/*-2*/(LogLevel level, IRedactorProvider provider); + + [LogMethod(LogLevel.Debug)] + public partial void /*3+*/M4/*-3*/(IRedactorProvider provider); + }"; + + await RunGenerator(Source, DiagDescriptors.EmptyLoggingMethod, ignoreDiag: DiagDescriptors.MissingDataClassificationArgument); + } + + [Fact] + public async Task StaticEmptyLoggingMethod() + { + const string Source = @" + using Microsoft.Extensions.Compliance.Redaction; + + partial class C + { + [LogMethod] + public static partial void /*0+*/M1/*-0*/(ILogger logger, LogLevel level); + + [LogMethod(LogLevel.Debug)] + public static partial void /*1+*/M2/*-1*/(ILogger logger); + + [LogMethod] + public static partial void /*2+*/M3/*-2*/(ILogger logger, LogLevel level, IRedactorProvider provider); + + [LogMethod(LogLevel.Debug)] + public static partial void /*3+*/M4/*-3*/(ILogger logger, IRedactorProvider provider); + }"; + + await RunGenerator(Source, DiagDescriptors.EmptyLoggingMethod, ignoreDiag: DiagDescriptors.MissingDataClassificationArgument); + } + + [Fact] + public async Task NonEmptyLoggingMethod() + { + await RunGenerator(@" + partial class C + { + public ILogger _logger; + + [LogMethod] + public partial void M1(LogLevel level, Exception ex); + + [LogMethod(LogLevel.Debug)] + public partial void M2(Exception ex); + + [LogMethod] + public static partial void M3(ILogger logger, LogLevel level, Exception ex); + + [LogMethod(LogLevel.Debug)] + public static partial void M4(ILogger logger, Exception ex); + }"); + } + +#if ROSLYN_4_0_OR_GREATER + [Fact] + public async Task NotPartial() + { + const string Source = @" + partial class C + { + [LogMethod(0, LogLevel.Debug, ""M1"")] + static void /*0+*/M1/*-0*/(ILogger logger); + } + "; + + await RunGenerator(Source, DiagDescriptors.LoggingMethodMustBePartial); + } +#endif + + [Fact] + public async Task MethodGeneric() + { + const string Source = @" + partial class C + { + [LogMethod(0, LogLevel.Debug, ""M1"")] + static partial void M1/*0+*//*-0*/(ILogger logger); + } + "; + + await RunGenerator(Source, DiagDescriptors.LoggingMethodIsGeneric); + } + + [Theory] + [CombinatorialData] + public async Task LogMethodParamsRefKind([CombinatorialValues("ref", "out")] string modifier) + { + string source = @$" + partial class C + {{ + [LogMethod(0, LogLevel.Debug, ""Parameter {{P1}}"")] + static partial void M(ILogger logger, {modifier} int /*0+*/p1/*-0*/); + }}"; + + await RunGenerator(source, DiagDescriptors.LoggingMethodParameterRefKind); + } + + [Fact] + public async Task Templates() + { + await RunGenerator(@" + partial class C + { + [LogMethod(1, LogLevel.Debug, ""M1"")] + static partial void M1(ILogger logger); + + [LogMethod(2, LogLevel.Debug, ""M2 {arg1} {arg2}"")] + static partial void M2(ILogger logger, string arg1, string arg2); + + [LogMethod(3, LogLevel.Debug, ""M3 {arg1"")] + static partial void M3(ILogger logger); + + [LogMethod(4, LogLevel.Debug, ""M4 arg1}"")] + static partial void M4(ILogger logger); + + [LogMethod(5, LogLevel.Debug, ""M5 {"")] + static partial void M5(ILogger logger); + + [LogMethod(6, LogLevel.Debug, ""}M6 "")] + static partial void M6(ILogger logger); + + [LogMethod(7, LogLevel.Debug, ""M7 {{arg1}}"")] + static partial void M7(ILogger logger); + } + "); + } + + [Fact] + public async Task Cancellation() + { + await Assert.ThrowsAsync(async () => + await RunGenerator(@" + partial class C + { + [LogMethod(0, LogLevel.Debug, ""M1"")] + static partial void M1(ILogger logger); + } + ", cancellationToken: new CancellationToken(true))); + } + + [Fact] + public async Task SourceErrors() + { + await RunGenerator(@" + static partial class C + { + // bogus argument type + [LogMethod(0, "", ""Hello"")] + static partial void M1(ILogger logger); + + // missing parameter name + [LogMethod(1, LogLevel.Debug, ""Hello"")] + static partial void M2(ILogger); + + // bogus parameter type + [LogMethod(2, LogLevel.Debug, ""Hello"")] + static partial void M3(XILogger logger); + + // bogus enum value + [LogMethod(3, LogLevel.Foo, ""Hello"")] + static partial void M4(ILogger logger); + + // attribute applied to something other than a method + [LogMethod(4, "", ""Hello"")] + int M5; + } + "); + } + +#pragma warning disable S107 // Methods should not have too many parameters + private static async Task RunGenerator( + string code, + DiagnosticDescriptor? expectedDiagnostic = null, + bool wrap = true, + bool inNamespace = true, + bool includeBaseReferences = true, + bool includeLoggingReferences = true, + DiagnosticDescriptor? ignoreDiag = null, + CancellationToken cancellationToken = default) +#pragma warning restore S107 // Methods should not have too many parameters + { + var text = code; + if (wrap) + { + var nspaceStart = "namespace Test {"; + var nspaceEnd = "}"; + if (!inNamespace) + { + nspaceStart = ""; + nspaceEnd = ""; + } + + text = $@" + {nspaceStart} + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Telemetry.Logging; + {code} + {nspaceEnd}"; + } + + Assembly[]? refs = null; + if (includeLoggingReferences) + { + refs = new[] + { + Assembly.GetAssembly(typeof(ILogger))!, + Assembly.GetAssembly(typeof(LogMethodAttribute))!, + Assembly.GetAssembly(typeof(IEnrichmentPropertyBag))!, + Assembly.GetAssembly(typeof(DataClassification))!, + Assembly.GetAssembly(typeof(PrivateDataAttribute))!, + }; + } + + var (d, _) = await RoslynTestUtils.RunGenerator( + new Generator(), + refs, + new[] { text }, + includeBaseReferences: includeBaseReferences, + cancellationToken: cancellationToken).ConfigureAwait(false); + + if (ignoreDiag != null) + { + d = d.FilterOutDiagnostics(ignoreDiag); + } + + if (expectedDiagnostic != null) + { + RoslynTestUtils.AssertDiagnostics(text, expectedDiagnostic, d); + } + else if (d.Count > 0) + { + Assert.True(false, $"Expected no diagnostics, got {d.Count} diagnostics"); + } + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Unit/Common/ParserUtilitiesTests.cs b/test/Generators/Microsoft.Gen.Logging/Unit/Common/ParserUtilitiesTests.cs new file mode 100644 index 0000000000..db6ff26329 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Unit/Common/ParserUtilitiesTests.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Gen.Shared; +using Moq; +using Xunit; + +namespace Microsoft.Gen.Logging.Test; + +public class ParserUtilitiesTests +{ + [Fact] + public void ShouldDetect_SymbolHasModifier() + { + var propertyDeclaration = SyntaxFactory.PropertyDeclaration( + default, + SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.StaticKeyword), + SyntaxFactory.Token(SyntaxKind.PartialKeyword)), + SyntaxFactory.ParseTypeName("string"), + null!, + SyntaxFactory.Identifier("Identifier_1"), + null!); + + var anotherPropertyDeclaration = SyntaxFactory.PropertyDeclaration( + default, + SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.ProtectedKeyword), + SyntaxFactory.Token(SyntaxKind.VirtualKeyword)), + SyntaxFactory.ParseTypeName("object"), + null!, + SyntaxFactory.Identifier("Identifier_2"), + null!); + + var syntaxReferenceMock = new Mock(); + syntaxReferenceMock.Setup(x => x.GetSyntax(It.IsAny())) + .Returns(propertyDeclaration); + + var anotherSyntaxReferenceMock = new Mock(); + anotherSyntaxReferenceMock.Setup(x => x.GetSyntax(It.IsAny())) + .Returns(anotherPropertyDeclaration); + + var symbolMock = new Mock(); + symbolMock + .SetupGet(x => x.DeclaringSyntaxReferences) + .Returns(new[] { syntaxReferenceMock.Object, anotherSyntaxReferenceMock.Object }.ToImmutableArray()); + + var result = ParserUtilities.PropertyHasModifier(symbolMock.Object, SyntaxKind.ProtectedKeyword, CancellationToken.None); + Assert.True(result); + } + + [Fact] + public void ShouldDetect_SymbolHasNoModifier() + { + var propertyDeclaration = SyntaxFactory.FieldDeclaration( + default, + SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PartialKeyword)), + SyntaxFactory.VariableDeclaration(SyntaxFactory.ParseTypeName("string"))); + + var syntaxReferenceMock = new Mock(); + syntaxReferenceMock.Setup(x => x.GetSyntax(It.IsAny())) + .Returns(propertyDeclaration); + + var symbolMock = new Mock(); + symbolMock + .SetupGet(x => x.DeclaringSyntaxReferences) + .Returns(new[] { syntaxReferenceMock.Object }.ToImmutableArray()); + + var result = ParserUtilities.PropertyHasModifier(symbolMock.Object, SyntaxKind.ProtectedKeyword, CancellationToken.None); + Assert.False(result); + } + + [Fact] + public void ShouldGetSymbolAttributeWhenSymbolNull() + { + var result = ParserUtilities.GetSymbolAttributeAnnotationOrDefault(null, null!); + Assert.Null(result); + } + + [Fact] + public void Should_ReturnNull_GetLocation() + { + Assert.Null(ParserUtilities.GetLocation(null!)); + + var symbolMock = new Mock(); + symbolMock.SetupGet(x => x.Locations) + .Returns(default(ImmutableArray)); + + Assert.Null(ParserUtilities.GetLocation(symbolMock.Object)); + + symbolMock.SetupGet(x => x.Locations) + .Returns(ImmutableArray.Empty); + + Assert.Null(ParserUtilities.GetLocation(symbolMock.Object)); + } + + [Fact] + public void Should_ReturnFirstLocation_GetLocation() + { + var symbolMock = new Mock(); + var locationMock = Mock.Of(); + symbolMock.SetupGet(x => x.Locations) + .Returns(new[] { locationMock, Mock.Of() }.ToImmutableArray()); + + var result = ParserUtilities.GetLocation(symbolMock.Object); + Assert.Same(locationMock, result); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Unit/Common/SymbolLoaderTests.cs b/test/Generators/Microsoft.Gen.Logging/Unit/Common/SymbolLoaderTests.cs new file mode 100644 index 0000000000..53f8814e83 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Unit/Common/SymbolLoaderTests.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.Gen.Logging.Parsing; +using Moq; +using Moq.Protected; +using Xunit; + +namespace Microsoft.Gen.Logging.Test; + +public class SymbolLoaderTests +{ + [Theory] + [InlineData(SymbolLoader.LogMethodAttribute)] + [InlineData(SymbolLoader.LogLevelType)] + [InlineData(SymbolLoader.ILoggerType)] + [InlineData(SymbolLoader.ExceptionType, true)] + [InlineData(SymbolLoader.LogPropertiesAttribute)] + [InlineData(SymbolLoader.ILogPropertyCollectorType)] + [InlineData(SymbolLoader.LogPropertyIgnoreAttribute)] + public void Loader_ReturnsNull_WhenTypeIsUnavailable(string type, bool callbackShouldBeCalled = false) + { + var compilationMock = new Mock( + string.Empty, + Array.Empty().ToImmutableArray(), + new Dictionary(), + false, + null!, + null!); + + compilationMock + .Protected() + .Setup("CommonGetTypeByMetadataName", ItExpr.Is(t => t != type)) + .Returns(Mock.Of()); + + compilationMock + .Protected() + .Setup("CommonGetTypeByMetadataName", ItExpr.Is(t => t == type)) + .Returns((INamedTypeSymbol?)null); + + var callbackMock = new Mock>(); + var result = SymbolLoader.LoadSymbols(compilationMock.Object, callbackMock.Object); + Assert.Null(result); + if (callbackShouldBeCalled) + { + callbackMock.Verify( + x => x(It.IsAny(), It.IsAny(), It.Is(p => p != null && p.Length > 0)), + Times.Once); + } + + callbackMock.VerifyNoOtherCalls(); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Unit/Common/TemplatesExtractorTests.cs b/test/Generators/Microsoft.Gen.Logging/Unit/Common/TemplatesExtractorTests.cs new file mode 100644 index 0000000000..764ca243b7 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Unit/Common/TemplatesExtractorTests.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Gen.Logging.Parsing; +using Xunit; + +namespace Microsoft.Gen.Logging.Test; + +public class TemplatesExtractorTests +{ + [Theory] + [InlineData("c", 1)] + [InlineData("test", 4)] + [InlineData("toast", 2)] + [InlineData("October", 4)] + [InlineData("AaBbZz", 1)] + [InlineData("NewLine \n Test", 8)] + public void Should_FindIndexOfAny_Correctly(string message, int expectedResult) + { + var result = TemplateExtractor.FindIndexOfAny(message, new[] { '\n', 'a', 'b', 'z' }, 0, message.Length); + Assert.Equal(expectedResult, result); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Unit/Directory.Build.props b/test/Generators/Microsoft.Gen.Logging/Unit/Directory.Build.props new file mode 100644 index 0000000000..e1504f9ae8 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Unit/Directory.Build.props @@ -0,0 +1,32 @@ + + + + + Microsoft.Gen.Logging.Test + Unit tests for Gen.Logging. + + + + true + true + true + + + + + + + + + + + + + + + + + + + + diff --git a/test/Generators/Microsoft.Gen.Logging/Unit/Roslyn3.8/Microsoft.Gen.Logging.Roslyn3.8.Unit.Tests.csproj b/test/Generators/Microsoft.Gen.Logging/Unit/Roslyn3.8/Microsoft.Gen.Logging.Roslyn3.8.Unit.Tests.csproj new file mode 100644 index 0000000000..eac2eac217 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Unit/Roslyn3.8/Microsoft.Gen.Logging.Roslyn3.8.Unit.Tests.csproj @@ -0,0 +1,5 @@ + + + 3.8 + + diff --git a/test/Generators/Microsoft.Gen.Logging/Unit/Roslyn4.0/Microsoft.Gen.Logging.Roslyn4.0.Unit.Tests.csproj b/test/Generators/Microsoft.Gen.Logging/Unit/Roslyn4.0/Microsoft.Gen.Logging.Roslyn4.0.Unit.Tests.csproj new file mode 100644 index 0000000000..18ce9dd9ba --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/Unit/Roslyn4.0/Microsoft.Gen.Logging.Roslyn4.0.Unit.Tests.csproj @@ -0,0 +1,6 @@ + + + 4.0 + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + diff --git a/test/Generators/Microsoft.Gen.Metering/Generated/Common/MetricTests.Ext.cs b/test/Generators/Microsoft.Gen.Metering/Generated/Common/MetricTests.Ext.cs new file mode 100644 index 0000000000..aba545a9bc --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/Generated/Common/MetricTests.Ext.cs @@ -0,0 +1,307 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if false + +using System; +using System.Collections.Generic; +using System.Linq; +using TestClasses; +using Xunit; + +namespace Microsoft.Gen.Metering.Test; + +public partial class MetricTests +{ + [Fact] + public void ThrowsOnNullStrongTypeObjectExt() + { + StrongTypeHistogramExt recorder = _meter.CreateHistogramExtStrongType(); + var ex = Assert.Throws(() => recorder.Record(4L, null!)); + Assert.NotNull(ex); + + StrongTypeDecimalCounterExt counter = _meter.CreateStrongTypeDecimalCounterExt(); + ex = Assert.Throws(() => counter.Add(4M, null!)); + Assert.NotNull(ex); + } + + [Fact] + public void NonGenericCounterExtInstrumentTests() + { + CounterExt0D counter0D = _meter.CreateCounterExt0D(); + counter0D.Add(10L); + counter0D.Add(5L); + + var measurements = _collector.GetSnapshot(); + Assert.Collection(measurements, x => Assert.Equal(10L, x.GetValueOrThrow()), x => Assert.Equal(5L, x.GetValueOrThrow())); + Assert.All(measurements, x => Assert.Empty(x.Tags)); + _collector.Clear(); + + CounterExt2D counter2D = _meter.CreateCounterExt2D(); + counter2D.Add(11L, "val1", "val2"); + + var measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(11L, measurement.GetValueOrThrow()); + Assert.Equal(new (string, object?)[] { ("s1", "val1"), ("s2", "val2") }, measurement.Tags.Select(x => (x.Key, x.Value))); + } + + [Fact] + public void NonGenericHistogramExtInstrumentTests() + { + HistogramExt0D histogram0D = _meter.CreateHistogramExt0D(); + histogram0D.Record(12L); + histogram0D.Record(6L); + + var measurements = _collector.GetSnapshot(); + Assert.Collection(measurements, x => Assert.Equal(12L, x.GetValueOrThrow()), x => Assert.Equal(6L, x.GetValueOrThrow())); + Assert.All(measurements, x => Assert.Empty(x.Tags)); + _collector.Clear(); + + HistogramExt1D histogram1D = _meter.CreateHistogramExt1D(); + histogram1D.Record(17L, "val_1"); + + var measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(17L, measurement.GetValueOrThrow()); + var tag = Assert.Single(measurement.Tags); + Assert.Equal(new KeyValuePair("s1", "val_1"), tag); + } + + [Fact] + public void GenericCounterExtInstrumentTests() + { + GenericIntCounterExt0D counter0D = _meter.CreateGenericIntCounterExt0D(); + counter0D.Add(10); + counter0D.Add(5); + + var measurements = _collector.GetSnapshot(); + Assert.Collection(measurements, x => Assert.Equal(10, x.GetValueOrThrow()), x => Assert.Equal(5, x.GetValueOrThrow())); + Assert.All(measurements, x => Assert.Empty(x.Tags)); + _collector.Clear(); + + GenericIntCounterExt1D counter2D = _meter.CreateGenericIntCounterExt1D(); + counter2D.Add(11, "val1"); + + var measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(11, measurement.GetValueOrThrow()); + var tag = Assert.Single(measurement.Tags); + Assert.Equal(new KeyValuePair("s1", "val1"), tag); + } + + [Fact] + public void GenericHistogramExtInstrumentTests() + { + GenericIntHistogramExt0D histogram0D = _meter.CreateGenericIntHistogramExt0D(); + histogram0D.Record(12); + histogram0D.Record(6); + + var measurements = _collector.GetSnapshot(); + Assert.Collection(measurements, x => Assert.Equal(12, x.GetValueOrThrow()), x => Assert.Equal(6, x.GetValueOrThrow())); + Assert.All(measurements, x => Assert.Empty(x.Tags)); + _collector.Clear(); + + GenericIntHistogramExt2D histogram1D = _meter.CreateGenericIntHistogramExt2D(); + histogram1D.Record(17, "val_1", "val_2"); + + var measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(17, measurement.GetValueOrThrow()); + Assert.Equal(new (string, object?)[] { ("s1", "val_1"), ("s2", "val_2") }, measurement.Tags.Select(x => (x.Key, x.Value))); + } + + [Fact] + public void ValidateHistogramExtStrongType() + { + var histogramDimensionsTest = new HistogramDimensionsTest + { + Dim1 = "Dim1", + OperationsEnum = HistogramOperations.Operation1, + OperationsEnum2 = HistogramOperations.Operation1, + ParentOperationName = "ParentOperationName", + ChildDimensionsObject = new HistogramChildDimensions + { + Dim2 = "Dim2", + SomeDim = "SomeDime" + }, + ChildDimensionsStruct = new HistogramDimensionsStruct + { + Dim4Struct = "Dim4", + Dim5Struct = "Dim5" + }, + GrandChildrenDimensionsObject = new HistogramGrandChildrenDimensions + { + Dim3 = "Dim3", + SomeDim = "SomeDim" + } + }; + + StrongTypeHistogramExt recorder = _meter.CreateHistogramExtStrongType(); + recorder.Record(1L, histogramDimensionsTest); + + var measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(1L, measurement.GetValueOrThrow()); + Assert.NotNull(measurement.Instrument); + Assert.Equal("MyHistogramStrongTypeMetricExt", measurement.Instrument.Name); + Assert.Equal( + new (string, object?)[] + { + ("Dim1", histogramDimensionsTest.Dim1), + ("OperationsEnum", histogramDimensionsTest.OperationsEnum.ToString()), + ("Enum2", histogramDimensionsTest.OperationsEnum2.ToString()), + ("Dim2", histogramDimensionsTest.ChildDimensionsObject.Dim2), + ("dim2FromAttribute", histogramDimensionsTest.ChildDimensionsObject.SomeDim), + ("Dim3", histogramDimensionsTest.GrandChildrenDimensionsObject.Dim3), + ("Dim3FromAttribute", histogramDimensionsTest.GrandChildrenDimensionsObject.SomeDim), + ("ParentOperationName", histogramDimensionsTest.ParentOperationName), + ("Dim4Struct", histogramDimensionsTest.ChildDimensionsStruct.Dim4Struct), + ("Dim5FromAttribute", histogramDimensionsTest.ChildDimensionsStruct.Dim5Struct) + }, + measurement.Tags.Select(x => (x.Key, x.Value))); + + histogramDimensionsTest.ChildDimensionsObject = null!; + _collector.Clear(); + recorder.Record(2L, histogramDimensionsTest); + + measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(2L, measurement.GetValueOrThrow()); + Assert.NotNull(measurement.Instrument); + Assert.Equal("MyHistogramStrongTypeMetricExt", measurement.Instrument.Name); + Assert.Equal( + new (string, object?)[] + { + ("Dim1", histogramDimensionsTest.Dim1), + ("OperationsEnum", histogramDimensionsTest.OperationsEnum.ToString()), + ("Enum2", histogramDimensionsTest.OperationsEnum2.ToString()), + ("Dim2", null), + ("dim2FromAttribute", null), + ("Dim3", histogramDimensionsTest.GrandChildrenDimensionsObject.Dim3), + ("Dim3FromAttribute", histogramDimensionsTest.GrandChildrenDimensionsObject.SomeDim), + ("ParentOperationName", histogramDimensionsTest.ParentOperationName), + ("Dim4Struct", histogramDimensionsTest.ChildDimensionsStruct.Dim4Struct), + ("Dim5FromAttribute", histogramDimensionsTest.ChildDimensionsStruct.Dim5Struct) + }, + measurement.Tags.Select(x => (x.Key, x.Value))); + + histogramDimensionsTest.GrandChildrenDimensionsObject = null!; + _collector.Clear(); + recorder.Record(3L, histogramDimensionsTest); + + measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(3L, measurement.GetValueOrThrow()); + Assert.NotNull(measurement.Instrument); + Assert.Equal("MyHistogramStrongTypeMetricExt", measurement.Instrument.Name); + Assert.Equal( + new (string, object?)[] + { + ("Dim1", histogramDimensionsTest.Dim1), + ("OperationsEnum", histogramDimensionsTest.OperationsEnum.ToString()), + ("Enum2", histogramDimensionsTest.OperationsEnum2.ToString()), + ("Dim2", null), + ("dim2FromAttribute", null), + ("Dim3", null), + ("Dim3FromAttribute", null), + ("ParentOperationName", histogramDimensionsTest.ParentOperationName), + ("Dim4Struct", histogramDimensionsTest.ChildDimensionsStruct.Dim4Struct), + ("Dim5FromAttribute", histogramDimensionsTest.ChildDimensionsStruct.Dim5Struct) + }, + measurement.Tags.Select(x => (x.Key, x.Value))); + } + + [Fact] + public void ValidateCounterExtStrongType() + { + var counterDimensionsTest = new CounterDimensions + { + OperationsEnum = CounterOperations.Operation1, + OperationsEnum2 = CounterOperations.Operation1, + ParentOperationName = "ParentOperationName", + ChildDimensionsObject = new CounterChildDimensions + { + Dim2 = "Dim2", + SomeDim = "SomeDime" + }, + ChildDimensionsStruct = new CounterDimensionsStruct + { + Dim4Struct = "Dim4", + Dim5Struct = "Dim5" + }, + GrandChildDimensionsObject = new CounterGrandChildCounterDimensions + { + Dim3 = "Dim3", + SomeDim = "SomeDim" + }, + Dim1 = "Dim1", + }; + + StrongTypeDecimalCounterExt counter = _meter.CreateStrongTypeDecimalCounterExt(); + counter.Add(1M, counterDimensionsTest); + + var measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(1M, measurement.GetValueOrThrow()); + Assert.NotNull(measurement.Instrument); + Assert.Equal("MyCounterStrongTypeMetricExt", measurement.Instrument.Name); + Assert.Equal( + new (string, object?)[] + { + ("Dim1", counterDimensionsTest.Dim1), + ("OperationsEnum", counterDimensionsTest.OperationsEnum.ToString()), + ("Enum2", counterDimensionsTest.OperationsEnum2.ToString()), + ("Dim2", counterDimensionsTest.ChildDimensionsObject.Dim2), + ("dim2FromAttribute", counterDimensionsTest.ChildDimensionsObject.SomeDim), + ("Dim3", counterDimensionsTest.GrandChildDimensionsObject.Dim3), + ("Dim3FromAttribute", counterDimensionsTest.GrandChildDimensionsObject.SomeDim), + ("ParentOperationName", counterDimensionsTest.ParentOperationName), + ("Dim4Struct", counterDimensionsTest.ChildDimensionsStruct.Dim4Struct), + ("Dim5FromAttribute", counterDimensionsTest.ChildDimensionsStruct.Dim5Struct) + }, + measurement.Tags.Select(x => (x.Key, x.Value))); + + counterDimensionsTest.ChildDimensionsObject = null!; + _collector.Clear(); + counter.Add(2M, counterDimensionsTest); + + measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(2M, measurement.GetValueOrThrow()); + Assert.NotNull(measurement.Instrument); + Assert.Equal("MyCounterStrongTypeMetricExt", measurement.Instrument.Name); + Assert.Equal( + new (string, object?)[] + { + ("Dim1", counterDimensionsTest.Dim1), + ("OperationsEnum", counterDimensionsTest.OperationsEnum.ToString()), + ("Enum2", counterDimensionsTest.OperationsEnum2.ToString()), + ("Dim2", null), + ("dim2FromAttribute", null), + ("Dim3", counterDimensionsTest.GrandChildDimensionsObject.Dim3), + ("Dim3FromAttribute", counterDimensionsTest.GrandChildDimensionsObject.SomeDim), + ("ParentOperationName", counterDimensionsTest.ParentOperationName), + ("Dim4Struct", counterDimensionsTest.ChildDimensionsStruct.Dim4Struct), + ("Dim5FromAttribute", counterDimensionsTest.ChildDimensionsStruct.Dim5Struct) + }, + measurement.Tags.Select(x => (x.Key, x.Value))); + + counterDimensionsTest.GrandChildDimensionsObject = null!; + _collector.Clear(); + counter.Add(3M, counterDimensionsTest); + + measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(3M, measurement.GetValueOrThrow()); + Assert.NotNull(measurement.Instrument); + Assert.Equal("MyCounterStrongTypeMetricExt", measurement.Instrument.Name); + Assert.Equal( + new (string, object?)[] + { + ("Dim1", counterDimensionsTest.Dim1), + ("OperationsEnum", counterDimensionsTest.OperationsEnum.ToString()), + ("Enum2", counterDimensionsTest.OperationsEnum2.ToString()), + ("Dim2", null), + ("dim2FromAttribute", null), + ("Dim3", null), + ("Dim3FromAttribute", null), + ("ParentOperationName", counterDimensionsTest.ParentOperationName), + ("Dim4Struct", counterDimensionsTest.ChildDimensionsStruct.Dim4Struct), + ("Dim5FromAttribute", counterDimensionsTest.ChildDimensionsStruct.Dim5Struct) + }, + measurement.Tags.Select(x => (x.Key, x.Value))); + } +} + +#endif diff --git a/test/Generators/Microsoft.Gen.Metering/Generated/Common/MetricTests.cs b/test/Generators/Microsoft.Gen.Metering/Generated/Common/MetricTests.cs new file mode 100644 index 0000000000..18c8adeda3 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/Generated/Common/MetricTests.cs @@ -0,0 +1,613 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if false + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Globalization; +using System.Linq; +using Microsoft.Extensions.Telemetry.Metering; +using TestClasses; +using Xunit; + +namespace Microsoft.Gen.Metering.Test; + +public partial class MetricTests : IDisposable +{ + private const string BaseMeterName = "Microsoft.GeneratedCode.Test.Metering." + nameof(MetricTests) + "."; + + private readonly Meter _meter; + private readonly FakeMeteringCollector _collector; + private readonly string _meterName; + private bool _disposedValue; + + public MetricTests() + { + _meterName = BaseMeterName + Guid.NewGuid().ToString("d", CultureInfo.InvariantCulture); + _meter = new Meter(_meterName); + _collector = new FakeMeteringCollector(new HashSet { _meterName }); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _meter.Dispose(); + _collector.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + [Fact] + public void NonGenericCounter0DInstrumentTests() + { + Counter0D counter0D = CounterTestExtensions.CreateCounter0D(_meter); + counter0D.Add(10L); + counter0D.Add(5L); + + var measurements = _collector.GetSnapshot(); + Assert.Collection(measurements, x => Assert.Equal(10L, x.GetValueOrThrow()), x => Assert.Equal(5L, x.GetValueOrThrow())); + Assert.All(measurements, x => Assert.Empty(x.Tags)); + } + + [Fact] + public void NonGenericCounter2DInstrumentTests() + { + const long Value = int.MaxValue + 4L; + + Counter2D counter2D = CounterTestExtensions.CreateCounter2D(_meter); + counter2D.Add(Value, "val1", "val2"); + + var measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(Value, measurement.GetValueOrThrow()); + Assert.Equal(new (string, object?)[] { ("s1", "val1"), ("s2", "val2") }, measurement.Tags.Select(x => (x.Key, x.Value))); + } + + [Fact] + public void NonGenericHistogram0DInstrumentTests() + { + Histogram0D histogram0D = HistogramTestExtensions.CreateHistogram0D(_meter); + histogram0D.Record(12L); + histogram0D.Record(6L); + + var measurements = _collector.GetSnapshot(); + Assert.Collection(measurements, x => Assert.Equal(12L, x.GetValueOrThrow()), x => Assert.Equal(6L, x.GetValueOrThrow())); + Assert.All(measurements, x => Assert.Empty(x.Tags)); + } + + [Fact] + public void NonGenericHistogram1DInstrumentTests() + { + const long Value = int.MaxValue + 3L; + + Histogram1D histogram1D = HistogramTestExtensions.CreateHistogram1D(_meter); + histogram1D.Record(Value, "val_1"); + + var measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(Value, measurement.GetValueOrThrow()); + var tag = Assert.Single(measurement.Tags); + Assert.Equal(new KeyValuePair("s1", "val_1"), tag); + } + + [Fact] + public void GenericCounter0DInstrumentTests() + { + GenericIntCounter0D counter0D = CounterTestExtensions.CreateGenericIntCounter0D(_meter); + counter0D.Add(10); + counter0D.Add(5); + + var measurements = _collector.GetSnapshot(); + Assert.Collection(measurements, x => Assert.Equal(10, x.GetValueOrThrow()), x => Assert.Equal(5, x.GetValueOrThrow())); + Assert.All(measurements, x => Assert.Empty(x.Tags)); + } + + [Fact] + public void GenericCounter2DInstrumentTests() + { + const int Value = int.MaxValue - 1; + + GenericIntCounter1D counter2D = CounterTestExtensions.CreateGenericIntCounter1D(_meter); + counter2D.Add(Value, "val1"); + + var measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(Value, measurement.GetValueOrThrow()); + var tag = Assert.Single(measurement.Tags); + Assert.Equal(new KeyValuePair("s1", "val1"), tag); + } + + [Fact] + public void GenericHistogram0DInstrumentTests() + { + GenericIntHistogram0D histogram0D = HistogramTestExtensions.CreateGenericIntHistogram0D(_meter); + histogram0D.Record(12); + histogram0D.Record(6); + + var measurements = _collector.GetSnapshot(); + Assert.Collection(measurements, x => Assert.Equal(12, x.GetValueOrThrow()), x => Assert.Equal(6, x.GetValueOrThrow())); + Assert.All(measurements, x => Assert.Empty(x.Tags)); + } + + [Fact] + public void GenericHistogram2DInstrumentTests() + { + const int Value = short.MaxValue + 2; + + GenericIntHistogram2D histogram1D = HistogramTestExtensions.CreateGenericIntHistogram2D(_meter); + histogram1D.Record(Value, "val_1", "val_2"); + + var measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(Value, measurement.GetValueOrThrow()); + Assert.Equal(new (string, object?)[] { ("s1", "val_1"), ("s2", "val_2") }, measurement.Tags.Select(x => (x.Key, x.Value))); + } + + [Fact] + public void CreateOnExistingCounter() + { + const long Value = int.MaxValue + 2L; + + Counter4D counter4D = CounterTestExtensions.CreateCounter4D(_meter); + Counter4D newCounter4D = CounterTestExtensions.CreateCounter4D(_meter); + Assert.Same(counter4D, newCounter4D); + + counter4D.Add(Value, "val1", "val2", "val3", "val4"); + newCounter4D.Add(Value, "val3", "val4", "val5", "val6"); + + Assert.Equal(2, _collector.Count); + var measurements = _collector.GetSnapshot(); + Assert.All(measurements, x => Assert.Equal(Value, x.GetValueOrThrow())); + Assert.Same(measurements[0].Instrument, measurements[1].Instrument); + + var tags = measurements[0].Tags.Select(x => (x.Key, x.Value)); + Assert.Equal(new (string, object?)[] { ("s1", "val1"), ("s2", "val2"), ("s3", "val3"), ("s4", "val4") }, tags); + + tags = measurements[1].Tags.Select(x => (x.Key, x.Value)); + Assert.Equal(new (string, object?)[] { ("s1", "val3"), ("s2", "val4"), ("s3", "val5"), ("s4", "val6") }, tags); + } + + [Fact] + public void CreateOnExistingHistogram() + { + const long Value = int.MaxValue + 1L; + + Histogram4D histogram4D = HistogramTestExtensions.CreateHistogram4D(_meter); + Histogram4D newHistogram4D = HistogramTestExtensions.CreateHistogram4D(_meter); + Assert.Same(histogram4D, newHistogram4D); + + histogram4D.Record(Value, "val1", "val2", "val3", "val4"); + newHistogram4D.Record(Value, "val3", "val4", "val5", "val6"); + + Assert.Equal(2, _collector.Count); + var measurements = _collector.GetSnapshot(); + Assert.All(measurements, x => Assert.Equal(Value, x.GetValueOrThrow())); + Assert.Same(measurements[0].Instrument, measurements[1].Instrument); + + var tags = measurements[0].Tags.Select(x => (x.Key, x.Value)); + Assert.Equal(new (string, object?)[] { ("s1", "val1"), ("s2", "val2"), ("s3", "val3"), ("s4", "val4") }, tags); + + tags = measurements[1].Tags.Select(x => (x.Key, x.Value)); + Assert.Equal(new (string, object?)[] { ("s1", "val3"), ("s2", "val4"), ("s3", "val5"), ("s4", "val6") }, tags); + } + + [Fact] + public void CreateOnExistingCounter_WithDifferentMeterName_ShouldReturnNewMetric() + { + using var meter2 = new Meter(_meterName + "2"); + Counter3D counter = CounterTestExtensions.CreateCounter3D(_meter); + + // "Create()" with another meter name should return a different counter object + Counter3D counterWithDifferentMeterName = CounterTestExtensions.CreateCounter3D(meter2); + Assert.NotNull(counterWithDifferentMeterName); + Assert.NotSame(counter, counterWithDifferentMeterName); + + Histogram3D histogram = HistogramTestExtensions.CreateHistogram3D(_meter); + + // "Create()" with another meter name should return a different histogram object + Histogram3D histogramWithDifferentMeterName = HistogramTestExtensions.CreateHistogram3D(meter2); + Assert.NotNull(histogramWithDifferentMeterName); + Assert.NotSame(histogram, histogramWithDifferentMeterName); + } + + [Fact] + public void CreateOnExistingCounter_WithSameMeterName_ShouldReturnDifferentMetric() + { + using var meter2 = new Meter(_meterName); + Counter3D counter = CounterTestExtensions.CreateCounter3D(_meter); + + // "Create()" with the same meter name should return a different counter object + Counter3D counterWithSameMeterName = CounterTestExtensions.CreateCounter3D(meter2); + Assert.NotNull(counterWithSameMeterName); + Assert.NotSame(counter, counterWithSameMeterName); + + Histogram3D histogram3D = HistogramTestExtensions.CreateHistogram3D(_meter); + + // "Create()" with the same meter name should return a different histogram object + Histogram3D histogramWithSameMeterName = HistogramTestExtensions.CreateHistogram3D(meter2); + Assert.NotNull(histogramWithSameMeterName); + Assert.NotSame(histogram3D, histogramWithSameMeterName); + } + + [Fact] + public void ValidateCounterWithDifferentDimensions() + { + Counter2D counter2D = CounterTestExtensions.CreateCounter2D(_meter); + + counter2D.Add(17L, "val1", "val2"); + + var measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(17L, measurement.GetValueOrThrow()); + Assert.Equal(new (string, object?)[] { ("s1", "val1"), ("s2", "val2") }, measurement.Tags.Select(x => (x.Key, x.Value))); + _collector.Clear(); + + counter2D.Add(5L, "val1", "val2"); + measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(5L, measurement.GetValueOrThrow()); + Assert.Equal(new (string, object?)[] { ("s1", "val1"), ("s2", "val2") }, measurement.Tags.Select(x => (x.Key, x.Value))); + _collector.Clear(); + + // Different Dimensions + counter2D.Add(5L, "val1", "val4"); + measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(5L, measurement.GetValueOrThrow()); + Assert.Equal(new (string, object?)[] { ("s1", "val1"), ("s2", "val4") }, measurement.Tags.Select(x => (x.Key, x.Value))); + } + + [Fact] + public void ValidateHistogramWithDifferentDimensions() + { + Histogram2D histogram = HistogramTestExtensions.CreateHistogram2D(_meter); + histogram.Record(10L, "val1", "val2"); + + var measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(10L, measurement.GetValueOrThrow()); + Assert.Equal(new (string, object?)[] { ("s1", "val1"), ("s2", "val2") }, measurement.Tags.Select(x => (x.Key, x.Value))); + _collector.Clear(); + + histogram.Record(5L, "val1", "val2"); + measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(5L, measurement.GetValueOrThrow()); + Assert.Equal(new (string, object?)[] { ("s1", "val1"), ("s2", "val2") }, measurement.Tags.Select(x => (x.Key, x.Value))); + _collector.Clear(); + + // Different Dimensions + histogram.Record(5L, "val1", "val4"); + measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(5L, measurement.GetValueOrThrow()); + Assert.Equal(new (string, object?)[] { ("s1", "val1"), ("s2", "val4") }, measurement.Tags.Select(x => (x.Key, x.Value))); + } + +#if ROSLYN_4_0_OR_GREATER + [Fact] + public void ValidateCounterWithFileScopedNamespace() + { + var longCunter = FileScopedExtensions.CreateCounter(_meter); + longCunter.Add(12L); + + var measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(12L, measurement.GetValueOrThrow()); + Assert.Empty(measurement.Tags); + _collector.Clear(); + + var genericDoubleCounter = FileScopedExtensions.CreateGenericDoubleCounter(_meter); + genericDoubleCounter.Add(1.05D); + + measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(1.05D, measurement.GetValueOrThrow()); + Assert.Empty(measurement.Tags); + } +#endif + + [Fact] + public void ValidateCounterWithVariableParamsDimensions() + { + CounterWithVariableParams counter = CounterTestExtensions.CreateCounterWithVariableParams(_meter); + + counter.Add(100_500L, Dim1: "val1", Dim_2: "val2", Dim_3: "val3"); + + var measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(100_500L, measurement.GetValueOrThrow()); + Assert.NotNull(measurement.Instrument); + Assert.Equal("MyCounterMetric", measurement.Instrument.Name); + Assert.Equal(new (string, object?)[] { ("Dim1", "val1"), ("Dim_2", "val2"), ("Dim_3", "val3") }, measurement.Tags.Select(x => (x.Key, x.Value))); + } + + [Fact] + public void ValidateHistogramWithVariableParamsDimensions() + { + HistogramWithVariableParams histogram = HistogramTestExtensions.CreateHistogramWithVariableParams(_meter); + + histogram.Record(100L, Dim1: "val1", Dim_2: "val2", Dim_3: "val3"); + + var measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(100L, measurement.GetValueOrThrow()); + Assert.NotNull(measurement.Instrument); + Assert.Equal("MyHistogramMetric", measurement.Instrument.Name); + Assert.Equal(new (string, object?)[] { ("Dim1", "val1"), ("Dim_2", "val2"), ("Dim_3", "val3") }, measurement.Tags.Select(x => (x.Key, x.Value))); + } + + [Fact] + public void ValidateHistogramStructType() + { + var histogramStruct = new HistogramStruct + { + Dim1 = "Dim1", + Dim2 = "Dim2", + DimInField = "Dim in field", + Operations = HistogramOperations.Operation1, + Operations2 = HistogramOperations.Operation1 + }; + + StructTypeHistogram recorder = HistogramTestExtensions.CreateHistogramStructType(_meter); + recorder.Record(10L, histogramStruct); + + var measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(10L, measurement.GetValueOrThrow()); + Assert.NotNull(measurement.Instrument); + Assert.Equal("MyHistogramStructTypeMetric", measurement.Instrument.Name); + Assert.Equal( + new (string, object?)[] + { + ("Dim1", histogramStruct.Dim1), + ("DimInField", histogramStruct.DimInField), + ("Dim2_FromAttribute", histogramStruct.Dim2), + ("Operations", histogramStruct.Operations.ToString()), + ("Operations_FromAttribute", histogramStruct.Operations2.ToString()) + }, + measurement.Tags.Select(x => (x.Key, x.Value))); + } + + [Fact] + public void ValidateHistogramStrongType() + { + var histogramDimensionsTest = new HistogramDimensionsTest + { + Dim1 = "Dim1", + OperationsEnum = HistogramOperations.Operation1, + OperationsEnum2 = HistogramOperations.Operation1, + ParentOperationName = "ParentOperationName", + ChildDimensionsObject = new HistogramChildDimensions + { + Dim2 = "Dim2", + SomeDim = "SomeDime" + }, + ChildDimensionsStruct = new HistogramDimensionsStruct + { + Dim4Struct = "Dim4", + Dim5Struct = "Dim5" + }, + GrandChildrenDimensionsObject = new HistogramGrandChildrenDimensions + { + Dim3 = "Dim3", + SomeDim = "SomeDim" + } + }; + + StrongTypeHistogram recorder = HistogramTestExtensions.CreateHistogramStrongType(_meter); + recorder.Record(1L, histogramDimensionsTest); + + var measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(1L, measurement.GetValueOrThrow()); + Assert.NotNull(measurement.Instrument); + Assert.Equal("MyHistogramStrongTypeMetric", measurement.Instrument.Name); + Assert.Equal( + new (string, object?)[] + { + ("Dim1", histogramDimensionsTest.Dim1), + ("OperationsEnum", histogramDimensionsTest.OperationsEnum.ToString()), + ("Enum2", histogramDimensionsTest.OperationsEnum2.ToString()), + ("Dim2", histogramDimensionsTest.ChildDimensionsObject.Dim2), + ("dim2FromAttribute", histogramDimensionsTest.ChildDimensionsObject.SomeDim), + ("Dim3", histogramDimensionsTest.GrandChildrenDimensionsObject.Dim3), + ("Dim3FromAttribute", histogramDimensionsTest.GrandChildrenDimensionsObject.SomeDim), + ("ParentOperationName", histogramDimensionsTest.ParentOperationName), + ("Dim4Struct", histogramDimensionsTest.ChildDimensionsStruct.Dim4Struct), + ("Dim5FromAttribute", histogramDimensionsTest.ChildDimensionsStruct.Dim5Struct) + }, + measurement.Tags.Select(x => (x.Key, x.Value))); + + histogramDimensionsTest.ChildDimensionsObject = null!; + _collector.Clear(); + + recorder.Record(2L, histogramDimensionsTest); + + measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(2L, measurement.GetValueOrThrow()); + Assert.NotNull(measurement.Instrument); + Assert.Equal("MyHistogramStrongTypeMetric", measurement.Instrument.Name); + Assert.Equal( + new (string, object?)[] + { + ("Dim1", histogramDimensionsTest.Dim1), + ("OperationsEnum", histogramDimensionsTest.OperationsEnum.ToString()), + ("Enum2", histogramDimensionsTest.OperationsEnum2.ToString()), + ("Dim2", null), + ("dim2FromAttribute", null), + ("Dim3", histogramDimensionsTest.GrandChildrenDimensionsObject.Dim3), + ("Dim3FromAttribute", histogramDimensionsTest.GrandChildrenDimensionsObject.SomeDim), + ("ParentOperationName", histogramDimensionsTest.ParentOperationName), + ("Dim4Struct", histogramDimensionsTest.ChildDimensionsStruct.Dim4Struct), + ("Dim5FromAttribute", histogramDimensionsTest.ChildDimensionsStruct.Dim5Struct) + }, + measurement.Tags.Select(x => (x.Key, x.Value))); + + histogramDimensionsTest.GrandChildrenDimensionsObject = null!; + _collector.Clear(); + + recorder.Record(3L, histogramDimensionsTest); + + measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(3L, measurement.GetValueOrThrow()); + Assert.NotNull(measurement.Instrument); + Assert.Equal("MyHistogramStrongTypeMetric", measurement.Instrument.Name); + Assert.Equal( + new (string, object?)[] + { + ("Dim1", histogramDimensionsTest.Dim1), + ("OperationsEnum", histogramDimensionsTest.OperationsEnum.ToString()), + ("Enum2", histogramDimensionsTest.OperationsEnum2.ToString()), + ("Dim2", null), + ("dim2FromAttribute", null), + ("Dim3", null), + ("Dim3FromAttribute", null), + ("ParentOperationName", histogramDimensionsTest.ParentOperationName), + ("Dim4Struct", histogramDimensionsTest.ChildDimensionsStruct.Dim4Struct), + ("Dim5FromAttribute", histogramDimensionsTest.ChildDimensionsStruct.Dim5Struct) + }, + measurement.Tags.Select(x => (x.Key, x.Value))); + } + + [Fact] + public void ThrowsOnNullStrongTypeObject() + { + StrongTypeHistogram recorder = HistogramTestExtensions.CreateHistogramStrongType(_meter); + var ex = Assert.Throws(() => recorder.Record(4L, null!)); + Assert.NotNull(ex); + + StrongTypeDecimalCounter counter = CounterTestExtensions.CreateStrongTypeDecimalCounter(_meter); + ex = Assert.Throws(() => counter.Add(4M, null!)); + Assert.NotNull(ex); + } + + [Fact] + public void ValidateCounterStructType() + { + var counterStruct = new CounterStructDimensions + { + Dim1 = "Dim1", + Dim2 = "Dim2", + DimInField = "Dim in field", + Operations = CounterOperations.Operation1, + Operations2 = CounterOperations.Operation1 + }; + + StructTypeCounter recorder = CounterTestExtensions.CreateCounterStructType(_meter); + recorder.Add(11L, counterStruct); + + var measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(11L, measurement.GetValueOrThrow()); + Assert.NotNull(measurement.Instrument); + Assert.Equal("MyCounterStructTypeMetric", measurement.Instrument.Name); + Assert.Equal( + new (string, object?)[] + { + ("Dim1", counterStruct.Dim1), + ("DimInField", counterStruct.DimInField), + ("Dim2_FromAttribute", counterStruct.Dim2), + ("Operations", counterStruct.Operations.ToString()), + ("Operations_FromAttribute", counterStruct.Operations2.ToString()) + }, + measurement.Tags.Select(x => (x.Key, x.Value))); + } + + [Fact] + public void ValidateCounterStrongType() + { + var counterDimensionsTest = new CounterDimensions + { + OperationsEnum = CounterOperations.Operation1, + OperationsEnum2 = CounterOperations.Operation1, + ParentOperationName = "ParentOperationName", + ChildDimensionsObject = new CounterChildDimensions + { + Dim2 = "Dim2", + SomeDim = "SomeDime" + }, + ChildDimensionsStruct = new CounterDimensionsStruct + { + Dim4Struct = "Dim4", + Dim5Struct = "Dim5" + }, + GrandChildDimensionsObject = new CounterGrandChildCounterDimensions + { + Dim3 = "Dim3", + SomeDim = "SomeDim" + }, + Dim1 = "Dim1", + }; + + StrongTypeDecimalCounter counter = CounterTestExtensions.CreateStrongTypeDecimalCounter(_meter); + counter.Add(1M, counterDimensionsTest); + + var measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(1M, measurement.GetValueOrThrow()); + Assert.NotNull(measurement.Instrument); + Assert.Equal("MyCounterStrongTypeMetric", measurement.Instrument.Name); + Assert.Equal( + new (string, object?)[] + { + ("Dim1", counterDimensionsTest.Dim1), + ("OperationsEnum", counterDimensionsTest.OperationsEnum.ToString()), + ("Enum2", counterDimensionsTest.OperationsEnum2.ToString()), + ("Dim2", counterDimensionsTest.ChildDimensionsObject.Dim2), + ("dim2FromAttribute", counterDimensionsTest.ChildDimensionsObject.SomeDim), + ("Dim3", counterDimensionsTest.GrandChildDimensionsObject.Dim3), + ("Dim3FromAttribute", counterDimensionsTest.GrandChildDimensionsObject.SomeDim), + ("ParentOperationName", counterDimensionsTest.ParentOperationName), + ("Dim4Struct", counterDimensionsTest.ChildDimensionsStruct.Dim4Struct), + ("Dim5FromAttribute", counterDimensionsTest.ChildDimensionsStruct.Dim5Struct) + }, + measurement.Tags.Select(x => (x.Key, x.Value))); + + counterDimensionsTest.ChildDimensionsObject = null!; + _collector.Clear(); + + counter.Add(2M, counterDimensionsTest); + + measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(2M, measurement.GetValueOrThrow()); + Assert.NotNull(measurement.Instrument); + Assert.Equal("MyCounterStrongTypeMetric", measurement.Instrument.Name); + Assert.Equal( + new (string, object?)[] + { + ("Dim1", counterDimensionsTest.Dim1), + ("OperationsEnum", counterDimensionsTest.OperationsEnum.ToString()), + ("Enum2", counterDimensionsTest.OperationsEnum2.ToString()), + ("Dim2", null), + ("dim2FromAttribute", null), + ("Dim3", counterDimensionsTest.GrandChildDimensionsObject.Dim3), + ("Dim3FromAttribute", counterDimensionsTest.GrandChildDimensionsObject.SomeDim), + ("ParentOperationName", counterDimensionsTest.ParentOperationName), + ("Dim4Struct", counterDimensionsTest.ChildDimensionsStruct.Dim4Struct), + ("Dim5FromAttribute", counterDimensionsTest.ChildDimensionsStruct.Dim5Struct) + }, + measurement.Tags.Select(x => (x.Key, x.Value))); + + counterDimensionsTest.GrandChildDimensionsObject = null!; + _collector.Clear(); + counter.Add(3M, counterDimensionsTest); + + measurement = Assert.Single(_collector.GetSnapshot()); + Assert.Equal(3M, measurement.GetValueOrThrow()); + Assert.NotNull(measurement.Instrument); + Assert.Equal("MyCounterStrongTypeMetric", measurement.Instrument.Name); + Assert.Equal( + new (string, object?)[] + { + ("Dim1", counterDimensionsTest.Dim1), + ("OperationsEnum", counterDimensionsTest.OperationsEnum.ToString()), + ("Enum2", counterDimensionsTest.OperationsEnum2.ToString()), + ("Dim2", null), + ("dim2FromAttribute", null), + ("Dim3", null), + ("Dim3FromAttribute", null), + ("ParentOperationName", counterDimensionsTest.ParentOperationName), + ("Dim4Struct", counterDimensionsTest.ChildDimensionsStruct.Dim4Struct), + ("Dim5FromAttribute", counterDimensionsTest.ChildDimensionsStruct.Dim5Struct) + }, + measurement.Tags.Select(x => (x.Key, x.Value))); + } +} + +#endif diff --git a/test/Generators/Microsoft.Gen.Metering/Generated/Directory.Build.props b/test/Generators/Microsoft.Gen.Metering/Generated/Directory.Build.props new file mode 100644 index 0000000000..15aba1b631 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/Generated/Directory.Build.props @@ -0,0 +1,30 @@ + + + + + Microsoft.Gen.Metering.Tes + Tests for code generated by Gen.Metering. + + + + $(NetCoreTargetFrameworks) + $(NetCoreTargetFrameworks)$(ConditionalNet462) + true + true + $(NoWarn);IDE0161;S1144 + + + + + + + + + + + + + + + + diff --git a/test/Generators/Microsoft.Gen.Metering/Generated/Roslyn3.8/Microsoft.Gen.Metering.Roslyn3.8.Generated.Tests.csproj b/test/Generators/Microsoft.Gen.Metering/Generated/Roslyn3.8/Microsoft.Gen.Metering.Roslyn3.8.Generated.Tests.csproj new file mode 100644 index 0000000000..eac2eac217 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/Generated/Roslyn3.8/Microsoft.Gen.Metering.Roslyn3.8.Generated.Tests.csproj @@ -0,0 +1,5 @@ + + + 3.8 + + diff --git a/test/Generators/Microsoft.Gen.Metering/Generated/Roslyn4.0/Microsoft.Gen.Metering.Roslyn4.0.Generated.Tests.csproj b/test/Generators/Microsoft.Gen.Metering/Generated/Roslyn4.0/Microsoft.Gen.Metering.Roslyn4.0.Generated.Tests.csproj new file mode 100644 index 0000000000..0cd2e6b265 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/Generated/Roslyn4.0/Microsoft.Gen.Metering.Roslyn4.0.Generated.Tests.csproj @@ -0,0 +1,6 @@ + + + 4.0 + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + diff --git a/test/Generators/Microsoft.Gen.Metering/TestClasses/AttributedWithoutNamespace.cs b/test/Generators/Microsoft.Gen.Metering/TestClasses/AttributedWithoutNamespace.cs new file mode 100644 index 0000000000..cb0cac517a --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/TestClasses/AttributedWithoutNamespace.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +[SuppressMessage("Usage", "CA1801:Review unused parameters", + Justification = "For testing emitter for classes without namespace")] +[SuppressMessage("Readability", "R9A046:Source generated metrics (fast metrics) should be located in 'Metric' class", + Justification = "Metering generator tests")] +internal static partial class InstrumentsWithoutNamespace +{ + [Counter] + public static partial NoNamespaceCounterInstrument CreatePublicCounter(Meter meter); + + [Histogram] + public static partial NoNamespaceHistogramInstrument CreatePublicHistogram(Meter meter); +} diff --git a/test/Generators/Microsoft.Gen.Metering/TestClasses/CounterDimensions.cs b/test/Generators/Microsoft.Gen.Metering/TestClasses/CounterDimensions.cs new file mode 100644 index 0000000000..e52e0a0519 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/TestClasses/CounterDimensions.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Telemetry.Metering; + +namespace TestClasses +{ +#pragma warning disable SA1402 // File may only contain a single type + public class CounterDimensions : CounterParentDimensions + { + public string? Dim1; + + public CounterOperations OperationsEnum { get; set; } + + [Dimension("Enum2")] + public CounterOperations OperationsEnum2 { get; set; } + + public CounterChildDimensions? ChildDimensionsObject { get; set; } + + public CounterGrandChildCounterDimensions? GrandChildDimensionsObject { get; set; } + } + + public enum CounterOperations + { + Unknown = 0, + Operation1 = 1, + } + + public class CounterParentDimensions + { + public string? ParentOperationName { get; set; } + + public CounterDimensionsStruct ChildDimensionsStruct { get; set; } + } + + public class CounterChildDimensions + { + public string? Dim2 { get; set; } + + [Dimension("dim2FromAttribute")] + public string? SomeDim; + } + + public struct CounterDimensionsStruct + { + public string Dim4Struct { get; set; } + + [Dimension("Dim5FromAttribute")] + public string Dim5Struct { get; set; } + } + + public class CounterGrandChildCounterDimensions + { + public string? Dim3 { get; set; } + + [Dimension("Dim3FromAttribute")] + public string? SomeDim { get; set; } + } + + public struct CounterStructDimensions + { + public string? Dim1 { get; set; } + + [Dimension("DimInField")] + public string? DimInField; + + [Dimension("Dim2_FromAttribute")] + public string? Dim2 { get; set; } + + public CounterOperations Operations { get; set; } + + [Dimension("Operations_FromAttribute")] + public CounterOperations Operations2 { get; set; } + } + + public record class CounterRecordClassDimensions + { + public string? Dim1 { get; set; } + + [Dimension("DimInField")] + public string? DimInField; + + [Dimension("Dim2_FromAttribute")] + public string? Dim2 { get; set; } + + public CounterOperations Operations { get; set; } + + [Dimension("Operations_FromAttribute")] + public CounterOperations Operations2 { get; set; } + } +#pragma warning restore SA1402 // File may only contain a single type +} diff --git a/test/Generators/Microsoft.Gen.Metering/TestClasses/CounterTestExtensions.cs b/test/Generators/Microsoft.Gen.Metering/TestClasses/CounterTestExtensions.cs new file mode 100644 index 0000000000..209b709180 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/TestClasses/CounterTestExtensions.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +namespace TestClasses +{ + [SuppressMessage("Readability", "R9A046:Source generated metrics (fast metrics) should be located in 'Metric' class", + Justification = "Metering generator tests")] + internal static partial class CounterTestExtensions + { + [Counter] + public static partial GenericIntCounter0D CreateGenericIntCounter0D(Meter meter); + + [Counter] + public static partial GenericIntCounterExt0D CreateGenericIntCounterExt0D(this Meter meter); + + [Counter] + public static partial Counter0D CreateCounter0D(Meter meter); + + [Counter] + public static partial MyNamedCounter CreateCounterDifferentName(Meter meter); + + [Counter] + public static partial CounterExt0D CreateCounterExt0D(this Meter meter); + + [Counter("s1")] + public static partial GenericIntCounter1D CreateGenericIntCounter1D(Meter meter); + + [Counter("s1")] + public static partial GenericIntCounterExt1D CreateGenericIntCounterExt1D(this Meter meter); + + [Counter("s1")] + public static partial GenericFloatCounter1D CreateGenericFloatCounter1D(Meter meter); + + [Counter("s1")] + public static partial GenericFloatCounterExt1D CreateGenericFloatCounterExt1D(this Meter meter); + + [Counter("s1", "s2")] + public static partial Counter2D CreateCounter2D(Meter meter); + + [Counter("s1", "s2")] + public static partial CounterExt2D CreateCounterExt2D(this Meter meter); + + [Counter("s1", "s2", "s3")] + public static partial Counter3D CreateCounter3D(Meter meter); + + [Counter("s1", "s2", "s3")] + public static partial CounterExt3D CreateCounterExt3D(this Meter meter); + + [Counter("s1", "s2", "s3", "s4")] + public static partial Counter4D CreateCounter4D(Meter meter); + + [Counter("s1", "s2", "s3", "s4")] + public static partial CounterExt4D CreateCounterExt4D(this Meter meter); + + [Counter("d1", "d2")] + public static partial CounterS0D2 CreateCounterS0D2(Meter meter); + + [Counter("d1", "d2")] + public static partial CounterExtS0D2 CreateCounterExtS0D2(this Meter meter); + + [Counter("s1", "d1")] + public static partial CounterS1D1 CreateCounterS1D1(Meter meter); + + [Counter("s1", "d1")] + public static partial CounterExtS1D1 CreateCounterExtS1D1(this Meter meter); + + [Counter("s1", "s2", "s3", "d1", "d2")] + public static partial CounterS3D2 CreateCounterS3D2(Meter meter); + + [Counter("s1", "s2", "s3", "d1", "d2")] + public static partial CounterExtS3D2 CreateCounterExtS3D2(this Meter meter); + + [Counter("s1", "s2", "s3", "s4", "s5", "d1", "d2", "d3", "d4", "d5")] + public static partial CounterS5D5 CreateCounterS5D5(Meter meter); + + [Counter("s1", "s2", "s3", "s4", "s5", "d1", "d2", "d3", "d4", "d5")] + public static partial CounterExtS5D5 CreateCounterExtS5D5(this Meter meter); + + [Counter("Static:1", "Static-2", "Dyn_1", "Dyn")] + public static partial TestCounter CreateTestCounter(Meter meter); + + [Counter("Static:1", "Static-2", "Dyn_1", "Dyn")] + public static partial TestCounterExt CreateTestCounterExt(this Meter meter); + + [Counter(MetricConstants.D1, MetricConstants.D2, MetricConstants.D3, Name = @"MyCounterMetric")] + public static partial CounterWithVariableParams CreateCounterWithVariableParams(Meter meter); + + [Counter(MetricConstants.D1, MetricConstants.D2, MetricConstants.D3, Name = @"MyCounterMetric")] + public static partial CounterExtWithVariableParams CreateCounterExtWithVariableParams(this Meter meter); + + [Counter(Name = @"MyMetric\Category\SingleSlash")] + public static partial CounterX CreateCounterX(Meter meter); + + [Counter(Name = @"MyMetric\Category\SingleSlash")] + public static partial CounterExtX CreateCounterExtX(this Meter meter); + + [Counter(Name = @"MyMetric\\Category\\DoubleSlash")] + public static partial CounterY CreateCounterY(Meter meter); + + [Counter(Name = @"MyMetric\\Category\\DoubleSlash")] + public static partial CounterExtY CreateCounterExtY(this Meter meter); + + [Counter(typeof(CounterDimensions), Name = "MyCounterStrongTypeMetric")] + public static partial StrongTypeDecimalCounter CreateStrongTypeDecimalCounter(Meter meter); + + [Counter(typeof(CounterDimensions), Name = "MyCounterStrongTypeMetricExt")] + public static partial StrongTypeDecimalCounterExt CreateStrongTypeDecimalCounterExt(this Meter meter); + + [Counter(typeof(CounterStructDimensions), Name = "MyCounterStructTypeMetric")] + public static partial StructTypeCounter CreateCounterStructType(Meter meter); + + [Counter(typeof(CounterStructDimensions), Name = "MyCounterStructTypeMetric")] + public static partial StructTypeCounterExt CreateCounterStructTypeExt(this Meter meter); + + [Counter(typeof(CounterRecordClassDimensions), Name = "MyCounterRecordClassTypeMetric")] + public static partial RecordClassTypeCounter CreateCounterRecordClassType(Meter meter); + } +} diff --git a/test/Generators/Microsoft.Gen.Metering/TestClasses/FileScopedNamespaceExtensions.cs b/test/Generators/Microsoft.Gen.Metering/TestClasses/FileScopedNamespaceExtensions.cs new file mode 100644 index 0000000000..811bb20f1b --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/TestClasses/FileScopedNamespaceExtensions.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if ROSLYN_4_0_OR_GREATER + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +namespace TestClasses; + +[SuppressMessage("Readability", "R9A046:Source generated metrics (fast metrics) should be located in 'Metric' class", + Justification = "Metering generator tests")] +internal static partial class FileScopedExtensions +{ + [Counter] + public static partial FileScopedNamespaceCounter CreateCounter(Meter meter); + + [Counter] + public static partial FileScopedNamespaceGenericDoubleCounter CreateGenericDoubleCounter(Meter meter); +} + +#endif diff --git a/test/Generators/Microsoft.Gen.Metering/TestClasses/HistogramTestExtensions.cs b/test/Generators/Microsoft.Gen.Metering/TestClasses/HistogramTestExtensions.cs new file mode 100644 index 0000000000..7e11eba514 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/TestClasses/HistogramTestExtensions.cs @@ -0,0 +1,175 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +namespace TestClasses +{ + [SuppressMessage("Usage", "CA1801:Review unused parameters", + Justification = "Method body is source generated where the parameters will be used")] + [SuppressMessage("Readability", "R9A046:Source generated metrics (fast metrics) should be located in 'Metric' class", + Justification = "Metering generator tests")] + internal static partial class HistogramTestExtensions + { + [Histogram] + public static partial Histogram0D CreateHistogram0D(Meter meter); + + [Histogram] + public static partial GenericIntHistogram0D CreateGenericIntHistogram0D(Meter meter); + + [Histogram] + public static partial HistogramExt0D CreateHistogramExt0D(this Meter meter); + + [Histogram] + public static partial GenericIntHistogramExt0D CreateGenericIntHistogramExt0D(this Meter meter); + + [Histogram("s1")] + public static partial Histogram1D CreateHistogram1D(Meter meter); + + [Histogram("s1")] + public static partial HistogramExt1D CreateHistogramExt1D(this Meter meter); + + [Histogram("s1", "s2")] + public static partial Histogram2D CreateHistogram2D(Meter meter); + + [Histogram("s1", "s2")] + public static partial GenericIntHistogram2D CreateGenericIntHistogram2D(Meter meter); + + [Histogram("s1", "s2")] + public static partial GenericIntHistogramExt2D CreateGenericIntHistogramExt2D(this Meter meter); + + [Histogram("s1", "s2")] + public static partial HistogramExt2D CreateHistogramExt2D(this Meter meter); + + [Histogram("s1", "s2", "s3")] + public static partial Histogram3D CreateHistogram3D(Meter meter); + + [Histogram("s1", "s2", "s3")] + public static partial HistogramExt3D CreateHistogramExt3D(this Meter meter); + + [Histogram("s1", "s2", "s3", "s4")] + public static partial Histogram4D CreateHistogram4D(Meter meter); + + [Histogram("s1", "s2", "s3", "s4")] + public static partial HistogramExt4D CreateHistogramExt4D(this Meter meter); + + [Histogram("d1", "d2")] + public static partial HistogramS0D2 CreateHistogramS0D2(Meter meter); + + [Histogram("d1", "d2")] + public static partial HistogramExtS0D2 CreateHistogramExtS0D2(this Meter meter); + + [Histogram("s1", "d1")] + public static partial HistogramS1D1 CreateHistogramS1D1(Meter meter); + + [Histogram("s1", "d1")] + public static partial HistogramExtS1D1 CreateHistogramExtS1D1(this Meter meter); + + [Histogram("s1", "s2", "s3", "d1", "d2")] + public static partial HistogramS3D2 CreateHistogramS3D2(Meter meter); + + [Histogram("s1", "s2", "s3", "d1", "d2")] + public static partial HistogramExtS3D2 CreateHistogramExtS3D2(this Meter meter); + + [Histogram("s1", "s2", "s3", "s4", "s5", "d1", "d2", "d3", "d4", "d5")] + public static partial HistogramS5D5 CreateHistogramS5D5(Meter meter); + + [Histogram("s1", "s2", "s3", "s4", "s5", "d1", "d2", "d3", "d4", "d5")] + public static partial HistogramExtS5D5 CreateHistogramExtS5D5(this Meter meter); + + [Histogram("Static:1", "Static-2", "Dyn_1", "Dyn")] + public static partial TestHistogram CreateTestHistogram(Meter meter); + + [Histogram("Static:1", "Static-2", "Dyn_1", "Dyn")] + public static partial TestHistogramExt CreateTestHistogramExt(this Meter meter); + + [Histogram(MetricConstants.D1, MetricConstants.D2, MetricConstants.D3, Name = "MyHistogramMetric")] + public static partial HistogramWithVariableParams CreateHistogramWithVariableParams(Meter meter); + + [Histogram(MetricConstants.D1, MetricConstants.D2, MetricConstants.D3, Name = "MyHistogramMetric")] + public static partial HistogramExtWithVariableParams CreateHistogramExtWithVariableParams(this Meter meter); + + [Histogram(typeof(HistogramDimensionsTest), Name = "MyHistogramStrongTypeMetric")] + public static partial StrongTypeHistogram CreateHistogramStrongType(Meter meter); + + [Histogram(typeof(HistogramDimensionsTest), Name = "MyHistogramStrongTypeMetricExt")] + public static partial StrongTypeHistogramExt CreateHistogramExtStrongType(this Meter meter); + + [Histogram(typeof(HistogramStruct), Name = "MyHistogramStructTypeMetric")] + public static partial StructTypeHistogram CreateHistogramStructType(Meter meter); + + [Histogram(typeof(HistogramStruct), Name = "MyHistogramStructTypeMetric")] + public static partial StructTypeHistogramExt CreateHistogramExtStructType(this Meter meter); + } + + // The order of the below is extremely important for unit testing. + // The metricGenerator will create the code based on the order it is defined, and the unit test depends on the order being the same. +#pragma warning disable SA1402 // File may only contain a single type + public class HistogramDimensionsTest : HistogramParentDimensions + { + public string? Dim1; + public HistogramOperations OperationsEnum { get; set; } + + [Dimension("Enum2")] + public HistogramOperations OperationsEnum2 { get; set; } + + public HistogramChildDimensions? ChildDimensionsObject { get; set; } + public HistogramGrandChildrenDimensions? GrandChildrenDimensionsObject { get; set; } + } + + public enum HistogramOperations + { + Unknown = 0, + Operation1 = 1, + } + + public class HistogramParentDimensions + { + public string? ParentOperationName { get; set; } + + public HistogramDimensionsStruct ChildDimensionsStruct { get; set; } + } + + public class HistogramChildDimensions + { + public string? Dim2 { get; set; } + + [Dimension("dim2FromAttribute")] + public string? SomeDim; + } + + public struct HistogramDimensionsStruct + { + public string Dim4Struct { get; set; } + + [Dimension("Dim5FromAttribute")] + public string Dim5Struct { get; set; } + } + + public class HistogramGrandChildrenDimensions + { + public string? Dim3 { get; set; } + + [Dimension("Dim3FromAttribute")] + public string? SomeDim { get; set; } + } + + public struct HistogramStruct + { + public string? Dim1 { get; set; } + + [Dimension("DimInField")] + public string? DimInField; + + [Dimension("Dim2_FromAttribute")] + public string? Dim2 { get; set; } + + public HistogramOperations Operations { get; set; } + + [Dimension("Operations_FromAttribute")] + public HistogramOperations Operations2 { get; set; } + } +#pragma warning restore SA1402 // File may only contain a single type +} diff --git a/test/Generators/Microsoft.Gen.Metering/TestClasses/MeterTExtensions.cs b/test/Generators/Microsoft.Gen.Metering/TestClasses/MeterTExtensions.cs new file mode 100644 index 0000000000..12afe494bd --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/TestClasses/MeterTExtensions.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Telemetry.Metering; + +namespace TestClasses +{ + [SuppressMessage("Readability", "R9A046:Source generated metrics (fast metrics) should be located in 'Metric' class", + Justification = "Metering generator tests")] + internal static partial class MeterTExtensions + { + internal sealed class DummyType + { + } + + [Counter] + public static partial GenericIntCounter0DMeterT CreateGenericIntCounter0D(Meter meter); + + [Counter] + public static partial GenericIntCounterExt0DMeterT CreateGenericIntCounterExt0D(this Meter meter); + + [Counter] + public static partial Counter0DMeterT CreateCounter0D(Meter meter); + + [Counter] + public static partial MyNamedCounterMeterT CreateCounterDifferentName(Meter meter); + + [Counter] + public static partial CounterExt0DMeterT CreateCounterExt0D(this Meter meter); + + [Counter("s1", "s2", "s3", "s4", "s5", "d1", "d2", "d3", "d4", "d5")] + public static partial CounterS5D5MeterT CreateCounterS5D5(Meter meter); + + [Counter("s1", "s2", "s3", "s4", "s5", "d1", "d2", "d3", "d4", "d5")] + public static partial CounterExtS5D5MeterT CreateCounterExtS5D5(this Meter meter); + + [Counter("Static:1", "Static-2", "Dyn_1", "Dyn")] + public static partial TestCounterMeterT CreateTestCounter(Meter meter); + + [Counter("Static:1", "Static-2", "Dyn_1", "Dyn")] + public static partial TestCounterExtMeterT CreateTestCounterExt(this Meter meter); + + [Counter(MetricConstants.D1, MetricConstants.D2, MetricConstants.D3, Name = @"MyCounterMetric")] + public static partial CounterWithVariableParamsMeterT CreateCounterWithVariableParams(Meter meter); + + [Counter(MetricConstants.D1, MetricConstants.D2, MetricConstants.D3, Name = @"MyCounterMetric")] + public static partial CounterExtWithVariableParamsMeterT CreateCounterExtWithVariableParams(this Meter meter); + + [Counter(typeof(CounterDimensions), Name = "MyCounterStrongTypeMetric")] + public static partial StrongTypeDecimalCounterMeterT CreateStrongTypeDecimalCounter(Meter meter); + + [Counter(typeof(CounterDimensions), Name = "MyCounterStrongTypeMetric")] + public static partial StrongTypeDecimalCounterExtMeterT CreateStrongTypeDecimalCounterExt(this Meter meter); + + [Counter(typeof(CounterStructDimensions), Name = "MyCounterStructTypeMetric")] + public static partial StructTypeCounterMeterT CreateCounterStructType(Meter meter); + + [Counter(typeof(CounterStructDimensions), Name = "MyCounterStructTypeMetric")] + public static partial StructTypeCounterExtMeterT CreateCounterStructTypeExt(this Meter meter); + } +} diff --git a/test/Generators/Microsoft.Gen.Metering/TestClasses/MetricConstants.cs b/test/Generators/Microsoft.Gen.Metering/TestClasses/MetricConstants.cs new file mode 100644 index 0000000000..500aafac9c --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/TestClasses/MetricConstants.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace TestClasses +{ + internal static class MetricConstants + { + public const string D1 = "Dim1"; + + public const string D2 = "Dim_2"; // dots are not supported in dimension names + + public const string D3 = "Dim_3"; // dashes are not supported in dimension names + } +} diff --git a/test/Generators/Microsoft.Gen.Metering/TestClasses/MetricRecordClassTestExtensions.cs b/test/Generators/Microsoft.Gen.Metering/TestClasses/MetricRecordClassTestExtensions.cs new file mode 100644 index 0000000000..1d4ed65257 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/TestClasses/MetricRecordClassTestExtensions.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +namespace TestClasses +{ + internal partial record class MetricRecordClassTestExtensions(string Name, string Address) + { + [Counter] + public static partial CounterFromRecordClass CreateCounterFromRecordClass(Meter meter); + } +} diff --git a/test/Generators/Microsoft.Gen.Metering/TestClasses/MetricRecordStructTestExtensions.cs b/test/Generators/Microsoft.Gen.Metering/TestClasses/MetricRecordStructTestExtensions.cs new file mode 100644 index 0000000000..bd4c37706c --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/TestClasses/MetricRecordStructTestExtensions.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if ROSLYN_4_0_OR_GREATER + +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +namespace TestClasses +{ + internal partial record struct MetricRecordStructTestExtensions(string Name, string Address) + { + [Counter] + public static partial CounterFromRecordStruct CreateCounterFromRecordStruct(Meter meter); + } +} + +#endif diff --git a/test/Generators/Microsoft.Gen.Metering/TestClasses/MetricStructTestExtensions.cs b/test/Generators/Microsoft.Gen.Metering/TestClasses/MetricStructTestExtensions.cs new file mode 100644 index 0000000000..63b7ea43ae --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/TestClasses/MetricStructTestExtensions.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +namespace TestClasses +{ + internal partial struct MetricStructTestExtensions + { + [Counter] + public static partial CounterFromStruct CreateCounterFromStruct(Meter meter); + } +} diff --git a/test/Generators/Microsoft.Gen.Metering/TestClasses/NestedClassMetrics.cs b/test/Generators/Microsoft.Gen.Metering/TestClasses/NestedClassMetrics.cs new file mode 100644 index 0000000000..298c6ada54 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/TestClasses/NestedClassMetrics.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +namespace NestedClass.Metering +{ + [SuppressMessage("Usage", "CA1801:Review unused parameters", + Justification = "For testing emitter for classes")] + [SuppressMessage("Readability", "R9A046:Source generated metrics (fast metrics) should be located in 'Metric' class", + Justification = "Metering generator tests")] + public static partial class TopLevelClass + { + internal static partial class InstrumentsInNestedClass + { + [Counter] + public static partial NestedClassCounter CreateCounterInNestedClass(Meter meter); + + [Histogram] + public static partial NestedClassHistogram CreateHistogramInNestedClass(Meter meter); + } + + internal static partial class MultiLevelNesting + { + internal static partial class MultiLevelNestedClass + { + [Counter] + public static partial MultiLevelNestedClassCounter CreateCounterInMultiLevelNestedClass(Meter meter); + + [Histogram] + public static partial MultiLevelNestedClassHistogram CreateHistogramInMultiLevelNestedClass(Meter meter); + } + } + } +} diff --git a/test/Generators/Microsoft.Gen.Metering/TestClasses/NestedRecordClassMetrics.cs b/test/Generators/Microsoft.Gen.Metering/TestClasses/NestedRecordClassMetrics.cs new file mode 100644 index 0000000000..5c0bd77eab --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/TestClasses/NestedRecordClassMetrics.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +namespace NestedRecordClass.Metering +{ + [SuppressMessage("Usage", "CA1801:Review unused parameters", + Justification = "For testing emitter for records")] + [SuppressMessage("Readability", "R9A046:Source generated metrics (fast metrics) should be located in 'Metric' class", + Justification = "Metering generator tests")] + public static partial class TopLevelRecordClass + { + public partial record class InstrumentsInNestedRecordClass(string Address) + { + [Counter] + public static partial NestedRecordClassCounter CreateCounterInNestedRecordClass(Meter meter); + + [Histogram] + public static partial NestedRecordClassHistogram CreateHistogramInNestedRecordClass(Meter meter); + } + } +} diff --git a/test/Generators/Microsoft.Gen.Metering/TestClasses/NestedRecordStructMetrics.cs b/test/Generators/Microsoft.Gen.Metering/TestClasses/NestedRecordStructMetrics.cs new file mode 100644 index 0000000000..880bbfcca1 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/TestClasses/NestedRecordStructMetrics.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if ROSLYN_4_0_OR_GREATER + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +namespace NestedRecordStruct.Metering +{ + [SuppressMessage("Usage", "CA1801:Review unused parameters", + Justification = "For testing emitter for structs")] + [SuppressMessage("Readability", "R9A046:Source generated metrics (fast metrics) should be located in 'Metric' class", + Justification = "Metering generator tests")] + public static partial class TopLevelStructClass + { + public partial record struct InstrumentsInNestedRecordStruct(string Address) + { + [Counter] + public static partial NestedRecordStructCounter CreateCounterInNestedRecordStruct(Meter meter); + + [Histogram] + public static partial NestedRecordStructHistogram CreateHistogramInNestedRecordStruct(Meter meter); + } + } +} + +#endif diff --git a/test/Generators/Microsoft.Gen.Metering/TestClasses/NestedStructMetrics.cs b/test/Generators/Microsoft.Gen.Metering/TestClasses/NestedStructMetrics.cs new file mode 100644 index 0000000000..4de1b0ee4a --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/TestClasses/NestedStructMetrics.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +namespace NestedStruct.Metering +{ + [SuppressMessage("Usage", "CA1801:Review unused parameters", + Justification = "For testing emitter for classes")] + [SuppressMessage("Readability", "R9A046:Source generated metrics (fast metrics) should be located in 'Metric' class", + Justification = "Metering generator tests")] + public partial struct TopLevelStruct + { + internal partial struct InstrumentsInNestedStruct + { + [Counter] + public static partial NestedStructCounter CreateCounterInNestedStruct(Meter meter); + + [Histogram] + public static partial NestedStructHistogram CreateHistogramInNestedStruct(Meter meter); + } + } +} diff --git a/test/Generators/Microsoft.Gen.Metering/TestClasses/OverlappingNamesTestExtensions.cs b/test/Generators/Microsoft.Gen.Metering/TestClasses/OverlappingNamesTestExtensions.cs new file mode 100644 index 0000000000..a3e4fe2d3d --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/TestClasses/OverlappingNamesTestExtensions.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +// Generator emits the code without compilation errors only when +// a class named 'TestClassesNspace' is a 'part' of the namespace 'TestClassesNspace.Metering'. +namespace TestClassesNspace.Metering +{ + [SuppressMessage("Usage", "CA1801:Review unused parameters", + Justification = "Method body is source generated where the parameters will be used")] + [SuppressMessage("Readability", "R9A046:Source generated metrics (fast metrics) should be located in 'Metric' class", + Justification = "Metering generator tests")] + public static partial class OverlappingNamesTestExtensions + { + [Counter(typeof(StrongTypeDimensionsOverlappingNames))] + public static partial OverlappingNamesCounter CreateOverlappingNamesCounter(Meter meter); + + [Histogram(typeof(StrongTypeDimensionsOverlappingNames))] + public static partial OverlappingNamesHistogram CreateOverlappingNamesHistogram(Meter meter); + } + +#pragma warning disable SA1402 // File may only contain a single type + public class StrongTypeDimensionsOverlappingNames + { + public string? Dimension1; + public string? Dimension2; + } +} + +#pragma warning disable SA1403 // File may only contain a single namespace +namespace TestClassesNspace +{ + public class TestClassesNspace + { + } +} diff --git a/test/Generators/Microsoft.Gen.Metering/TestClasses/Public.cs b/test/Generators/Microsoft.Gen.Metering/TestClasses/Public.cs new file mode 100644 index 0000000000..92b19ca2b4 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/TestClasses/Public.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +namespace PublicMetering +{ + [SuppressMessage("Usage", "CA1801:Review unused parameters", + Justification = "For testing emitter for classes without namespace")] + [SuppressMessage("Readability", "R9A046:Source generated metrics (fast metrics) should be located in 'Metric' class", + Justification = "Metering generator tests")] + public static partial class PublicMetricInstruments + { + [Counter] + public static partial PublicCounter CreatePublicCounter(Meter meter); + + [Histogram] + public static partial PublicHistogram CreatePublicHistogram(Meter meter); + } +} diff --git a/test/Generators/Microsoft.Gen.Metering/Unit/Common/DiagDescriptorsTests.cs b/test/Generators/Microsoft.Gen.Metering/Unit/Common/DiagDescriptorsTests.cs new file mode 100644 index 0000000000..77b98cc515 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/Unit/Common/DiagDescriptorsTests.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Xunit; + +namespace Microsoft.Gen.Metering.Test; + +public class DiagDescriptorsTests +{ + public static IEnumerable DiagDescriptorsData() + { + var type = typeof(DiagDescriptors); + foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.GetProperty)) + { + var value = property.GetValue(type, null); + yield return new[] { value }; + } + } + + [Theory] + [MemberData(nameof(DiagDescriptorsData))] + public void ShouldContainValidLinkAndBeEnabled(DiagnosticDescriptor descriptor) + { + Assert.True(descriptor.IsEnabledByDefault, descriptor.Id + " should be enabled by default"); + Assert.EndsWith("/" + descriptor.Id, descriptor.HelpLinkUri, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/test/Generators/Microsoft.Gen.Metering/Unit/Common/EmitterTests.cs b/test/Generators/Microsoft.Gen.Metering/Unit/Common/EmitterTests.cs new file mode 100644 index 0000000000..7dfc6664b8 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/Unit/Common/EmitterTests.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Gen.Metering.Model; +using Microsoft.Gen.Shared; +using Xunit; + +namespace Microsoft.Gen.Metering.Test; + +public class EmitterTests +{ + [Fact] + public async Task TestEmitter() + { + var sources = new List(); + foreach (var file in Directory.GetFiles("TestClasses")) + { +#if !ROSLYN_4_0_OR_GREATER + if (file.EndsWith("FileScopedNamespaceExtensions.cs") || file.EndsWith("MetricRecordStructTestExtensions.cs")) + { + continue; + } +#endif + sources.Add(File.ReadAllText(file)); + } + + var (d, r) = await RoslynTestUtils.RunGenerator( + new Generator(), + new[] + { + Assembly.GetAssembly(typeof(Meter))!, + Assembly.GetAssembly(typeof(CounterAttribute))!, + Assembly.GetAssembly(typeof(HistogramAttribute))!, + Assembly.GetAssembly(typeof(CounterAttribute<>))!, + Assembly.GetAssembly(typeof(HistogramAttribute<>))!, + }, + sources) + .ConfigureAwait(false); + + Assert.Empty(d); + Assert.Equal(2, r.Length); + + string generatedContentPath = "GoldenFiles/Microsoft.Gen.Metering/Microsoft.Gen.Metering.Generator"; + var goldenCache = File.ReadAllText($"{generatedContentPath}/Factory.g.cs"); + var goldenMetrics = File.ReadAllText($"{generatedContentPath}/Metering.g.cs"); + + var result = r.First(x => x.HintName == "Factory.g.cs").SourceText.ToString(); + Assert.Equal(goldenCache, result); + + result = r.First(x => x.HintName == "Metering.g.cs").SourceText.ToString(); + Assert.Equal(goldenMetrics, result); + } + + [Theory] + [InlineData(10)] + [InlineData((int)InstrumentKind.None)] + [InlineData((int)InstrumentKind.Gauge)] + public void EmitMeter_GivenMetricTypeIsUnknown_ThrowsNotSupportedException(int instrumentKind) + { + var metricClass = new MetricType + { + Name = "Logger", + Namespace = "Samples", + Methods = + { + new MetricMethod + { + Name = "CreateUnknownMetric", + MetricName = "UnknownMetric", + MetricTypeName = "UnknownMetric", + InstrumentKind = (InstrumentKind)instrumentKind, + DimensionsKeys = { "Dim1" }, + IsExtensionMethod = false, + Modifiers = "static partial", + AllParameters = + { + new MetricParameter + { + Name = "meter", + Type = "global::Microsoft.Extensions.Telemetry.Metering.IMeter", + IsMeter = true + } + } + } + } + }; + + var emitter = new Emitter(); + + Assert.Throws(() => + emitter.EmitMetrics(new List { metricClass }, cancellationToken: default)); + } +} diff --git a/test/Generators/Microsoft.Gen.Metering/Unit/Common/MetricMethodTests.cs b/test/Generators/Microsoft.Gen.Metering/Unit/Common/MetricMethodTests.cs new file mode 100644 index 0000000000..b6e770e683 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/Unit/Common/MetricMethodTests.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Gen.Metering.Model; +using Xunit; + +namespace Microsoft.Gen.Metering.Test; + +public class MetricMethodTests +{ + [Fact] + public void Fields_Should_BeInitialized() + { + var instance = new MetricMethod(); + Assert.Empty(instance.Modifiers); + Assert.Empty(instance.MetricTypeModifiers); + Assert.Empty(instance.MetricTypeName); + Assert.Empty(instance.GenericType); + } +} diff --git a/test/Generators/Microsoft.Gen.Metering/Unit/Common/MetricParameterTests.cs b/test/Generators/Microsoft.Gen.Metering/Unit/Common/MetricParameterTests.cs new file mode 100644 index 0000000000..195faf1983 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/Unit/Common/MetricParameterTests.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Gen.Metering.Model; +using Xunit; + +namespace Microsoft.Gen.Metering.Test; + +public class MetricParameterTests +{ + [Fact] + public void Fields_Should_BeInitialized() + { + var instance = new MetricParameter(); + Assert.Empty(instance.Name); + Assert.Empty(instance.Type); + } +} diff --git a/test/Generators/Microsoft.Gen.Metering/Unit/Common/MetricTypeTests.cs b/test/Generators/Microsoft.Gen.Metering/Unit/Common/MetricTypeTests.cs new file mode 100644 index 0000000000..df5ef89baf --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/Unit/Common/MetricTypeTests.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Gen.Metering.Model; +using Xunit; + +namespace Microsoft.Gen.Metering.Test; + +public class MetricTypeTests +{ + [Fact] + public void Fields_Should_BeInitialized() + { + var instance = new MetricType(); + Assert.Empty(instance.Name); + Assert.Empty(instance.Namespace); + Assert.Empty(instance.Constraints); + Assert.Empty(instance.Modifiers); + Assert.Empty(instance.Keyword); + } +} diff --git a/test/Generators/Microsoft.Gen.Metering/Unit/Common/ParserTests.StrongTypes.cs b/test/Generators/Microsoft.Gen.Metering/Unit/Common/ParserTests.StrongTypes.cs new file mode 100644 index 0000000000..903f3333dd --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/Unit/Common/ParserTests.StrongTypes.cs @@ -0,0 +1,555 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Gen.Metering.Test; + +public partial class ParserTests +{ + [Fact] + public async Task NullDimensionNamesInAttributes() + { + var d = await RunGenerator(@" + public struct HistogramStruct + { + [Dimension(null)] + public string? Dim1 { get; set; } + } + + public static partial class MetricClass + { + [Histogram(typeof(HistogramStruct), Name=""TotalCountTest"")] + public static partial TotalCount CreateTotalCountCounter(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task StructTypeHistogram() + { + var d = await RunGenerator(@" + public enum Operations + { + Unknown = 0, + Operation1 = 1, + } + + public struct HistogramStruct + { + [Dimension(""Dim1_FromAttribute"")] + public string? Dim1 { get; set; } + + [Dimension(""Operations_FromAttribute"")] + public Operations Operations { get; set; } + } + + public static partial class MetricClass + { + [Histogram(typeof(HistogramStruct), Name=""TotalCountTest"")] + public static partial TotalCount CreateTotalCountCounter(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task StrongTypeHistogram() + { + // This test should return no errors. + var d = await RunGenerator(@" + public class DimensionsTest : ParentDimensions + { + public string? test1 { get; set; } + + [Dimension(""test1_FromAttribute"")] + public string? test1_WithAttribute { get; set; } + + [Dimension(""operations_FromAttribute"")] + public Operations operations {get;set;} + + public ChildDimensions? ChildDimensions1 { get; set; } + + public void Method() + { + System.Console.WriteLine(""I am a method.""); + } + } + + public enum Operations + { + Unknown = 0, + Operation1 = 1, + } + + public class ParentDimensions + { + [Dimension(""parentDimension_FromAttribute"")] + public string? ParentOperationNameWithAttribute { get;set; } + + public string? ParentOperationName { get;set; } + + public DimensionsStruct ChildDimensionsStruct { get; set; } + } + + public class ChildDimensions + { + [Dimension(""test2_FromAttribute"")] + public string test2_WithAttribute { get; set; } + + public string test2 { get; set; } + + [Dimension(""test1_FromAttribute_In_Child1"")] + public string? test1 { get; set; } + + public ChildDimensions2? ChildDimensions2 { get; set;} + } + + public class ChildDimensions2 + { + [Dimension(""test3_FromAttribute"")] + public string test3_WithAttribute { get; set; } + + public string test3 { get; set; } + + [Dimension(""test1_FromAttribute_In_Child2"")] + public string? test1 { get; set; } + } + + public struct DimensionsStruct + { + [Dimension(""testStruct_FromAttribute"")] + public string testStruct_WithAttribute { get; set; } + + public string testStruct { get; set; } + } + + public static partial class MetricClass + { + [Histogram(typeof(DimensionsTest), Name=""TotalCountTest"")] + public static partial TotalCount CreateTotalCountCounter(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task SimpleStrongTypeHistogram() + { + // This test should return no errors. + var d = await RunGenerator(@" + public struct DimensionsTest + { + [Dimension(""test1_FromAttribute"")] + public string? test1_WithAttribute { get; set; } + public string? test1 { get; set; } + } + + public static partial class MetricClass + { + [Histogram(typeof(DimensionsTest))] + public static partial TotalCount CreateTotalCountCounter(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task TestNoStrongTypeDefined() + { + var d = await RunGenerator(@" + public static partial class MetricClass + { + [Histogram(typeof(DimensionsTest))] + public static partial TotalCount CreateTotalCountCounter(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task StructTypeCounter() + { + var d = await RunGenerator(@" + public enum Operations + { + Unknown = 0, + Operation1 = 1, + } + + public struct CounterStruct + { + [Dimension(""Dim1_FromAttribute"")] + public string? Dim1 { get; set; } + + [Dimension(""Dim2_FromAttribute"")] + public string? Dim2; + + [Dimension(""Operations_FromAttribute"")] + public Operations Operations { get; set; } + } + + public static partial class MetricClass + { + [Counter(typeof(CounterStruct), Name=""TotalCountTest"")] + public static partial TotalCount CreateTotalCountCounter(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task StrongTypeCounter() + { + // This test should return no errors. + var d = await RunGenerator(@" + public class DimensionsTest : ParentDimensions + { + [Dimension(""test1_FromAttribute"")] + public string? test1 { get; set; } + + [Dimension(""Operations_FromAttribute"")] + public Operations operations {get;set;} + + public ChildDimensions? ChildDimensions1 { get; set; } + + public void Method() + { + System.Console.WriteLine(""I am a method.""); + } + } + + public enum Operations + { + Unknown = 0, + Operation1 = 1, + } + + public class ParentDimensions + { + [Dimension(""parentDimension_FromAttribute"")] + public string? ParentOperationNameWithAttribute { get;set; } + + public string? ParentOperationName { get;set; } + + public DimensionsStruct ChildDimensionsStruct { get; set; } + } + + public class ChildDimensions + { + [Dimension(""test2_FromAttribute"")] + public string test2_WithAttribute { get; set; } + + public string test2 { get; set; } + + [Dimension(""test1_FromAttribute_In_Child1"")] + public string? test1 { get; set; } + } + + public struct DimensionsStruct + { + [Dimension(""testStruct_FromAttribute"")] + public string testStruct_WithAttribute { get; set; } + + public string testStruct { get; set; } + } + + public static partial class MetricClass + { + [Counter(typeof(DimensionsTest), Name=""TotalCountTest"")] + public static partial TotalCount CreateTotalCountCounter(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task StructTypeGauge() + { + var d = await RunGenerator(@" + public enum Operations + { + Unknown = 0, + Operation1 = 1, + } + + public struct GaugeStruct + { + [Dimension(""Dim1_FromAttribute"")] + public string? Dim1 { get; set; } + + [Dimension(""Operations_FromAttribute"")] + public Operations Operations { get; set; } + } + + public static partial class MetricClass + { + [Gauge(typeof(GaugeStruct), Name=""TotalCountTest"")] + public static partial TotalCount CreateTotalCountCounter(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task StrongTypeGauge() + { + // This test should return no errors. + var d = await RunGenerator(@" + public class DimensionsTest : ParentDimensions + { + [Dimension(""test1_FromAttribute"")] + public string? test1 { get; set; } + + [Dimension(""Operations_FromAttribute"")] + public Operations operations {get;set;} + + public ChildDimensions? ChildDimensions1 { get; set; } + + public void Method() + { + System.Console.WriteLine(""I am a method.""); + } + } + + public enum Operations + { + Unknown = 0, + Operation1 = 1, + } + + public class ParentDimensions + { + [Dimension(""parentDimension_FromAttribute"")] + public string? ParentOperationNameWithAttribute { get;set; } + + public string? ParentOperationName { get;set; } + + public DimensionsStruct ChildDimensionsStruct { get; set; } + } + + public class ChildDimensions + { + [Dimension(""test2_FromAttribute"")] + public string test2_WithAttribute { get; set; } + + public string test2 { get; set; } + + [Dimension(""test1_FromAttribute_In_Child1"")] + public string? test1 { get; set; } + } + + public struct DimensionsStruct + { + [Dimension(""testStruct_FromAttribute"")] + public string testStruct_WithAttribute { get; set; } + + public string testStruct { get; set; } + } + + public static partial class MetricClass + { + [Gauge(typeof(DimensionsTest), Name=""TotalCountTest"")] + public static partial TotalCount CreateTotalCountCounter(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task DuplicateDimensionStringName() + { + var d = await RunGenerator(@" + public class DimensionsTest + { + public string dim1 { get; set; } + public ChildDimensions childDimensions { get; set; } + } + + public class ChildDimensions + { + public string dim1 {get;set;} + } + + public static partial class MetricClass + { + [Histogram(typeof(DimensionsTest), Name=""TotalCountTest"")] + public static partial TotalCount CreateTotalCountCounter(Meter meter); + }"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorDuplicateDimensionName.Id, d[0].Id); + } + + [Fact] + public async Task DuplicateDimensionStringNameInAttribute() + { + var d = await RunGenerator(@" + public class DimensionsTest + { + [Dimension(""dim1FromAttribute"")] + public string dim1 { get; set; } + public ChildDimensions childDimensions { get; set; } + } + + public class ChildDimensions + { + [Dimension(""dim1FromAttribute"")] + public string dim1 {get;set;} + } + + public static partial class MetricClass + { + [Histogram(typeof(DimensionsTest), Name=""TotalCountTest"")] + public static partial TotalCount CreateTotalCountCounter(Meter meter); + }"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorDuplicateDimensionName.Id, d[0].Id); + } + + [Fact] + public async Task DuplicateDimensionEnumName() + { + var d = await RunGenerator(@" + public class DimensionsTest + { + public Operations operations { get; set; } + public ChildDimensions childDimensions { get; set; } + } + + public class ChildDimensions + { + public Operations operations { get; set; } + } + + public enum Operations + { + Unknown = 0, + Operation1 = 1, + } + + public static partial class MetricClass + { + [Histogram(typeof(DimensionsTest), Name=""TotalCountTest"")] + public static partial TotalCount CreateTotalCountCounter(Meter meter); + }"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorDuplicateDimensionName.Id, d[0].Id); + } + + [Fact] + public async Task DuplicateDimensionEnumNameInAttribute() + { + var d = await RunGenerator(@" + public class DimensionsTest + { + [Dimension(""operations"")] + public Operations operations { get; set; } + public ChildDimensions childDimensions { get; set; } + } + + public class ChildDimensions + { + public Operations operations { get; set; } + } + + public enum Operations + { + Unknown = 0 + } + + public static partial class MetricClass + { + [Histogram(typeof(DimensionsTest), Name=""TotalCountTest"")] + public static partial TotalCount CreateTotalCountCounter(Meter meter); + }"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorDuplicateDimensionName.Id, d[0].Id); + } + + [Theory] + [InlineData("int")] + [InlineData("int?")] + [InlineData("System.Int32")] + [InlineData("System.Int32?")] + [InlineData("bool")] + [InlineData("bool?")] + [InlineData("System.Boolean")] + [InlineData("System.Boolean?")] + [InlineData("byte")] + [InlineData("byte?")] + [InlineData("char?")] + [InlineData("double?")] + [InlineData("decimal?")] + [InlineData("object")] + [InlineData("object?")] + [InlineData("System.Object")] + [InlineData("System.Object?")] + [InlineData("int[]")] + [InlineData("int?[]")] + [InlineData("int[]?")] + [InlineData("int?[]?")] + [InlineData("object[]")] + [InlineData("object[]?")] + [InlineData("System.Array")] + [InlineData("System.DateTime")] + [InlineData("System.DateTime?")] + [InlineData("System.IDisposable")] + [InlineData("System.Action")] + [InlineData("System.Action")] + [InlineData("System.Func")] + [InlineData("System.Nullable")] + [InlineData("System.Nullable")] + [InlineData("System.Nullable")] + [InlineData("System.Nullable")] + [InlineData("System.Nullable")] + public async Task InvalidDimensionType(string type) + { + var d = await RunGenerator(@$" + public class DimensionsTest + {{ + public {type} dim1 {{ get; set; }} + }} + + public static partial class MetricClass + {{ + [Histogram(typeof(DimensionsTest), Name=""TotalCountTest"")] + public static partial TotalCount CreateTotalCountCounter(Meter meter); + }}"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorInvalidDimensionType.Id, d[0].Id); + } + + [Fact] + public async Task TooManyDimensions() + { + StringBuilder sb = new StringBuilder(); + + int i = 0; + + for (; i < 21; i++) + { + sb.AppendLine($"public class C{i} : C{i + 1} {{ public string dim{i} {{get;set;}}}}"); + } + + sb.AppendLine($"public class C{i} {{ public string dim{i} {{get;set;}}}}"); + + sb.AppendLine(@" public static partial class MetricClass + { + [Histogram(typeof(C0), Name=""TotalCountTest"")] + public static partial TotalCount CreateTotalCountCounter(Meter meter); + }"); + + var d = await RunGenerator(sb.ToString()); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorTooManyDimensions.Id, d[0].Id); + } +} diff --git a/test/Generators/Microsoft.Gen.Metering/Unit/Common/ParserTests.cs b/test/Generators/Microsoft.Gen.Metering/Unit/Common/ParserTests.cs new file mode 100644 index 0000000000..16705ad5ad --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/Unit/Common/ParserTests.cs @@ -0,0 +1,658 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Gen.Shared; +using Xunit; + +namespace Microsoft.Gen.Metering.Test; + +public partial class ParserTests +{ + [Fact] + public async Task InvalidMethodName() + { + var d = await RunGenerator(@" + partial class C + { + [Counter(""d1"")] + static partial MetricName1 __M1(Meter meter); + }"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorInvalidMethodName.Id, d[0].Id); + } + + [Theory] + [InlineData("Nested.MetricClassName")] + [InlineData("Nested.Inner.MetricClassName")] + public async Task InvalidReturnTypeLocation(string returnType) + { + var d = await RunGenerator(@$" + partial class C + {{ + [Counter(""d1"")] + static partial {returnType} CreateMetricName(Meter meter); + }}"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorInvalidMethodReturnTypeLocation.Id, d[0].Id); + } + + [Theory] + [InlineData("GenericMetricClass")] + [InlineData("GenericMetricClass")] + public async Task InvalidReturnTypeArity(string returnType) + { + var d = await RunGenerator(@$" + partial class C + {{ + [Counter(""d1"")] + static partial {returnType} CreateMetricName(Meter meter); + }}"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorInvalidMethodReturnTypeArity.Id, d[0].Id); + } + + [Theory] + [InlineData("void")] + [InlineData("int")] + [InlineData("double")] + [InlineData("object")] + [InlineData("CustomClass")] + public async Task InvalidReturnType(string returnType) + { + var d = await RunGenerator(@$" + partial class C + {{ + class CustomClass {{ }} + + [Counter(""d1"")] + static partial {returnType} CreateMetricName(Meter meter); + }}"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorInvalidMethodReturnType.Id, d[0].Id); + } + + [Theory] + [InlineData("uint")] + [InlineData("ulong")] + [InlineData("ushort")] + [InlineData("System.UInt16")] + [InlineData("System.UInt32")] + [InlineData("System.UInt64")] + [InlineData("bool")] + [InlineData("System.Boolean")] + [InlineData("char")] + [InlineData("System.Char")] + [InlineData("CustomStruct")] + public async Task InvalidAttributeGenericType(string genericType) + { + var d = await RunGenerator(@$" + partial class C + {{ + struct CustomStruct {{ }} + + [Counter<{genericType}>] + static partial MeteringInstrument CreateMetricInstrument(Meter meter); + }}"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorInvalidAttributeGenericType.Id, d[0].Id); + } + + [Fact] + public async Task InvalidStaticDimensionsKeyNames() + { + var d = await RunGenerator(@" + partial class C + { + [Counter(""Env&Name"")] + static partial TestCounter CreateMetricName(Meter meter); + }"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorInvalidDimensionNames.Id, d[0].Id); + } + + [Fact] + public async Task InvalidDynamicDimensionsKeyNames() + { + var d = await RunGenerator(@" + partial class C + { + [Counter(""Req*Name"")] + static partial TestCounter CreateMetricName(Meter meter); + }"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorInvalidDimensionNames.Id, d[0].Id); + } + + [Fact] + public async Task ValidDimensionsKeyNames() + { + var d = await RunGenerator(@" + partial class C + { + [Counter(""Env.Name"", ""clustr:region"", ""Req_Name"", ""Req-Status"")] + static partial TestCounter CreateMetricName(Meter meter, string env, string region); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task ValidGenericAttribute() + { + var d = await RunGenerator(@" + partial class C + { + [Counter(""d1"")] + static partial TestCounter CreateTestCounter(Meter meter); + }"); + + Assert.Empty(d); + } + +#if ROSLYN_4_0_OR_GREATER + [Fact] + public async Task NotPartialMethod() + { + var d = await RunGenerator(@" + partial class C + { + [Counter(""d1"")] + static MetricName1 CreateMetricName(Meter meter); + }"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorNotPartialMethod.Id, d[0].Id); + } +#endif + + [Fact] + public async Task NotStaticMethod() + { + var d = await RunGenerator(@" + partial class C + { + [Counter(""d1"")] + partial MetricName1 CreateMetricName(Meter meter); + }"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorNotStaticMethod.Id, d[0].Id); + } + + [Fact] + public async Task MetricNameStartingLowercaseChar() + { + var d = await RunGenerator(@" + partial class C + { + [Counter(""d1"")] + static partial myMetric CreateMetricName(Meter meter); + }"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorInvalidMetricName.Id, d[0].Id); + } + + [Fact] + public async Task MetricNameStartingWithNonSymbol() + { + var d = await RunGenerator(@" + partial class C + { + [Counter(""d1"")] + static partial _Metric CreateMetricName(Meter meter); + }"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorInvalidMetricName.Id, d[0].Id); + } + + [Fact] + public async Task MethodIsGeneric() + { + var d = await RunGenerator(@" + partial class C + { + [Counter(""d1"")] + static partial MetricName1 CreateMetricName(Meter meter); + }"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorMethodIsGeneric.Id, d[0].Id); + } + + [Fact] + public async Task InvalidParameterName() + { + var d = await RunGenerator(@" + partial class C + { + [Counter(""d1"")] + static partial MetricName1 CreateMetricName(Meter _meter); + }"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorInvalidParameterName.Id, d[0].Id); + } + + [Fact] + public async Task NullDimensionNames() + { + var d = await RunGenerator(@" + partial class C + { + [Counter(null)] + static partial MetricName1 CreateMetricName(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task NullMetricNameParameter() + { + var d = await RunGenerator(@" + partial class C + { + [Counter(Name = null)] + static partial MetricName1 CreateMetricName(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task ValidParameterChecks() + { + var d = await RunGenerator(@" + internal class MetricConstants + { + public const string Env = ""Env.Name""; + public const string Region = ""region""; + public const string RequestName = ""requestName""; + public const string RequestStatus = ""requestStatus""; + } + + partial class C + { + [Counter(MetricConstants.Env, Name = ""myMetricName"")] + static partial MetricName1 CreateMetric(Meter meter); + + [Counter(MetricConstants.Env, MetricConstants.Region)] + static partial MetricName2 CreateMetric2(Meter meter); + + [Counter(MetricConstants.Env, MetricConstants.Region, MetricConstants.RequestName, MetricConstants.RequestStatus)] + static partial MetricName3 CreateMetric3(Meter meter); + + [Counter(MetricConstants.Env, MetricName = @""MetricType\\Standard"")] + static partial MetricName4 CreateMetric4(Meter meter); + + [Counter(MetricConstants.Env, MetricName = @""MetricType\Custom"")] + static partial MetricName5 CreateMetric5(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task ValidExtensionMethodsChecks() + { + var d = await RunGenerator(@" + internal class MetricConstants + { + public const string Env = ""Env.Name""; + public const string Region = ""region""; + public const string RequestName = ""requestName""; + public const string RequestStatus = ""requestStatus""; + } + + static partial class C + { + [Counter(MetricConstants.Env, Name = ""myMetricName"")] + static partial MetricName1 CreateMetric(this Meter meter); + + [Counter(MetricConstants.Env, MetricConstants.Region)] + static partial MetricName2 CreateMetric2(this Meter meter); + + [Counter(MetricConstants.Env, MetricConstants.Region, MetricConstants.RequestName, MetricConstants.RequestStatus)] + static partial MetricName3 CreateMetric3(this Meter meter); + + [Counter(MetricConstants.Env, MetricName = @""MetricType\\Standard"")] + static partial MetricName4 CreateMetric4(this Meter meter); + + [Counter(MetricConstants.Env, MetricName = @""MetricType\Custom"")] + static partial MetricName5 CreateMetric5(this Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task ExistingMetricName() + { + var d = await RunGenerator(@" + partial class C + { + [Counter(""d1"")] + static partial MetricName1 CreateMetricName(Meter meter); + + [Counter(""d2"")] + static partial MetricName1 CreateMetricWithSameNameAgain(Meter meter); + }"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorMetricNameReuse.Id, d[0].Id); + } + + [Fact] + public async Task NestedType() + { + var d = await RunGenerator(@" + partial class C + { + public partial class Nested + { + [Counter(""d1"")] + static partial MetricName1 CreateMetricName(Meter meter); + } + }"); + + Assert.Empty(d); + } + +#if ROSLYN_4_0_OR_GREATER + [Fact] + public async Task FileScopedNamespace() + { + var d = await RunGenerator(@" + namespace Test; + + public partial class C + { + [Counter(""d1"")] + static partial MetricName1 CreateMetricName(Meter meter); + }", inNamespace: false); + + Assert.Empty(d); + } +#endif + + [Theory] + [InlineData("")] + [InlineData("string s")] + [InlineData("int a, string s")] + public async Task MissingMeterObject(string args) + { + var d = await RunGenerator(@$" + partial class C + {{ + [Counter(""d1"")] + static partial MetricName1 CreateMetric({args}); + }}"); + + var diag = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorMissingMeter.Id, diag.Id); + } + + [Fact] + public async Task MeterIsNotFirst() + { + var d = await RunGenerator(@" + partial class C + { + [Counter(""d1"")] + static partial MetricName1 CreateMetric(string s, Meter meter); + }"); + + var diag = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorMissingMeter.Id, diag.Id); + } + + [Fact] + public async Task InvalidMethodBody() + { + var d = await RunGenerator(@" + partial class C + { + [Counter(""d1"")] + static partial MetricName1 CreateMetricName(Meter meter) + {} + }"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorMethodHasBody.Id, d[0].Id); + } + + [Fact] + public async Task SemanticProblems() + { + var d = await RunGenerator(@" + partial class C + { + [Counter(""d1"")] + + [Histogram(""d1"", ""d2"")] + + [Gauge(""d1"", ""d2"")] + + [Counter(""d1"")] + static partial 1Metric CreateMetricName(Meter meter); + + [CounterUnknown(""Unknown"")] + [Counter()] + static partial NewMetric CreateNewMetric(Meter meter); + + [Fact] + static partial NewMetric1 CreateNewMetric1(Meter meter) + {} + + // badly formatted + [Counter(""d1"")] + static partial Metric&Name1 Metric&Name1(Meter meter); + + // bogus parameter type + [Counter] + static partial Metric CreateMetric(XIMeter meter); + + // missing parameter name + [Counter] + static partial Metric2 CreateMetric2(Meter); + + // attribute applied to something other than method + [Counter] + int x; + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task MissingMeterType() + { + var d = await RunGenerator(@" + namespace Microsoft.Extensions.Telemetry.Metering + { + public sealed class CounterAttribute : System.Attribute {} + public sealed class HistogramAttribute : System.Attribute {} + } + partial class C + { + [Microsoft.Extensions.Telemetry.Metering.Counter] + static partial MetricName1 CreateMetricName(Meter meter); + }", + wrap: false, + inNamespace: false, + includeBaseReferences: true, + includeMeterReferences: false); + + Assert.Empty(d); + } + + [Fact] + public async Task MissingCounterAttributeType() + { + var d = await RunGenerator(@" + namespace System.Diagnostics.Metrics + { + public class Meter {} + } + namespace Microsoft.Extensions.Telemetry.Metering + { + public class HistogramAttribute : System.Attribute {} + } + partial class C + { + [Microsoft.Extensions.Telemetry.Metering.Histogram] + static partial MetricName1 CreateMetricName(Meter meter); + }", + wrap: false, + includeBaseReferences: true, + includeMeterReferences: false); + + Assert.Empty(d); + } + + [Fact] + public async Task MissingHistogramAttributeType() + { + var d = await RunGenerator(@" + namespace System.Diagnostics.Metrics + { + public class Meter {} + } + namespace Microsoft.Extensions.Telemetry.Metering + { + public class CounterAttribute : System.Attribute {} + } + partial class C + { + [Microsoft.Extensions.Telemetry.Metering.Counter] + static partial MetricName1 CreateMetricName(Meter meter); + }", + wrap: false, + includeBaseReferences: true, + includeMeterReferences: false); + + Assert.Empty(d); + } + + [Fact] + public async Task Cancellation() + { + await Assert.ThrowsAsync(async () => + _ = await RunGenerator(@" + partial class C + { + [Counter(""d1"")] + static partial MetricName1 CreateMetricName(Meter meter); + }", + cancellationToken: new CancellationToken(true))); + } + + [Fact] + public async Task ContainingClassIsInNestedNamespace() + { + var d = await RunGenerator(@" + namespace Nested + { + partial class C + { + [Counter(""d1"")] + static partial MetricName1 CreateMetricName(Meter meter); + } + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task ContainingClassHasTypeParameter() + { + var d = await RunGenerator(@" + partial class C + { + [Counter(""d1"")] + static partial MetricName1 CreateMetricName(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task MeterTypeIsConvertableToIMeter() + { + var d = await RunGenerator(@" + partial class C + { + [Counter(""d1"")] + static partial MetricName1 CreateMetricName(Meter meter); + }"); + + Assert.Empty(d); + } + + private static async Task> RunGenerator( + string code, + bool wrap = true, + bool inNamespace = true, + bool includeBaseReferences = true, + bool includeMeterReferences = true, + CancellationToken cancellationToken = default) + { + var text = code; + if (wrap) + { + var nspaceStart = "namespace Test {"; + var nspaceEnd = "}"; + if (!inNamespace) + { + nspaceStart = ""; + nspaceEnd = ""; + } + + text = $@" + {nspaceStart} + using Microsoft.Extensions.Telemetry.Metering; + using System.Diagnostics.Metrics; + {code} + {nspaceEnd} + "; + } + + Assembly[]? refs = null; + if (includeMeterReferences) + { + refs = new[] + { + Assembly.GetAssembly(typeof(Meter))!, + Assembly.GetAssembly(typeof(CounterAttribute))!, + Assembly.GetAssembly(typeof(HistogramAttribute))!, + Assembly.GetAssembly(typeof(GaugeAttribute))!, + }; + } + + var (d, _) = await RoslynTestUtils.RunGenerator( + new Generator(), + refs, + new[] { text }, + includeBaseReferences: includeBaseReferences, + cancellationToken: cancellationToken).ConfigureAwait(false); + + return d; + } +} diff --git a/test/Generators/Microsoft.Gen.Metering/Unit/Common/StrongTypeConfigTests.cs b/test/Generators/Microsoft.Gen.Metering/Unit/Common/StrongTypeConfigTests.cs new file mode 100644 index 0000000000..49e18418bc --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/Unit/Common/StrongTypeConfigTests.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Gen.Metering.Model; +using Xunit; + +namespace Microsoft.Gen.Metering.Test; + +public class StrongTypeConfigTests +{ + [Fact] + public void Fields_Should_BeInitialized() + { + var instance = new StrongTypeConfig(); + Assert.Empty(instance.Name); + Assert.Empty(instance.Path); + Assert.Empty(instance.DimensionName); + } +} diff --git a/test/Generators/Microsoft.Gen.Metering/Unit/Directory.Build.props b/test/Generators/Microsoft.Gen.Metering/Unit/Directory.Build.props new file mode 100644 index 0000000000..78a949ac8b --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/Unit/Directory.Build.props @@ -0,0 +1,26 @@ + + + + + Microsoft.Gen.Metering.Test + Unit tests for Gen.Metering + true + true + + + + + + + + + + + + + + + + + + diff --git a/test/Generators/Microsoft.Gen.Metering/Unit/Roslyn3.8/Microsoft.Gen.Metering.Roslyn3.8.Unit.Tests.csproj b/test/Generators/Microsoft.Gen.Metering/Unit/Roslyn3.8/Microsoft.Gen.Metering.Roslyn3.8.Unit.Tests.csproj new file mode 100644 index 0000000000..eac2eac217 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/Unit/Roslyn3.8/Microsoft.Gen.Metering.Roslyn3.8.Unit.Tests.csproj @@ -0,0 +1,5 @@ + + + 3.8 + + diff --git a/test/Generators/Microsoft.Gen.Metering/Unit/Roslyn4.0/Microsoft.Gen.Metering.Roslyn4.0.Unit.Tests.csproj b/test/Generators/Microsoft.Gen.Metering/Unit/Roslyn4.0/Microsoft.Gen.Metering.Roslyn4.0.Unit.Tests.csproj new file mode 100644 index 0000000000..18ce9dd9ba --- /dev/null +++ b/test/Generators/Microsoft.Gen.Metering/Unit/Roslyn4.0/Microsoft.Gen.Metering.Roslyn4.0.Unit.Tests.csproj @@ -0,0 +1,6 @@ + + + 4.0 + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + diff --git a/test/Generators/Microsoft.Gen.MeteringReports/Unit/Common/EmitterTests.cs b/test/Generators/Microsoft.Gen.MeteringReports/Unit/Common/EmitterTests.cs new file mode 100644 index 0000000000..1b75cdeeed --- /dev/null +++ b/test/Generators/Microsoft.Gen.MeteringReports/Unit/Common/EmitterTests.cs @@ -0,0 +1,176 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using Microsoft.Gen.Metering.Model; +using Xunit; + +namespace Microsoft.Gen.MeteringReports.Test; + +public class EmitterTests +{ + private static readonly ReportedMetricClass[] _metricClasses = new[] + { + new ReportedMetricClass + { + Name = "MetricClass1", + RootNamespace = "MetricContainingAssembly1", + Methods = new [] + { + new ReportedMetricMethod + { + MetricName = "Requests", + Summary = "Requests summary.", + Kind = InstrumentKind.Counter, + Dimensions = new() { "StatusCode", "ErrorCode"}, + DimensionsDescriptions = new Dictionary + { + { "StatusCode", "Status code for request." }, + { "ErrorCode", "Error code for request." } + } + }, + new ReportedMetricMethod + { + MetricName = "Latency", + Summary = "Latency summary.", + Kind = InstrumentKind.Histogram, + Dimensions = new() { "Dim1" }, + DimensionsDescriptions = new() + }, + new ReportedMetricMethod + { + MetricName = "MemoryUsage", + Kind = InstrumentKind.Gauge, + Dimensions = new(), + DimensionsDescriptions = new() + } + } + }, + new ReportedMetricClass + { + Name = "MetricClass2", + RootNamespace = "MetricContainingAssembly2", + Methods = new[] + { + new ReportedMetricMethod + { + MetricName = "Counter", + Summary = "Counter summary.", + Kind = InstrumentKind.Counter, + Dimensions = new(), + DimensionsDescriptions = new() + }, + new ReportedMetricMethod + { + MetricName = "R9\\Test\\MemoryUsage", + Summary = "MemoryUsage summary.", + Kind = InstrumentKind.Gauge, + Dimensions = new() { "Path"}, + DimensionsDescriptions = new Dictionary + { + { "Path", "R9\\Test\\Description\\Path" } + }, + } + } + } + }; + + [Fact] + public void EmitterShouldThrowExceptionUponCancellation() + { + Assert.Throws(() => MetricDefinitionEmitter.GenerateReport(_metricClasses, new CancellationToken(true))); + } + + [Fact] + public void EmitterShouldOutputEmptyForNullInput() + { + Assert.Equal(string.Empty, MetricDefinitionEmitter.GenerateReport(null!, CancellationToken.None)); + } + + [Fact] + public void EmitterShouldOutputEmptyForEmptyInputForMetricClass() + { + Assert.Equal(string.Empty, MetricDefinitionEmitter.GenerateReport(Array.Empty(), CancellationToken.None)); + } + + [Fact] + public void GetMetricClassDefinition_GivenMetricTypeIsUnknown_ThrowsNotSupportedException() + { + const int UnknownMetricType = 10; + + var metricClass = new ReportedMetricClass + { + Name = "Test", + RootNamespace = "MetricContainingAssembly3", + Methods = new[] + { + new ReportedMetricMethod + { + MetricName = "UnknownMetric", + Kind = (InstrumentKind)UnknownMetricType, + Dimensions = new() { "Dim1" } + } + } + }; + + Assert.Throws(() => MetricDefinitionEmitter.GenerateReport(new[] { metricClass }, CancellationToken.None)); + } + + [Fact] + public void EmitterShouldOutputInJSONFormat() + { + const string Expected = + "[" + + "\n {" + + "\n \"MetricContainingAssembly1\":" + + "\n [" + + "\n {" + + "\n \"MetricName\": \"Requests\"," + + "\n \"MetricDescription\": \"Requests summary.\"," + + "\n \"InstrumentName\": \"Counter\"," + + "\n \"Dimensions\": {" + + "\n \"StatusCode\": \"Status code for request.\"," + + "\n \"ErrorCode\": \"Error code for request.\"" + + "\n }" + + "\n }," + + "\n {" + + "\n \"MetricName\": \"Latency\"," + + "\n \"MetricDescription\": \"Latency summary.\"," + + "\n \"InstrumentName\": \"Histogram\"," + + "\n \"Dimensions\": {" + + "\n \"Dim1\": \"\"" + + "\n }" + + "\n }," + + "\n {" + + "\n \"MetricName\": \"MemoryUsage\"," + + "\n \"InstrumentName\": \"Gauge\"" + + "\n }" + + "\n ]" + + "\n }," + + "\n {" + + "\n \"MetricContainingAssembly2\":" + + "\n [" + + "\n {" + + "\n \"MetricName\": \"Counter\"," + + "\n \"MetricDescription\": \"Counter summary.\"," + + "\n \"InstrumentName\": \"Counter\"" + + "\n }," + + "\n {" + + "\n \"MetricName\": \"R9\\\\Test\\\\MemoryUsage\"," + + "\n \"MetricDescription\": \"MemoryUsage summary.\"," + + "\n \"InstrumentName\": \"Gauge\"," + + "\n \"Dimensions\": {" + + "\n \"Path\": \"R9\\\\Test\\\\Description\\\\Path\"" + + "\n }" + + "\n }" + + "\n ]" + + "\n }" + + "\n]"; + + string json = MetricDefinitionEmitter.GenerateReport(_metricClasses, CancellationToken.None); + + Assert.Equal(Expected, json); + } +} diff --git a/test/Generators/Microsoft.Gen.MeteringReports/Unit/Common/GeneratorTests.cs b/test/Generators/Microsoft.Gen.MeteringReports/Unit/Common/GeneratorTests.cs new file mode 100644 index 0000000000..b232087751 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MeteringReports/Unit/Common/GeneratorTests.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Xunit; + +namespace Microsoft.Gen.MeteringReports.Test; + +/// +/// Test for . +/// +public class GeneratorTests +{ + [Fact] + public void GeneratorShouldNotDoAnythingIfGeneralExecutionContextDoesNotHaveClassDeclarationSyntaxReceiver() + { + var defaultGeneralExecutionContext = default(GeneratorExecutionContext); + new MetricDefinitionGenerator().Execute(defaultGeneralExecutionContext); + + Assert.Null(defaultGeneralExecutionContext.SyntaxReceiver); + } +} diff --git a/test/Generators/Microsoft.Gen.MeteringReports/Unit/Directory.Build.props b/test/Generators/Microsoft.Gen.MeteringReports/Unit/Directory.Build.props new file mode 100644 index 0000000000..1309376fe5 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MeteringReports/Unit/Directory.Build.props @@ -0,0 +1,26 @@ + + + + + Microsoft.Gen.MeteringReports.Test + Unit tests for Gen.MeteringReports. + + + + true + true + + + + + + + + + + + + + + + diff --git a/test/Generators/Microsoft.Gen.MeteringReports/Unit/Roslyn3.8/Microsoft.Gen.MeteringReports.Roslyn3.8.Unit.Tests.csproj b/test/Generators/Microsoft.Gen.MeteringReports/Unit/Roslyn3.8/Microsoft.Gen.MeteringReports.Roslyn3.8.Unit.Tests.csproj new file mode 100644 index 0000000000..eac2eac217 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MeteringReports/Unit/Roslyn3.8/Microsoft.Gen.MeteringReports.Roslyn3.8.Unit.Tests.csproj @@ -0,0 +1,5 @@ + + + 3.8 + + diff --git a/test/Generators/Microsoft.Gen.MeteringReports/Unit/Roslyn4.0/Microsoft.Gen.MeteringReports.Roslyn4.0.Unit.Tests.csproj b/test/Generators/Microsoft.Gen.MeteringReports/Unit/Roslyn4.0/Microsoft.Gen.MeteringReports.Roslyn4.0.Unit.Tests.csproj new file mode 100644 index 0000000000..18ce9dd9ba --- /dev/null +++ b/test/Generators/Microsoft.Gen.MeteringReports/Unit/Roslyn4.0/Microsoft.Gen.MeteringReports.Roslyn4.0.Unit.Tests.csproj @@ -0,0 +1,6 @@ + + + 4.0 + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/CustomAttrTests.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/CustomAttrTests.cs new file mode 100644 index 0000000000..9770924b5b --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/CustomAttrTests.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using CustomAttr; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Gen.OptionsValidation.Test; + +public class CustomAttrTests +{ + [Fact] + public void Invalid() + { + var firstModel = new FirstModel + { + P1 = 'a', + P2 = 'x', + }; + + var validator = new FirstValidator(); + var vr = validator.Validate("CustomAttr", firstModel); + + Utils.VerifyValidateOptionsResult(vr, 2, "P1", "P2"); + } + + [Fact] + public void Valid() + { + var firstModel = new FirstModel + { + P1 = 'A', + P2 = 'A', + }; + + var validator = new FirstValidator(); + Assert.Equal(ValidateOptionsResult.Success, validator.Validate("CustomAttr", firstModel)); + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/EnumerationTests.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/EnumerationTests.cs new file mode 100644 index 0000000000..d2f9bddca2 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/EnumerationTests.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Enumeration; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Gen.OptionsValidation.Test; + +public class EnumerationTests +{ + [Fact] + public void Invalid() + { + var secondModelC = new SecondModel + { + P6 = "1234", + }; + + var secondModelB = new SecondModel + { + P6 = "12345", + }; + + var secondModel = new SecondModel + { + P6 = "1234", + }; + + ThirdModel? thirdModel = new ThirdModel + { + Value = 11 + }; + + var firstModel = new FirstModel + { + P1 = new[] { secondModel }, + P2 = new[] { secondModel, secondModelB, secondModelC }, + P51 = new[] { thirdModel } + }; + + var validator = default(FirstValidator); + var vr = validator.Validate("Enumeration", firstModel); + + Utils.VerifyValidateOptionsResult(vr, 4, "P1[0].P6", "P2[0].P6", "P2[2].P6", "P51[0].Value"); + } + + [Fact] + public void NullElement() + { + var firstModel = new FirstModel + { + P1 = new[] { (SecondModel)null! }, + }; + + var validator = default(FirstValidator); + var vr = validator.Validate("Enumeration", firstModel); + + Utils.VerifyValidateOptionsResult(vr, 1, "P1[0]"); + } + + [Fact] + public void Valid() + { + var secondModel = new SecondModel + { + P6 = "12345", + }; + + var thirdModelA = new ThirdModel + { + Value = 2 + }; + + var thirdModelB = new ThirdModel + { + Value = 9 + }; + + var firstModel = new FirstModel + { + P1 = new[] { secondModel }, + P2 = new[] { secondModel }, + P3 = new[] { (SecondModel?)null }, + P4 = new[] { thirdModelA, thirdModelB }, + P5 = new ThirdModel?[] { thirdModelA, default }, + P51 = new ThirdModel?[] { thirdModelB, default } + }; + + var validator = default(FirstValidator); + Assert.Equal(ValidateOptionsResult.Success, validator.Validate("Enumeration", firstModel)); + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/FieldTests.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/FieldTests.cs new file mode 100644 index 0000000000..3656d4f359 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/FieldTests.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Fields; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Gen.OptionsValidation.Test; + +public class FieldTests +{ + [Fact] + public void Invalid() + { + var thirdModel = new ThirdModel + { + P5 = "1234", + }; + + var secondModel = new SecondModel + { + P4 = "1234", + }; + + var firstModel = new FirstModel + { + P1 = "1234", + P2 = secondModel, + P3 = thirdModel, + }; + + var validator = default(FirstValidator); + var vr = validator.Validate("Fields", firstModel); + + Utils.VerifyValidateOptionsResult(vr, 3, "P1", "P2.P4", "P3.P5"); + } + + [Fact] + public void Valid() + { + var thirdModel = new ThirdModel + { + P5 = "12345", + P6 = 1 + }; + + var secondModel = new SecondModel + { + P4 = "12345", + }; + + var firstModel = new FirstModel + { + P1 = "12345", + P2 = secondModel, + P3 = thirdModel, + }; + + var validator = default(FirstValidator); + Assert.Equal(ValidateOptionsResult.Success, validator.Validate("Fields", firstModel)); + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/FunnyStringsTests.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/FunnyStringsTests.cs new file mode 100644 index 0000000000..29b62a5c4b --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/FunnyStringsTests.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FunnyStrings; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Gen.OptionsValidation.Test; + +public class FunnyStringsTests +{ + [Fact] + public void Invalid() + { + var firstModel = new FirstModel + { + P1 = "XXX", + }; + + var validator = default(FirstValidator); + var vr = validator.Validate("FunnyStrings", firstModel); + + Utils.VerifyValidateOptionsResult(vr, 1, "P1"); + } + + [Fact] + public void Valid() + { + var firstModel = new FirstModel + { + P1 = "\"\r\n\\", + }; + + var validator = default(FirstValidator); + Assert.Equal(ValidateOptionsResult.Success, validator.Validate("FunnyStrings", firstModel)); + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/GenericsTests.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/GenericsTests.cs new file mode 100644 index 0000000000..838fe52968 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/GenericsTests.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Generics; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Gen.OptionsValidation.Test; + +public class GenericsTests +{ + [Fact] + public void Invalid() + { + var secondModel = new SecondModel + { + P4 = "1234", + }; + + var firstModel = new FirstModel + { + P1 = "1234", + P3 = secondModel, + }; + + var validator = new FirstValidator(); + var vr = validator.Validate("Generics", firstModel); + + Utils.VerifyValidateOptionsResult(vr, 2, "P1", "P3.P4"); + } + + [Fact] + public void Valid() + { + var secondModel = new SecondModel + { + P4 = "12345", + }; + + var firstModel = new FirstModel + { + P1 = "12345", + P3 = secondModel, + }; + + var validator = new FirstValidator(); + Assert.Equal(ValidateOptionsResult.Success, validator.Validate("Generics", firstModel)); + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/MultiModelValidatorTests.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/MultiModelValidatorTests.cs new file mode 100644 index 0000000000..fff83ced91 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/MultiModelValidatorTests.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using MultiModelValidator; +using Xunit; + +namespace Microsoft.Gen.OptionsValidation.Test; + +public class MultiModelValidatorTests +{ + [Fact] + public void Invalid() + { + var secondModel = new SecondModel + { + P3 = "1234", + }; + + var firstModel = new FirstModel + { + P1 = "1234", + P2 = secondModel, + }; + + var validator = default(MultiValidator); + var vr = validator.Validate("MultiModelValidator", firstModel); + + Utils.VerifyValidateOptionsResult(vr, 2, "P1", "P2.P3"); + } + + [Fact] + public void Valid() + { + var secondModel = new SecondModel + { + P3 = "12345", + }; + + var firstModel = new FirstModel + { + P1 = "12345", + P2 = secondModel, + }; + + var validator = default(MultiValidator); + Assert.Equal(ValidateOptionsResult.Success, validator.Validate("MultiModelValidator", firstModel)); + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/NestedTests.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/NestedTests.cs new file mode 100644 index 0000000000..8a4b04361a --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/NestedTests.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if ROSLYN_4_0_OR_GREATER + +using Microsoft.Extensions.Options; +using Nested; +using Xunit; + +namespace Microsoft.Gen.OptionsValidation.Test; + +public class NestedTests +{ + [Fact] + public void Invalid() + { + var thirdModel = new Container1.ThirdModel + { + P6 = "1234", + }; + + var secondModel = new Container1.SecondModel + { + P5 = "1234", + }; + + var firstModel = new Container1.FirstModel + { + P1 = "1234", + P2 = secondModel, + P3 = thirdModel, + P4 = secondModel, + }; + + var validator = default(Container2.Container3.FirstValidator); + var vr = validator.Validate("Nested", firstModel); + + Utils.VerifyValidateOptionsResult(vr, 4, "P1", "P2.P5", "P3.P6", "P4.P5"); + } + + [Fact] + public void Valid() + { + var thirdModel = new Container1.ThirdModel + { + P6 = "12345", + }; + + var secondModel = new Container1.SecondModel + { + P5 = "12345", + }; + + var firstModel = new Container1.FirstModel + { + P1 = "12345", + P2 = secondModel, + P3 = thirdModel, + P4 = secondModel, + }; + + var validator = default(Container2.Container3.FirstValidator); + Assert.Equal(ValidateOptionsResult.Success, validator.Validate("Nested", firstModel)); + } +} + +#endif diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/NoNamespaceTests.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/NoNamespaceTests.cs new file mode 100644 index 0000000000..5530b63953 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/NoNamespaceTests.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Gen.OptionsValidation.Test; + +public class NoNamespaceTests +{ + [Fact] + public void Invalid() + { + var thirdModel = new ThirdModelNoNamespace + { + P5 = "1234", + }; + + var secondModel = new SecondModelNoNamespace + { + P4 = "1234", + }; + + var firstModel = new FirstModelNoNamespace + { + P1 = "1234", + P2 = secondModel, + P3 = thirdModel, + }; + + var validator = new FirstValidatorNoNamespace(); + var vr = validator.Validate("NoNamespace", firstModel); + + Utils.VerifyValidateOptionsResult(vr, 3, "P1", "P2.P4", "P3.P5"); + } + + [Fact] + public void Valid() + { + var thirdModel = new ThirdModelNoNamespace + { + P5 = "12345", + }; + + var secondModel = new SecondModelNoNamespace + { + P4 = "12345", + }; + + var firstModel = new FirstModelNoNamespace + { + P1 = "12345", + P2 = secondModel, + P3 = thirdModel, + }; + + var validator = new FirstValidatorNoNamespace(); + Assert.Equal(ValidateOptionsResult.Success, validator.Validate("NoNamespace", firstModel)); + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/OptionsValidationTests.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/OptionsValidationTests.cs new file mode 100644 index 0000000000..49941010f1 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/OptionsValidationTests.cs @@ -0,0 +1,442 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using Microsoft.Extensions.Options; +using TestClasses.OptionsValidation; +using Xunit; + +namespace Microsoft.Gen.OptionsValidation.Test; + +public class OptionsValidationTests +{ + [Fact] + public void RequiredAttributeValid() + { + var validModel = new RequiredAttributeModel + { + Val = "val" + }; + + var modelValidator = new RequiredAttributeModelValidator(); + var result = modelValidator.Validate(nameof(validModel), validModel); + + Assert.Equal(ValidateOptionsResult.Success, result); + } + + [Fact] + public void RequiredAttributeInvalid() + { + var validModel = new RequiredAttributeModel + { + Val = null + }; + + var modelValidator = new RequiredAttributeModelValidator(); + Utils.VerifyValidateOptionsResult(modelValidator.Validate(nameof(validModel), validModel), 1); + } + + [Fact] + public void RegularExpressionAttributeValid() + { + var validModel = new RegularExpressionAttributeModel + { + Val = " " + }; + + var modelValidator = new RegularExpressionAttributeModelValidator(); + var result = modelValidator.Validate(nameof(validModel), validModel); + + Assert.Equal(ValidateOptionsResult.Success, result); + } + + [Fact] + public void RegularExpressionAttributeInvalid() + { + var validModel = new RegularExpressionAttributeModel + { + Val = "Not Space" + }; + + var modelValidator = new RegularExpressionAttributeModelValidator(); + Utils.VerifyValidateOptionsResult(modelValidator.Validate(nameof(validModel), validModel), 1); + } + + [Fact] + public void EmailAttributeValid() + { + var validModel = new EmailAttributeModel + { + Val = "abc@xyz.com" + }; + + var modelValidator = new EmailAttributeModelValidator(); + var result = modelValidator.Validate(nameof(validModel), validModel); + + Assert.Equal(ValidateOptionsResult.Success, result); + } + + [Fact] + public void EmailAttributeInvalid() + { + var validModel = new EmailAttributeModel + { + Val = "Not Email Address" + }; + + var modelValidator = new EmailAttributeModelValidator(); + Utils.VerifyValidateOptionsResult(modelValidator.Validate(nameof(validModel), validModel), 1); + } + + [Fact] + public void CustomValidationAttributeValid() + { + var validModel = new CustomValidationAttributeModel + { + Val = "Pass" + }; + + var modelValidator = new CustomValidationAttributeModelValidator(); + var result = modelValidator.Validate(nameof(validModel), validModel); + + Assert.Equal(ValidateOptionsResult.Success, result); + } + + [Fact] + public void CustomValidationAttributeInvalid() + { + var validModel = new CustomValidationAttributeModel + { + Val = "NOT PASS" + }; + + var modelValidator = new CustomValidationAttributeModelValidator(); + Assert.Throws(() => modelValidator.Validate(nameof(validModel), validModel)); + } + + [Fact] + public void DataTypeAttributeValid() + { + var validModel = new DataTypeAttributeModel + { + Val = "ABC" + }; + + var modelValidator = new DataTypeAttributeModelValidator(); + var result = modelValidator.Validate(nameof(validModel), validModel); + + Assert.Equal(ValidateOptionsResult.Success, result); + } + + [Fact] + public void RangeAttributeModelIntValid() + { + var validModel = new RangeAttributeModelInt + { + Val = 1 + }; + + var modelValidator = new RangeAttributeModelIntValidator(); + var result = modelValidator.Validate(nameof(validModel), validModel); + + Assert.Equal(ValidateOptionsResult.Success, result); + } + + [Fact] + public void RangeAttributeModelIntInvalid() + { + var validModel = new RangeAttributeModelInt + { + Val = 0 + }; + + var modelValidator = new RangeAttributeModelIntValidator(); + Utils.VerifyValidateOptionsResult(modelValidator.Validate(nameof(validModel), validModel), 1); + } + + [Fact] + public void RangeAttributeModelDoubleValid() + { + var validModel = new RangeAttributeModelDouble + { + Val = 0.6 + }; + + var modelValidator = new RangeAttributeModelDoubleValidator(); + var result = modelValidator.Validate(nameof(validModel), validModel); + + Assert.Equal(ValidateOptionsResult.Success, result); + } + + [Fact] + public void RangeAttributeModelDoubleInvalid() + { + var validModel = new RangeAttributeModelDouble + { + Val = 0.1 + }; + + var modelValidator = new RangeAttributeModelDoubleValidator(); + Utils.VerifyValidateOptionsResult(modelValidator.Validate(nameof(validModel), validModel), 1); + } + + [Fact] + public void RangeAttributeModelDateValid() + { +#if NETCOREAPP3_1_OR_GREATER + // Setting non-invariant culture to check that + // attribute's "ParseLimitsInInvariantCulture" property + // was set up correctly in the validator: + CultureInfo.CurrentCulture = new CultureInfo("cs"); +#else + // Setting invariant culture to avoid DateTime parsing discrepancies: + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; +#endif + var validModel = new RangeAttributeModelDate + { + Val = new DateTime(day: 3, month: 1, year: 2004) + }; + + var modelValidator = new RangeAttributeModelDateValidator(); + var result = modelValidator.Validate(nameof(validModel), validModel); + + Assert.Equal(ValidateOptionsResult.Success, result); + } + + [Fact] + public void RangeAttributeModelDateInvalid() + { + var validModel = new RangeAttributeModelDate + { + Val = new DateTime(day: 1, month: 1, year: 2004) + }; + + var modelValidator = new RangeAttributeModelDateValidator(); + Utils.VerifyValidateOptionsResult(modelValidator.Validate(nameof(validModel), validModel), 1); + } + + [Fact] + public void MultipleAttributeModelValid() + { + var validModel = new MultipleAttributeModel + { + Val1 = "abc", + Val2 = 2, + Val3 = 4, + Val4 = 6 + }; + + var modelValidator = new MultipleAttributeModelValidator(); + var result = modelValidator.Validate(nameof(validModel), validModel); + + Assert.Equal(ValidateOptionsResult.Success, result); + } + + [Theory] + [InlineData("", 2, 4, 7)] + [InlineData(null, 2, 4, 7)] + [InlineData("abc", 0, 4, 9)] + [InlineData("abc", 2, 8, 8)] + [InlineData("abc", 2, 4, 10)] + public void MultipleAttributeModelInvalid(string val1, int val2, int val3, int val4) + { + var validModel = new MultipleAttributeModel + { + Val1 = val1, + Val2 = val2, + Val3 = val3, + Val4 = val4 + }; + + var modelValidator = new MultipleAttributeModelValidator(); + Utils.VerifyValidateOptionsResult(modelValidator.Validate(nameof(validModel), validModel), 1); + } + + [Fact] + public void CustomTypeCustomValidationAttributeModelValid() + { + var validModel = new CustomTypeCustomValidationAttributeModel + { + Val = new CustomType { Val1 = "Pass", Val2 = "Pass" } + }; + + var modelValidator = new CustomTypeCustomValidationAttributeModelValidator(); + var result = modelValidator.Validate(nameof(validModel), validModel); + + Assert.Equal(ValidateOptionsResult.Success, result); + } + + [Fact] + public void CustomTypeCustomValidationAttributeModelInvalid() + { + var validModel = new CustomTypeCustomValidationAttributeModel + { + Val = new CustomType { Val1 = "Pass", Val2 = "Not Pass" } + }; + + var modelValidator = new CustomTypeCustomValidationAttributeModelValidator(); + Assert.Throws(() => modelValidator.Validate(nameof(validModel), validModel)); + } + + [Fact] + public void DerivedModelIsValid() + { + var validModel = new DerivedModel + { + Val = 1, + DerivedVal = "Valid", + VirtualValWithAttr = 1, + VirtualValWithoutAttr = null + }; + + ((RequiredAttributeModel)validModel).Val = "Valid hidden member from base class"; + + var validator = new DerivedModelValidator(); + var result = validator.Validate(nameof(validModel), validModel); + Assert.Equal(ValidateOptionsResult.Success, result); + } + + [Theory] + [InlineData(0, "", 1, null, "Valid hidden member from base class")] + [InlineData(null, "Valid", 1, null, "Valid hidden member from base class")] + [InlineData(1, "Valid", null, null, "Valid hidden member from base class")] + public void DerivedModelIsInvalid(int? val, string? derivedVal, int? virtValAttr, int? virtVal, string? hiddenValBaseClass) + { + var invalidModel = new DerivedModel + { + Val = val, + DerivedVal = derivedVal, + VirtualValWithAttr = virtValAttr, + VirtualValWithoutAttr = virtVal + }; + + ((RequiredAttributeModel)invalidModel).Val = hiddenValBaseClass; + + var validator = new DerivedModelValidator(); + Utils.VerifyValidateOptionsResult(validator.Validate(nameof(invalidModel), invalidModel), 1); + } + + [Fact] + public void LeafModelIsValid() + { + var validModel = new LeafModel + { + Val = 1, + DerivedVal = "Valid", + VirtualValWithAttr = null, + VirtualValWithoutAttr = 1 + }; + + ((RequiredAttributeModel)validModel).Val = "Valid hidden member from base class"; + + var validator = new LeafModelValidator(); + var result = validator.Validate(nameof(validModel), validModel); + Assert.Equal(ValidateOptionsResult.Success, result); + } + + [Fact] + public void ComplexModelValid() + { + var validModel = new ComplexModel + { + ComplexVal = new RequiredAttributeModel { Val = "Valid" } + }; + + var modelValidator = new ComplexModelValidator(); + var result = modelValidator.Validate(nameof(validModel), validModel); + Assert.Equal(ValidateOptionsResult.Success, result); + + validModel = new ComplexModel + { + ValWithoutOptionsValidator = new TypeWithoutOptionsValidator + { + Val1 = "Valid", + Val2 = new DateTime(day: 3, month: 1, year: 2004) + } + }; + + // Setting invariant culture to avoid DateTime parsing discrepancies: + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + result = modelValidator.Validate(nameof(validModel), validModel); + Assert.Equal(ValidateOptionsResult.Success, result); + + validModel = new ComplexModel + { + ValWithoutOptionsValidator = new TypeWithoutOptionsValidator + { + Val1 = "A", + Val2 = new DateTime(day: 2, month: 2, year: 2004), + YetAnotherComplexVal = new RangeAttributeModelDouble { Val = 0.7 } + } + }; + + result = modelValidator.Validate(nameof(validModel), validModel); + Assert.Equal(ValidateOptionsResult.Success, result); + } + + [Fact] + public void ComplexModelInvalid() + { + var invalidModel = new ComplexModel + { + ComplexVal = new RequiredAttributeModel { Val = null } + }; + + var modelValidator = new ComplexModelValidator(); + Utils.VerifyValidateOptionsResult(modelValidator.Validate(nameof(invalidModel), invalidModel), 1); + + invalidModel = new ComplexModel + { + ValWithoutOptionsValidator = new TypeWithoutOptionsValidator { Val1 = "Valid", Val2 = new DateTime(2003, 3, 3) } + }; + + Utils.VerifyValidateOptionsResult(modelValidator.Validate(nameof(invalidModel), invalidModel), 1); + + invalidModel = new ComplexModel + { + ValWithoutOptionsValidator = new TypeWithoutOptionsValidator { Val1 = string.Empty, Val2 = new DateTime(2004, 3, 3) } + }; + + Utils.VerifyValidateOptionsResult(modelValidator.Validate(nameof(invalidModel), invalidModel), 1); + + invalidModel = new ComplexModel + { + ValWithoutOptionsValidator = new TypeWithoutOptionsValidator + { + Val1 = "A", + Val2 = new DateTime(2004, 2, 2), + YetAnotherComplexVal = new RangeAttributeModelDouble { Val = 0.4999 } + } + }; + + Utils.VerifyValidateOptionsResult(modelValidator.Validate(nameof(invalidModel), invalidModel), 1); + } + + [Fact] + public void AttributePropertyModelTestOnErrorMessage() + { + var validModel = new AttributePropertyModel + { + Val1 = 5, + Val2 = 1 + }; + + var modelValidator = new AttributePropertyModelValidator(); + Utils.VerifyValidateOptionsResult(modelValidator.Validate(nameof(validModel), validModel), 1); + } + + [Fact] + public void AttributePropertyModelTestOnErrorMessageResource() + { + var validModel = new AttributePropertyModel + { + Val1 = 1, + Val2 = 5 + }; + + var modelValidator = new AttributePropertyModelValidator(); + Utils.VerifyValidateOptionsResult(modelValidator.Validate(nameof(validModel), validModel), 1); + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/RandomMembersTests.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/RandomMembersTests.cs new file mode 100644 index 0000000000..1c87892b27 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/RandomMembersTests.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using RandomMembers; +using Xunit; + +namespace Microsoft.Gen.OptionsValidation.Test; + +public class RandomMembersTests +{ + [Fact] + public void Invalid() + { + var firstModel = new FirstModel + { + P1 = "1234", + }; + + var validator = new FirstValidator(); + var vr = validator.Validate("RandomMembers", firstModel); + + Utils.VerifyValidateOptionsResult(vr, 1, "P1"); + } + + [Fact] + public void Valid() + { + var firstModel = new FirstModel + { + P1 = "12345", + }; + + var validator = new FirstValidator(); + Assert.Equal(ValidateOptionsResult.Success, validator.Validate("RandomMembers", firstModel)); + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/RecordTypesTests.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/RecordTypesTests.cs new file mode 100644 index 0000000000..bd0d872c8b --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/RecordTypesTests.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if ROSLYN_4_0_OR_GREATER + +using Microsoft.Extensions.Options; +using RecordTypes; +using Xunit; + +namespace Microsoft.Gen.OptionsValidation.Test; + +public class RecordTypesTests +{ + [Fact] + public void Invalid() + { + var thirdModel = new ThirdModel + { + P6 = "1234", + }; + + var secondModel = new SecondModel + { + P5 = "1234", + }; + + var firstModel = new FirstModel + { + P1 = "1234", + P2 = secondModel, + P3 = secondModel, + P4 = thirdModel, + }; + + var validator = default(FirstValidator); + var vr = validator.Validate("RecordTypes", firstModel); + + Utils.VerifyValidateOptionsResult(vr, 4, "P1", "P2.P5", "P3.P5", "P4.P6"); + } + + [Fact] + public void Valid() + { + var thirdModel = new ThirdModel + { + P6 = "12345", + }; + + var secondModel = new SecondModel + { + P5 = "12345", + }; + + var firstModel = new FirstModel + { + P1 = "12345", + P2 = secondModel, + P3 = secondModel, + P4 = thirdModel, + }; + + var validator = default(FirstValidator); + Assert.Equal(ValidateOptionsResult.Success, validator.Validate("RecordTypes", firstModel)); + } +} + +#endif diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/RepeatedTypesTests.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/RepeatedTypesTests.cs new file mode 100644 index 0000000000..b45fcbf9ce --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/RepeatedTypesTests.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using RepeatedTypes; +using Xunit; + +namespace Microsoft.Gen.OptionsValidation.Test; + +public class RepeatedTypesTests +{ + [Fact] + public void Invalid() + { + var thirdModel = new ThirdModel + { + P5 = "1234", + }; + + var secondModel = new SecondModel + { + P4 = thirdModel, + }; + + var firstModel = new FirstModel + { + P1 = secondModel, + P2 = secondModel, + P3 = thirdModel, + }; + + var validator = new FirstValidator(); + var vr = validator.Validate("RepeatedTypes", firstModel); + + Utils.VerifyValidateOptionsResult(vr, 3, "P1.P4.P5", "P2.P4.P5", "P3.P5"); + } + + [Fact] + public void Valid() + { + var thirdModel = new ThirdModel + { + P5 = "12345", + }; + + var secondModel = new SecondModel + { + P4 = thirdModel, + }; + + var firstModel = new FirstModel + { + P1 = secondModel, + P2 = secondModel, + P3 = thirdModel, + }; + + var validator = new FirstValidator(); + Assert.Equal(ValidateOptionsResult.Success, validator.Validate("RepeatedTypes", firstModel)); + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/SelfValidationTests.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/SelfValidationTests.cs new file mode 100644 index 0000000000..0a511333f0 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/SelfValidationTests.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using SelfValidation; +using Xunit; + +namespace Microsoft.Gen.OptionsValidation.Test; + +public class SelfValidationTests +{ + [Fact] + public void Invalid() + { + var firstModel = new FirstModel + { + P1 = "1234", + }; + + var validator = default(FirstValidator); + var vr = validator.Validate("SelfValidation", firstModel); + + Utils.VerifyValidateOptionsResult(vr, 1, "P1"); + } + + [Fact] + public void Valid() + { + var firstModel = new FirstModel + { + P1 = "12345", + }; + + var validator = default(FirstValidator); + Assert.Equal(ValidateOptionsResult.Success, validator.Validate("SelfValidation", firstModel)); + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/TestResource.Designer.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/TestResource.Designer.cs new file mode 100644 index 0000000000..4fa3e97cbd --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/TestResource.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Gen.OptionsValidation.Test { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class TestResource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal TestResource() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Gen.OptionsValidation.Test.TestResource", typeof(TestResource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to ErrorMessageResourceName. + /// + internal static string ErrorMessageResourceName { + get { + return ResourceManager.GetString("ErrorMessageResourceName", resourceCulture); + } + } + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/TestResource.resx b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/TestResource.resx new file mode 100644 index 0000000000..70f767945b --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/TestResource.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ErrorMessageResourceName + + \ No newline at end of file diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/Utils.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/Utils.cs new file mode 100644 index 0000000000..7412374f18 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/Utils.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETCOREAPP3_1_OR_GREATER +using System.Linq; +#endif +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Gen.OptionsValidation.Test; + +internal static class Utils +{ + public static void VerifyValidateOptionsResult(ValidateOptionsResult vr, int expectedErrorCount, params string[] expectedErrorSubstrings) + { + Assert.NotNull(vr); + +#if NETCOREAPP3_1_OR_GREATER + var failures = vr.Failures!.ToArray(); +#else + var failures = vr.FailureMessage!.Split(';'); +#endif + + Assert.Equal(expectedErrorCount, failures.Length); + + for (int i = 0; i < expectedErrorSubstrings.Length; i++) + { + Assert.Contains(expectedErrorSubstrings[i], failures[i]); + } + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/ValueTypesTests.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/ValueTypesTests.cs new file mode 100644 index 0000000000..543e8eb882 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Common/ValueTypesTests.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using ValueTypes; +using Xunit; + +namespace Microsoft.Gen.OptionsValidation.Test; + +public class ValueTypesTests +{ + [Fact] + public void Invalid() + { + var secondModel = new SecondModel + { + P4 = "1234", + }; + + var firstModel = new FirstModel + { + P1 = "1234", + P3 = secondModel, + P2 = secondModel, + P4 = default, + }; + + var validator = default(FirstValidator); + var vr = validator.Validate("ValueTypes", firstModel); + + Utils.VerifyValidateOptionsResult(vr, 3, "P1", "P2.P4", "P3.P4"); + } + + [Fact] + public void Valid() + { + var secondModel = new SecondModel + { + P4 = "12345", + }; + + var firstModel = new FirstModel + { + P1 = "12345", + P3 = secondModel, + P2 = secondModel, + P4 = default, + }; + + var validator = default(FirstValidator); + Assert.Equal(ValidateOptionsResult.Success, validator.Validate("ValueTypes", firstModel)); + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Directory.Build.props b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Directory.Build.props new file mode 100644 index 0000000000..51d1626b34 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Directory.Build.props @@ -0,0 +1,33 @@ + + + + + Microsoft.Gen.OptionsValidation.Test + Tests for code generated by Gen.OptionsValidation. + + + + $(NetCoreTargetFrameworks) + $(NetCoreTargetFrameworks)$(ConditionalNet462) + true + true + true + true + $(NoWarn);CA1824 + + + + + + + + + + + + + + + + + diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Roslyn3.8/Microsoft.Gen.OptionsValidation.Roslyn3.8.Generated.Tests.csproj b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Roslyn3.8/Microsoft.Gen.OptionsValidation.Roslyn3.8.Generated.Tests.csproj new file mode 100644 index 0000000000..68a4e354e8 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Roslyn3.8/Microsoft.Gen.OptionsValidation.Roslyn3.8.Generated.Tests.csproj @@ -0,0 +1,13 @@ + + + 3.8 + + + + + True + True + TestResource.resx + + + diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Roslyn4.0/Microsoft.Gen.OptionsValidation.Roslyn4.0.Generated.Tests.csproj b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Roslyn4.0/Microsoft.Gen.OptionsValidation.Roslyn4.0.Generated.Tests.csproj new file mode 100644 index 0000000000..1dbe0b6f25 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Generated/Roslyn4.0/Microsoft.Gen.OptionsValidation.Roslyn4.0.Generated.Tests.csproj @@ -0,0 +1,14 @@ + + + 4.0 + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + + + + True + True + TestResource.resx + + + diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/CustomAttr.cs b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/CustomAttr.cs new file mode 100644 index 0000000000..4259af039b --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/CustomAttr.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace CustomAttr +{ +#pragma warning disable SA1649 +#pragma warning disable SA1402 +#pragma warning disable CA1019 +#pragma warning disable IDE0052 + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] + public sealed class CustomAttribute : ValidationAttribute + { + private readonly char _ch; + private readonly bool _caseSensitive; + private readonly string? _extra; + + public CustomAttribute(char ch, bool caseSensitive, string? extra) + { + _ch = ch; + _caseSensitive = caseSensitive; + _extra = extra; + } + + protected override ValidationResult IsValid(object? value, ValidationContext? validationContext) + { + if (value == null) + { + return ValidationResult.Success!; + } + + if (_caseSensitive) + { + if ((char)value != _ch) + { + return new ValidationResult($"{validationContext?.MemberName} didn't match"); + } + } + else + { + if (char.ToUpperInvariant((char)value) != char.ToUpperInvariant(_ch)) + { + return new ValidationResult($"{validationContext?.MemberName} didn't match"); + } + } + + return ValidationResult.Success!; + } + } + + public class FirstModel + { + [Custom('A', true, null)] + public char P1 { get; set; } + + [Custom('A', false, "X")] + public char P2 { get; set; } + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions + { + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Enumeration.cs b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Enumeration.cs new file mode 100644 index 0000000000..bf5d1f7c63 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Enumeration.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Enumeration +{ +#pragma warning disable SA1649 +#pragma warning disable SA1402 + + public class FirstModel + { + [ValidateEnumeratedItems] + public IList? P1; + + [ValidateEnumeratedItems(typeof(SecondValidator))] + public IList? P2; + + [ValidateEnumeratedItems] + public IList? P3; + + [ValidateEnumeratedItems] + public IList? P4; + + [ValidateEnumeratedItems] + public IList? P5; + + [ValidateEnumeratedItems] + [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1125:Use shorthand for nullable types", Justification = "Testing System>Nullable")] + public IList>? P51; + + [ValidateEnumeratedItems] + public SynteticEnumerable? P6; + + [ValidateEnumeratedItems] + public SynteticEnumerable P7; + + [ValidateEnumeratedItems] + [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1125:Use shorthand for nullable types", Justification = "Testing System>Nullable")] + public Nullable P8; + } + + public class SecondModel + { + [Required] + [MinLength(5)] + public string P6 = string.Empty; + } + + public struct ThirdModel + { + [Range(0, 10)] + public int Value; + } + + public struct SynteticEnumerable : IEnumerable + { + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public IEnumerator GetEnumerator() => new InternalEnumerator(); + + private class InternalEnumerator : IEnumerator + { + public SecondModel Current => throw new NotSupportedException(); + + object IEnumerator.Current => Current; + + public void Dispose() + { + // Nothing to dispose... + } + + public bool MoveNext() => false; + + public void Reset() => throw new NotSupportedException(); + } + } + + [OptionsValidator] + public partial struct FirstValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial struct SecondValidator : IValidateOptions + { + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Fields.cs b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Fields.cs new file mode 100644 index 0000000000..ddf54f49f4 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Fields.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Fields +{ +#pragma warning disable SA1649 +#pragma warning disable SA1402 +#pragma warning disable S1186 +#pragma warning disable CA1822 + + public class FirstModel + { + [Required] + [MinLength(5)] + public string P1 = string.Empty; + + [Microsoft.Extensions.Options.Validation.ValidateObjectMembers(typeof(SecondValidator))] + public SecondModel? P2; + + [Microsoft.Extensions.Options.Validation.ValidateObjectMembers] + public ThirdModel P3; + } + + public class SecondModel + { + [Required] + [MinLength(5)] + public string P4 = string.Empty; + } + + public struct ThirdModel + { + [Required] + [MinLength(5)] + public string P5 = string.Empty; + + public int P6 = default; + + public ThirdModel(object _) + { + } + } + + [OptionsValidator] + public partial struct FirstValidator : IValidateOptions + { + public void Validate() + { + } + + public void Validate(int _) + { + } + + public void Validate(string? _) + { + } + + public void Validate(string? _0, object _1) + { + } + } + + [OptionsValidator] + public partial struct SecondValidator : IValidateOptions + { + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/FileScopedNamespace.cs b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/FileScopedNamespace.cs new file mode 100644 index 0000000000..1341181d24 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/FileScopedNamespace.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace FileScopedNamespace; + +#pragma warning disable SA1649 // File name should match first type name + +public class FirstModel +{ + [Required] + [MinLength(5)] + public string P1 = string.Empty; +} + +[OptionsValidator] +public partial struct FirstValidator : IValidateOptions +{ +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/FunnyStrings.cs b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/FunnyStrings.cs new file mode 100644 index 0000000000..401109dd62 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/FunnyStrings.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace FunnyStrings +{ +#pragma warning disable SA1649 +#pragma warning disable SA1402 + + public class FirstModel + { + [RegularExpression("\"\r\n\\\\")] + public string P1 { get; set; } = string.Empty; + } + + [OptionsValidator] + public partial struct FirstValidator : IValidateOptions + { + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Generics.cs b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Generics.cs new file mode 100644 index 0000000000..bde80402a1 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Generics.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Generics +{ +#pragma warning disable SA1649 +#pragma warning disable SA1402 + + public class FirstModel + { + [Required] + [MinLength(5)] + public string P1 { get; set; } = string.Empty; + + public T? P2 { get; set; } + + [Microsoft.Extensions.Options.Validation.ValidateObjectMembers] + public SecondModel? P3 { get; set; } + } + + public class SecondModel + { + [Required] + [MinLength(5)] + public string P4 { get; set; } = string.Empty; + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions> + { + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Models.cs b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Models.cs new file mode 100644 index 0000000000..2234bd650d --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Models.cs @@ -0,0 +1,252 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Gen.OptionsValidation.Test; + +#pragma warning disable SA1649 +#pragma warning disable SA1402 + +namespace TestClasses.OptionsValidation +{ + // ValidationAttribute without parameter + public class RequiredAttributeModel + { + [Required] + public string? Val { get; set; } + } + + // ValidationAttribute with string parameter + public class RegularExpressionAttributeModel + { + [RegularExpression("\\s")] + public string Val { get; set; } = string.Empty; + } + + // DataTypeAttribute + public class EmailAttributeModel + { + [EmailAddress] + public string Val { get; set; } = string.Empty; + } + + // ValidationAttribute with System.Type parameter + public class CustomValidationAttributeModel + { + [CustomValidation(typeof(CustomValidationTest), "TestMethod")] + public string Val { get; set; } = string.Empty; + } + +#pragma warning disable SA1204 // Static elements should appear before instance elements + public static class CustomValidationTest +#pragma warning restore SA1204 // Static elements should appear before instance elements + { + public static ValidationResult? TestMethod(string val, ValidationContext _) + { + if (val.Equals("Pass", StringComparison.Ordinal)) + { + return ValidationResult.Success; + } + + throw new ValidationException(); + } + } + + // ValidationAttribute with DataType parameter + public class DataTypeAttributeModel + { + [DataType(DataType.Text)] + public string Val { get; set; } = string.Empty; + } + + // ValidationAttribute with type, double, int parameters + public class RangeAttributeModelInt + { + [Range(1, 3)] + public int Val { get; set; } + } + + public class RangeAttributeModelDouble + { + [Range(0.5, 0.9)] + public double Val { get; set; } + } + + public class RangeAttributeModelDate + { +#if NETCOREAPP3_1_OR_GREATER + [Range(typeof(DateTime), "1/2/2004", "3/4/2004", ParseLimitsInInvariantCulture = true)] +#else + [Range(typeof(DateTime), "1/2/2004", "3/4/2004")] +#endif + public DateTime Val { get; set; } + } + + public class MultipleAttributeModel + { + [Required] + [DataType(DataType.Password)] + public string Val1 { get; set; } = string.Empty; + + [Range(1, 3)] + public int Val2 { get; set; } + + [Range(3, 5)] + public int Val3 { get; set; } + + [Range(5, 9)] + public int Val4 { get; set; } + } + + public class CustomTypeCustomValidationAttributeModel + { + [CustomValidation(typeof(CustomTypeCustomValidationTest), "TestMethod")] + public CustomType? Val { get; set; } + } + + public class CustomType + { + public string Val1 { get; set; } = string.Empty; + public string Val2 { get; set; } = string.Empty; + } + +#pragma warning disable SA1204 // Static elements should appear before instance elements + public static class CustomTypeCustomValidationTest +#pragma warning restore SA1204 // Static elements should appear before instance elements + { + public static ValidationResult? TestMethod(CustomType val, ValidationContext _) + { + if (val.Val1.Equals("Pass", StringComparison.Ordinal) && val.Val2.Equals("Pass", StringComparison.Ordinal)) + { + return ValidationResult.Success; + } + + throw new ValidationException(); + } + } + + public class AttributePropertyModel + { + [Range(1, 3, ErrorMessage = "ErrorMessage")] + public int Val1 { get; set; } + + [Range(1, 3, ErrorMessageResourceType = typeof(TestResource), ErrorMessageResourceName = "ErrorMessageResourceName")] + public int Val2 { get; set; } + } + + public class TypeWithoutOptionsValidator + { + [Required] + public string? Val1 { get; set; } + + [Range(typeof(DateTime), "1/2/2004", "3/4/2004")] + public DateTime Val2 { get; set; } + + [Microsoft.Extensions.Options.Validation.ValidateObjectMembers] + public RangeAttributeModelDouble? YetAnotherComplexVal { get; set; } + } + + public class DerivedModel : RequiredAttributeModel + { + [Required] + public string? DerivedVal { get; set; } + + [Required] + internal virtual int? VirtualValWithAttr { get; set; } + + public virtual int? VirtualValWithoutAttr { get; set; } + + [Required] + public new int? Val { get; set; } + } + + public class LeafModel : DerivedModel + { + internal override int? VirtualValWithAttr { get; set; } + + [Required] + public override int? VirtualValWithoutAttr { get; set; } + } + + public class ComplexModel + { + [Microsoft.Extensions.Options.Validation.ValidateObjectMembers] + public RequiredAttributeModel? ComplexVal { get; set; } + + [Microsoft.Extensions.Options.Validation.ValidateObjectMembers] + public TypeWithoutOptionsValidator? ValWithoutOptionsValidator { get; set; } + } + + [OptionsValidator] + public partial class RequiredAttributeModelValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial class RegularExpressionAttributeModelValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial class EmailAttributeModelValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial class CustomValidationAttributeModelValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial class DataTypeAttributeModelValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial class RangeAttributeModelIntValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial class RangeAttributeModelDoubleValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial class RangeAttributeModelDateValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial class MultipleAttributeModelValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial class CustomTypeCustomValidationAttributeModelValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial class AttributePropertyModelValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial class DerivedModelValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial class LeafModelValidator : IValidateOptions + { + } + + [OptionsValidator] + internal sealed partial class ComplexModelValidator : IValidateOptions + { + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/MultiModelValidator.cs b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/MultiModelValidator.cs new file mode 100644 index 0000000000..fd725d8403 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/MultiModelValidator.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace MultiModelValidator +{ +#pragma warning disable SA1649 +#pragma warning disable SA1402 + + public class FirstModel + { + [Required] + [MinLength(5)] + public string P1 = string.Empty; + + [Microsoft.Extensions.Options.Validation.ValidateObjectMembers(typeof(MultiValidator))] + public SecondModel? P2; + } + + public class SecondModel + { + [Required] + [MinLength(5)] + public string P3 = string.Empty; + } + + [OptionsValidator] + public partial struct MultiValidator : IValidateOptions, IValidateOptions + { + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Nested.cs b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Nested.cs new file mode 100644 index 0000000000..531e0eed0d --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/Nested.cs @@ -0,0 +1,106 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if ROSLYN_4_0_OR_GREATER + +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace Nested +{ +#pragma warning disable SA1649 +#pragma warning disable SA1402 + + public static class Container1 + { + public class FirstModel + { + [Required] + [MinLength(5)] + public string P1 { get; set; } = string.Empty; + + [Microsoft.Extensions.Options.Validation.ValidateObjectMembers(typeof(Container2.Container3.SecondValidator))] + public SecondModel? P2 { get; set; } + + [Microsoft.Extensions.Options.Validation.ValidateObjectMembers] + public ThirdModel P3 { get; set; } + + [Microsoft.Extensions.Options.Validation.ValidateObjectMembers(typeof(Container4.Container5.ThirdValidator))] + public SecondModel? P4 { get; set; } + } + + public class SecondModel + { + [Required] + [MinLength(5)] + public string P5 { get; set; } = string.Empty; + } + + public struct ThirdModel + { + public ThirdModel(int _) + { + } + + [Required] + [MinLength(5)] + public string P6 { get; set; } = string.Empty; + } + } + + public static partial class Container2 + { + public partial class Container3 + { + public Container3(int _) + { + // nothing to do + } + + [OptionsValidator] + public partial struct FirstValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial struct SecondValidator : IValidateOptions + { + } + } + } + + public partial record class Container4 + { + public partial record class Container5 + { + public Container5(int _) + { + // nothing to do + } + + [OptionsValidator] + public partial struct ThirdValidator : IValidateOptions + { + } + } + } + + public partial struct Container6 + { + [OptionsValidator] + public partial struct FourthValidator : IValidateOptions + { + } + } + + public partial record struct Container7 + { + [OptionsValidator] + public partial record struct FifthValidator : IValidateOptions + { + } + } +} + +#endif diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/NoNamespace.cs b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/NoNamespace.cs new file mode 100644 index 0000000000..db6461ddf1 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/NoNamespace.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +#pragma warning disable SA1649 +#pragma warning disable SA1402 + +public class FirstModelNoNamespace +{ + [Required] + [MinLength(5)] + public string P1 { get; set; } = string.Empty; + + [Microsoft.Extensions.Options.Validation.ValidateObjectMembers(typeof(SecondValidatorNoNamespace))] + public SecondModelNoNamespace? P2 { get; set; } + + [Microsoft.Extensions.Options.Validation.ValidateObjectMembers] + public ThirdModelNoNamespace? P3 { get; set; } +} + +public class SecondModelNoNamespace +{ + [Required] + [MinLength(5)] + public string P4 { get; set; } = string.Empty; +} + +public class ThirdModelNoNamespace +{ + [Required] + [MinLength(5)] + public string P5 { get; set; } = string.Empty; +} + +[OptionsValidator] +public partial class FirstValidatorNoNamespace : IValidateOptions +{ +} + +[OptionsValidator] +public partial class SecondValidatorNoNamespace : IValidateOptions +{ +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/RandomMembers.cs b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/RandomMembers.cs new file mode 100644 index 0000000000..08a3327a87 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/RandomMembers.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace RandomMembers +{ +#pragma warning disable SA1649 +#pragma warning disable SA1402 +#pragma warning disable CA1822 + + public class FirstModel + { + [Required] + [MinLength(5)] + public string? P1 { get; set; } + + public void Foo() + { + throw new NotSupportedException(); + } + + public class Nested + { + } + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions + { + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/RecordTypes.cs b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/RecordTypes.cs new file mode 100644 index 0000000000..b0b393f737 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/RecordTypes.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if ROSLYN_4_0_OR_GREATER + +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace RecordTypes +{ +#pragma warning disable SA1649 + + public record class FirstModel + { + [Required] + [MinLength(5)] + public string P1 { get; set; } = string.Empty; + + [Microsoft.Extensions.Options.Validation.ValidateObjectMembers(typeof(SecondValidator))] + public SecondModel? P2 { get; set; } + + [Microsoft.Extensions.Options.Validation.ValidateObjectMembers(typeof(ThirdValidator))] + public SecondModel P3 { get; set; } = new SecondModel(); + + [Microsoft.Extensions.Options.Validation.ValidateObjectMembers] + public ThirdModel P4 { get; set; } + } + + public record class SecondModel + { + [Required] + [MinLength(5)] + public string P5 { get; set; } = string.Empty; + } + + public record struct ThirdModel + { + [Required] + [MinLength(5)] + public string P6 { get; set; } = string.Empty; + + public ThirdModel(int _) + { + } + + public ThirdModel(object _) + { + } + } + + [OptionsValidator] + public partial record struct FirstValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial record struct SecondValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial record class ThirdValidator : IValidateOptions + { + } +} + +#endif diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/RepeatedTypes.cs b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/RepeatedTypes.cs new file mode 100644 index 0000000000..f5fa327bae --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/RepeatedTypes.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace RepeatedTypes +{ +#pragma warning disable SA1649 +#pragma warning disable SA1402 +#pragma warning disable CA1019 + + public class FirstModel + { + [Required] + [Microsoft.Extensions.Options.Validation.ValidateObjectMembers] + public SecondModel? P1 { get; set; } + + [Required] + [Microsoft.Extensions.Options.Validation.ValidateObjectMembers] + public SecondModel? P2 { get; set; } + + [Required] + [Microsoft.Extensions.Options.Validation.ValidateObjectMembers] + public ThirdModel? P3 { get; set; } + } + + public class SecondModel + { + [Required] + [Microsoft.Extensions.Options.Validation.ValidateObjectMembers] + public ThirdModel? P4 { get; set; } + } + + public class ThirdModel + { + [Required] + [MinLength(5)] + public string? P5; + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions + { + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/SelfValidation.cs b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/SelfValidation.cs new file mode 100644 index 0000000000..673af4a089 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/SelfValidation.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace SelfValidation +{ +#pragma warning disable SA1649 + + public class FirstModel : IValidatableObject + { + [Required] + public string P1 = string.Empty; + + public IEnumerable Validate(ValidationContext validationContext) + { + if (P1.Length < 5) + { + return new[] { new ValidationResult("P1 is not long enough") }; + } + + return Array.Empty(); + } + } + + [OptionsValidator] + public partial struct FirstValidator : IValidateOptions + { + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/ValueTypes.cs b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/ValueTypes.cs new file mode 100644 index 0000000000..aa6a8c3f97 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/TestClasses/ValueTypes.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; + +namespace ValueTypes +{ +#pragma warning disable SA1649 + + public class FirstModel + { + [Required] + [MinLength(5)] + public string P1 { get; set; } = string.Empty; + + [ValidateObjectMembers] + public SecondModel? P2 { get; set; } + + [ValidateObjectMembers] + public SecondModel P3 { get; set; } + + [ValidateObjectMembers] + [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1125:Use shorthand for nullable types", Justification = "Testing System>Nullable")] + public Nullable P4 { get; set; } + } + + public struct SecondModel + { + [Required] + [MinLength(5)] + public string P4 { get; set; } = string.Empty; + + public SecondModel(object _) + { + } + } + + [OptionsValidator] + public partial struct FirstValidator : IValidateOptions + { + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Common/EmitterTests.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Common/EmitterTests.cs new file mode 100644 index 0000000000..56c0abdbfc --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Common/EmitterTests.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Gen.Shared; +using Microsoft.Shared.Data.Validation; +using Xunit; + +namespace Microsoft.Gen.OptionsValidation.Test; + +public class EmitterTests +{ + [Fact] + public async Task TestEmitter() + { + var sources = new List(); + foreach (var file in Directory.GetFiles("TestClasses")) + { +#if !ROSLYN_4_0_OR_GREATER + if (file.EndsWith("Nested.cs") || file.EndsWith("RecordTypes.cs")) + { + continue; + } +#endif + +#if NETCOREAPP3_1_OR_GREATER + sources.Add("#define NETCOREAPP3_1_OR_GREATER\n" + File.ReadAllText(file)); +#else + sources.Add(File.ReadAllText(file)); +#endif + } + + var (d, r) = await RoslynTestUtils.RunGenerator( + new Generator(), + new[] + { + Assembly.GetAssembly(typeof(RequiredAttribute))!, + Assembly.GetAssembly(typeof(TimeSpanAttribute))!, + Assembly.GetAssembly(typeof(OptionsValidatorAttribute))!, + Assembly.GetAssembly(typeof(IValidateOptions))!, + }, + sources) + .ConfigureAwait(false); + + Assert.Empty(d); + _ = Assert.Single(r); + + var golden = File.ReadAllText($"GoldenFiles/Microsoft.Gen.OptionsValidation/Microsoft.Gen.OptionsValidation.Generator/Validators.g.cs"); + var result = r[0].SourceText.ToString(); + Assert.Equal(golden, result); + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Common/ParserTests.Enumeration.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Common/ParserTests.Enumeration.cs new file mode 100644 index 0000000000..4e99becec9 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Common/ParserTests.Enumeration.cs @@ -0,0 +1,212 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Gen.OptionsValidation.Test; + +public partial class ParserTests +{ + [Fact] + public async Task CircularTypeReferencesInEnumeration() + { + var (d, _) = await RunGenerator(@" + public class FirstModel + { + [Required] + [ValidateEnumeratedItems] + public FirstModel[]? P1 { get; set; } + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions + { + } + "); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.CircularTypeReferences.Id, d[0].Id); + } + + [Fact] + public async Task NotValidatorInEnumeration() + { + var (d, _) = await RunGenerator(@" + public class FirstModel + { + [ValidateEnumeratedItems(typeof(SecondValidator)] + public SecondModel[]? P1; + } + + public class SecondModel + { + [Required] + public string? P2; + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions + { + } + + public partial class SecondValidator + { + } + "); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.DoesntImplementIValidateOptions.Id, d[0].Id); + } + + [Fact] + public async Task NullValidatorInEnumeration() + { + var (d, _) = await RunGenerator(@" + public class FirstModel + { + [ValidateEnumeratedItems(null!)] + public SecondModel[]? P1; + } + + public class SecondModel + { + [Required] + public string? P2; + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial class SecondValidator : IValidateOptions + { + } + "); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.NullValidatorType.Id, d[0].Id); + } + + [Fact] + public async Task NoSimpleValidatorConstructorInEnumeration() + { + var (d, _) = await RunGenerator(@" + public class FirstModel + { + [Required] + public string? P1; + + [ValidateEnumeratedItems(typeof(SecondValidator)] + public SecondModel[]? P2; + } + + public class SecondModel + { + [Required] + public string? P3; + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial class SecondValidator : IValidateOptions + { + public SecondValidator(int _) + { + } + } + "); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ValidatorsNeedSimpleConstructor.Id, d[0].Id); + } + + [Fact] + public async Task CantValidateOpenGenericMembersInEnumeration() + { + var (d, _) = await RunGenerator(@" + public class FirstModel + { + [Required] + [ValidateEnumeratedItems] + public T[]? P1; + + [ValidateEnumeratedItems] + [Required] + public T[]? P2; + + [ValidateEnumeratedItems] + [Required] + public System.Collections.Generic.IList P3 = null!; + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions> + { + } + "); + + Assert.Equal(3, d.Count); + Assert.Equal(DiagDescriptors.CantUseWithGenericTypes.Id, d[0].Id); + Assert.Equal(DiagDescriptors.CantUseWithGenericTypes.Id, d[1].Id); + Assert.Equal(DiagDescriptors.CantUseWithGenericTypes.Id, d[2].Id); + } + + [Fact] + public async Task ClosedGenericsInEnumeration() + { + var (d, _) = await RunGenerator(@" + public class FirstModel + { + [ValidateEnumeratedItems] + [Required] + public T[]? P1; + + [ValidateEnumeratedItems] + [Required] + public int[]? P2; + + [ValidateEnumeratedItems] + [Required] + public System.Collections.Generic.IList? P3; + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions> + { + } + "); + + Assert.Equal(3, d.Count); + Assert.Equal(DiagDescriptors.NoEligibleMember.Id, d[0].Id); + Assert.Equal(DiagDescriptors.NoEligibleMember.Id, d[1].Id); + Assert.Equal(DiagDescriptors.NoEligibleMember.Id, d[2].Id); + } + + [Fact] + public async Task NotEnumerable() + { + var (d, _) = await RunGenerator(@" + public class FirstModel + { + [Required] + [ValidateEnumeratedItems] + public int P1; + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions + { + } + "); + + Assert.Equal(1, d.Count); + Assert.Equal(DiagDescriptors.NotEnumerableType.Id, d[0].Id); + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Common/ParserTests.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Common/ParserTests.cs new file mode 100644 index 0000000000..8dbdfa237e --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Common/ParserTests.cs @@ -0,0 +1,930 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation; +using Microsoft.Gen.Shared; +using Xunit; + +namespace Microsoft.Gen.OptionsValidation.Test; + +public partial class ParserTests +{ + [Fact] + public async Task PotentiallyMissingAttributes() + { + var (d, _) = await RunGenerator(@" + public class FirstModel + { + [Required] + public SecondModel? P1 { get; set; } + + [Required] + public System.Collections.Generic.IList? P2 { get; set; } + } + + public class SecondModel + { + [Required] + public string? P3; + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions + { + } + "); + + Assert.Equal(2, d.Count); + Assert.Equal(DiagDescriptors.PotentiallyMissingTransitiveValidation.Id, d[0].Id); + Assert.Equal(DiagDescriptors.PotentiallyMissingEnumerableValidation.Id, d[1].Id); + } + + [Fact] + public async Task CircularTypeReferences() + { + var (d, _) = await RunGenerator(@" + public class FirstModel + { + [Required] + [ValidateObjectMembers] + public FirstModel? P1 { get; set; } + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions + { + } + "); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.CircularTypeReferences.Id, d[0].Id); + } + + [Fact] + public async Task InvalidValidatorInterface() + { + var (d, _) = await RunGenerator(@" + public class FirstModel + { + [Required] + public string? P1; + } + + public class SecondModel + { + [Required] + public string? P2; + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial class SecondValidator + { + } + "); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.DoesntImplementIValidateOptions.Id, d[0].Id); + } + + [Fact] + public async Task NotValidator() + { + var (d, _) = await RunGenerator(@" + public class FirstModel + { + [ValidateObjectMembers(typeof(SecondValidator)] + public SecondModel? P1; + } + + public class SecondModel + { + [Required] + public string? P2; + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions + { + } + + public partial class SecondValidator + { + } + "); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.DoesntImplementIValidateOptions.Id, d[0].Id); + } + + [Fact] + public async Task ValidatorAlreadyImplementValidateFunction() + { + var (d, _) = await RunGenerator(@" + public class FirstModel + { + [Required] + public string? P1; + + [ValidateObjectMembers(typeof(SecondValidator)] + public SecondModel? P2; + } + + public class SecondModel + { + [Required] + public string? P3; + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial class SecondValidator : IValidateOptions + { + public ValidateOptionsResult Validate(string name, SecondModel options) + { + throw new System.NotSupportedException(); + } + } + "); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.AlreadyImplementsValidateMethod.Id, d[0].Id); + } + + [Fact] + public async Task NullValidator() + { + var (d, _) = await RunGenerator(@" + public class FirstModel + { + [ValidateObjectMembers(null!)] + public SecondModel? P1; + } + + public class SecondModel + { + [Required] + public string? P2; + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial class SecondValidator : IValidateOptions + { + } + "); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.NullValidatorType.Id, d[0].Id); + } + + [Fact] + public async Task NoSimpleValidatorConstructor() + { + var (d, _) = await RunGenerator(@" + public class FirstModel + { + [Required] + public string? P1; + + [ValidateObjectMembers(typeof(SecondValidator)] + public SecondModel? P2; + } + + public class SecondModel + { + [Required] + public string? P3; + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial class SecondValidator : IValidateOptions + { + public SecondValidator(int _) + { + } + } + "); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ValidatorsNeedSimpleConstructor.Id, d[0].Id); + } + + [Fact] + public async Task NoStaticValidator() + { + var (d, _) = await RunGenerator(@" + public class FirstModel + { + [Required] + public string P1; + } + + [OptionsValidator] + public static partial class FirstValidator : IValidateOptions + { + } + "); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.CantBeStaticClass.Id, d[0].Id); + } + + [Fact] + public async Task BogusModelType() + { + var (d, _) = await RunGenerator(@" + [OptionsValidator] + public partial class FirstValidator : IValidateOptions + { + } + "); + + // the generator doesn't produce any errors here, since the C# compiler will take care of it + Assert.Empty(d); + } + + [Fact] + public async Task CantValidateOpenGenericMembers() + { + var (d, _) = await RunGenerator(@" + public class FirstModel + { + [Required] + [ValidateObjectMembers] + public T? P1; + + [ValidateObjectMembers] + [Required] + public T[]? P2; + + [ValidateObjectMembers] + [Required] + public System.Collections.Generics.IList P3 = null!; + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions> + { + } + "); + + Assert.Equal(3, d.Count); + Assert.Equal(DiagDescriptors.CantUseWithGenericTypes.Id, d[0].Id); + Assert.Equal(DiagDescriptors.CantUseWithGenericTypes.Id, d[1].Id); + Assert.Equal(DiagDescriptors.CantUseWithGenericTypes.Id, d[2].Id); + } + + [Fact] + public async Task ClosedGenerics() + { + var (d, _) = await RunGenerator(@" + public class FirstModel + { + [Required] + [ValidateObjectMembers] + public T? P1; + + [ValidateObjectMembers] + [Required] + public T[]? P2; + + [ValidateObjectMembers] + [Required] + public int[]? P3; + + [ValidateObjectMembers] + [Required] + public System.Collections.Generics.IList? P4; + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions> + { + } + "); + + Assert.Equal(4, d.Count); + Assert.Equal(DiagDescriptors.NoEligibleMember.Id, d[0].Id); + Assert.Equal(DiagDescriptors.NoEligibleMember.Id, d[1].Id); + Assert.Equal(DiagDescriptors.NoEligibleMember.Id, d[2].Id); + Assert.Equal(DiagDescriptors.NoEligibleMember.Id, d[3].Id); + } + + [Fact] + public async Task NoEligibleMembers() + { + var (d, _) = await RunGenerator(@" + public class FirstModel + { + [Required] + [ValidateObjectMembers] + public SecondModel? P1; + } + + public class SecondModel + { + public string P2; + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions + { + } + + [OptionsValidator] + public partial class SecondValidator : IValidateOptions + { + } + "); + + Assert.Equal(2, d.Count); + Assert.Equal(DiagDescriptors.NoEligibleMember.Id, d[0].Id); + Assert.Equal(DiagDescriptors.NoEligibleMembersFromValidator.Id, d[1].Id); + } + + [Fact] + public async Task AlreadyImplemented() + { + var (d, _) = await RunGenerator(@" + public class FirstModel + { + [Required] + public string One { get; set; } = string.Empty; + } + + [OptionsValidator] + public partial class FirstValidator : IValidateOptions + { + public void Validate(string name, FirstModel fm) + { + } + } + "); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.AlreadyImplementsValidateMethod.Id, d[0].Id); + } + + [Fact] + public async Task ShouldNotProduceInfoWhenTheClassHasABaseClass() + { + var (d, _) = await RunGenerator(@" + public class Parent + { + [Required] + public string parentString { get; set; } + } + + public class Child : Parent + { + [Required] + public string childString { get; set; } + } + + [OptionsValidator] + public partial class Validator : IValidateOptions + { + } + "); + + Assert.Empty(d); + } + + [Fact] + public async Task ShouldNotProduceInfoWhenTransitiveClassHasABaseClass() + { + var (d, _) = await RunGenerator(@" + public class Parent + { + [Required] + public string parentString { get; set; } + } + + public class Child : Parent + { + [Required] + public string childString { get; set; } + } + + public class MyOptions + { + [ValidateObjectMembers] + public Child childVal { get; set; } + } + + [OptionsValidator] + public partial class Validator : IValidateOptions + { + } + "); + + Assert.Empty(d); + } + + [Theory] + [InlineData("bool")] + [InlineData("int")] + [InlineData("double")] + [InlineData("string")] + [InlineData("System.String")] + [InlineData("System.DateTime")] + public async Task ShouldProduceWarn_WhenTransitiveAttrMisused(string memberClass) + { + var (d, _) = await RunGenerator(@$" + public class InnerModel + {{ + [Required] + public string childString {{ get; set; }} + }} + + public class MyOptions + {{ + [Required] + public string simpleVal {{ get; set; }} + + [ValidateObjectMembers] + public {memberClass} complexVal {{ get; set; }} + }} + + [OptionsValidator] + public partial class Validator : IValidateOptions + {{ + }} + "); + + Assert.Single(d); + Assert.Equal(DiagDescriptors.NoEligibleMember.Id, d[0].Id); + } + + [Fact] + public async Task ShouldProduceWarningWhenTheClassHasNoEligibleMembers() + { + var (d, _) = await RunGenerator(@" + public class Child + { + private string AccountName { get; set; } + public object Weight; + } + + [OptionsValidator] + public partial class Validator : IValidateOptions + { + } + "); + + Assert.Single(d); + Assert.Equal(DiagDescriptors.NoEligibleMembersFromValidator.Id, d[0].Id); + } + + [Theory] + [InlineData("private")] + [InlineData("protected")] + public async Task ShouldProduceWarningWhenTheClassMembersAreInaccessible(string accessModifier) + { + var (d, _) = await RunGenerator($@" + public class Model + {{ + [Required] + public string? PublicVal {{ get; set; }} + + [Required] + {accessModifier} string? Val {{ get; set; }} + }} + + [OptionsValidator] + public partial class Validator : IValidateOptions + {{ + }} + "); + + Assert.Single(d); + Assert.Equal("R9G106", d[0].Id); + } + + [Fact] + public async Task ShouldNotProduceErrorWhenMultipleValidationAnnotationsExist() + { + var (d, _) = await RunGenerator(@" + public class IValidateOptionsTestFile + { + [MinLength(5)] + [MaxLength(15)] + public string Val9 { get; set; } + } + + [OptionsValidator] + public partial class Validator : IValidateOptions + { + } + "); + + Assert.Empty(d); + } + + [Fact] + public async Task ShouldNotProduceErrorWhenDataTypeAttributesAreUsed() + { + var (d, _) = await RunGenerator(@" + public class IValidateOptionsTestFile + { + [CreditCard] + public string Val3 = """"; + + [EmailAddress] + public string Val6 { get; set; } + + [EnumDataType(typeof(string))] + public string Val7 { get; set; } + + [FileExtensions] + public string Val8 { get; set; } + + [Phone] + public string Val10 { get; set; } + + [Url] + public string Val11 { get; set; } + } + + [OptionsValidator] + public partial class Validator : IValidateOptions + { + } + "); + + Assert.Empty(d); + } + + [Fact] + public async Task ShouldNotProduceErrorWhenConstVariableIsUsedAsAttributeArgument() + { + var (d, _) = await RunGenerator(@" + public class IValidateOptionsTestFile + { + private const int q = 5; + [Range(q, 10)] + public string Val11 { get; set; } + } + + [OptionsValidator] + public partial class Validator : IValidateOptions + { + } + "); + + Assert.Empty(d); + } + + // Testing on all existing & eligible annotations extending ValidationAttribute that aren't used above + [Fact] + public async Task ShouldNotProduceAnyMessagesWhenExistingValidationsArePlaced() + { + var (d, _) = await RunGenerator(@" + public class IValidateOptionsTestFile + { + [Required] + public string Val { get; set; } + + [Compare(""val"")] + public string Val2 { get; set; } + + [DataType(DataType.Password)] + public string _val5 = """"; + + [Range(5.1, 10.11)] + public string Val12 { get; set; } + + [Range(typeof(MemberDeclarationSyntax), ""1/2/2004"", ""3/4/2004"")] + public string Val14 { get; set; } + + [RegularExpression("""")] + public string Val15 { get; set; } + + [StringLength(5)] + public string Val16 { get; set; } + + [CustomValidation(typeof(MemberDeclarationSyntax), ""CustomMethod"")] + public string Val17 { get; set; } + } + + [OptionsValidator] + public partial class Validator : IValidateOptions + { + } + "); + + Assert.Empty(d); + } + + [Fact] + public async Task ShouldNotProduceErrorWhenPropertiesAreUsedAsAttributeArgument() + { + var (d, _) = await RunGenerator(@" + public class IValidateOptionsTestFile + { + private const int q = 5; + [Range(q, 10, ErrorMessage = ""ErrorMessage"")] + public string Val11 { get; set; } + } + + [OptionsValidator] + public partial class Validator : IValidateOptions + { + } + "); + + Assert.Empty(d); + } + + [Fact] + public async Task ShouldSkipWhenOptionsValidatorAttributeDoesNotExist() + { + var (d, _) = await RunGenerator(@" + public class IValidateOptionsTestFile + { + private const int q = 5; + [Range(q, 10, ErrorMessage = ""ErrorMessage"")] + public string Val11 { get; set; } + } + + [OptionsValidator] + public partial class Validator : IValidateOptions + { + } + ", includeR9References: false); + + Assert.Empty(d); + } + + [Fact] + public async Task ShouldSkipAtrributeWhenAttributeSymbolCannotBeFound() + { + var (d, _) = await RunGenerator(@" + public class IValidateOptionsTestFile + { + [RandomTest] + public string Val11 { get; set; } + + [Range(1, 10, ErrorMessage = ""ErrorMessage"")] + public string Val12 { get; set; } + } + + [OptionsValidator] + public partial class Validator : IValidateOptions + { + } + "); + + Assert.Empty(d); + } + + [Fact] + public async Task ShouldSkipAtrributeWhenAttributeSymbolIsNotBasedOnValidationAttribute() + { + var (d, _) = await RunGenerator(@" + public class IValidateOptionsTestFile + { + [FilterUIHint(""MultiForeignKey"")] + public string Val11 { get; set; } + + [Range(1, 10, ErrorMessage = ""ErrorMessage"")] + public string Val12 { get; set; } + } + + [OptionsValidator] + public partial class Validator : IValidateOptions + { + } + "); + + Assert.Empty(d); + } + + [Fact] + public async Task ShouldAcceptAtrributeWhenAttributeIsInDifferentNamespace() + { + var (d, _) = await RunGenerator(@" + namespace Test { + public class IValidateOptionsTestFile + { + [Test] + public string Val11 { get; set; } + } + + [AttributeUsage(AttributeTargets.Class)] + public sealed class TestAttribute : ValidationAttribute + { + } + + [OptionsValidator] + public partial class Validator : IValidateOptions + { + } + } + ", inNamespace: false); + + Assert.Empty(d); + } + + [Fact] + public async Task ShouldHandleAtrributePropertiesOtherThanString() + { + var (d, _) = await RunGenerator(@" + namespace Test { + public class IValidateOptionsTestFile + { + [Test(num = 5)] + public string Val11 { get; set; } + + [Required] + public string Val12 { get; set; } + } + + [OptionsValidator] + public partial class Validator : IValidateOptions + { + } + } + + namespace System.ComponentModel.DataAnnotations { + [AttributeUsage(AttributeTargets.Class)] + public sealed class TestAttribute : ValidationAttribute + { + public int num { get; set; } + public TestAttribute() { + } + } + } + ", inNamespace: false); + + Assert.Empty(d); + } + + [Fact] + public async Task ShouldStoreFloatValuesCorrectly() + { + var backupCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = new CultureInfo("ru-RU", false); + try + { + var (diagMessages, generatedResults) = await RunGenerator(@" + public class Model + { + [Range(-0.1, 1.3)] + public string Val { get; set; } + } + + [OptionsValidator] + public partial class Validator : IValidateOptions + { + } + "); + + Assert.Empty(diagMessages); + Assert.Single(generatedResults); + Assert.DoesNotContain("0,1", generatedResults[0].SourceText.ToString()); + Assert.DoesNotContain("1,3", generatedResults[0].SourceText.ToString()); + } + finally + { + CultureInfo.CurrentCulture = backupCulture; + } + } + + [Fact] + public async Task MultiModelValidatorGeneratesOnlyOnePartialTypeBlock() + { + var (d, sources) = await RunGenerator(@" + public class FirstModel + { + [Required] + public string P1 { get; set; } + } + + public class SecondModel + { + [Required] + public string P2 { get; set; } + } + + public class ThirdModel + { + [Required] + public string P3 { get; set; } + } + + [OptionsValidator] + public partial class MultiValidator : IValidateOptions, IValidateOptions, IValidateOptions + { + } + "); + + var typeDeclarations = sources[0].SyntaxTree + .GetRoot() + .DescendantNodes() + .OfType() + .ToArray(); + + var multiValidatorTypeDeclarations = typeDeclarations + .Where(x => x.Identifier.ValueText == "MultiValidator") + .ToArray(); + + Assert.Single(multiValidatorTypeDeclarations); + + var validateMethodDeclarations = multiValidatorTypeDeclarations[0] + .DescendantNodes() + .OfType() + .Where(x => x.Identifier.ValueText == "Validate") + .ToArray(); + + Assert.Equal(3, validateMethodDeclarations.Length); + } + + private static async Task<(IReadOnlyList diagnostics, ImmutableArray generatedSources)> RunGenerator( + string code, + bool wrap = true, + bool inNamespace = true, + bool includeR9References = true, + bool includeSystemReferences = true, + bool includeOptionsReferences = true, + bool includeTransitiveReferences = true) + { + var text = code; + if (wrap) + { + var nspaceStart = "namespace Test {"; + var nspaceEnd = "}"; + if (!inNamespace) + { + nspaceStart = ""; + nspaceEnd = ""; + } + + text = $@" + {nspaceStart} + using System.ComponentModel.DataAnnotations; + using Microsoft.Extensions.Options.Validation; + using Microsoft.Shared.Data.Validation; + using Microsoft.Extensions.Options; + using Microsoft.CodeAnalysis.CSharp.Syntax; + {code} + {nspaceEnd} + "; + } + + var assemblies = new List { Assembly.GetAssembly(typeof(MemberDeclarationSyntax))! }; + + if (includeR9References) + { + assemblies.Add(Assembly.GetAssembly(typeof(OptionsValidatorAttribute))!); + } + + if (includeSystemReferences) + { + assemblies.Add(Assembly.GetAssembly(typeof(RequiredAttribute))!); + } + + if (includeOptionsReferences) + { + assemblies.Add(Assembly.GetAssembly(typeof(IValidateOptions))!); + } + + if (includeTransitiveReferences) + { + assemblies.Add(Assembly.GetAssembly(typeof(Microsoft.Extensions.Options.Validation.ValidateObjectMembersAttribute))!); + } + + var result = await RoslynTestUtils.RunGenerator(new Generator(), assemblies.ToArray(), new[] { text }) + .ConfigureAwait(false); + + return result; + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Common/SymbolLoaderTests.cs b/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Common/SymbolLoaderTests.cs new file mode 100644 index 0000000000..287072ea95 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Common/SymbolLoaderTests.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Moq; +using Moq.Protected; +using Xunit; + +namespace Microsoft.Gen.OptionsValidation.Test; + +public class SymbolLoaderTests +{ + [Theory] + [InlineData(SymbolLoader.OptionsValidatorAttribute)] + [InlineData(SymbolLoader.ValidationAttribute)] + [InlineData(SymbolLoader.DataTypeAttribute)] + [InlineData(SymbolLoader.IValidatableObjectType)] + [InlineData(SymbolLoader.IValidateOptionsType)] + [InlineData(SymbolLoader.TypeOfType)] + public void Loader_ReturnsFalse_WhenRequiredTypeIsUnavailable(string type) + { + var compilationMock = new Mock( + string.Empty, + Array.Empty().ToImmutableArray(), + new Dictionary(), + false, + null!, + null!); + + compilationMock + .Protected() + .Setup("CommonGetTypeByMetadataName", ItExpr.Is(t => t != type)) + .Returns(Mock.Of()); + + compilationMock + .Protected() + .Setup("CommonGetTypeByMetadataName", ItExpr.Is(t => t == type)) + .Returns((INamedTypeSymbol?)null); + + var callbackMock = new Mock>(); + var result = SymbolLoader.TryLoad(compilationMock.Object, out var holder); + Assert.False(result); + Assert.Null(holder); + callbackMock.VerifyNoOtherCalls(); + } + + [Theory] + [InlineData(SymbolLoader.LegacyValidateTransitivelyAttribute)] + [InlineData(SymbolLoader.ValidateObjectMembersAttribute)] + [InlineData(SymbolLoader.ValidateEnumeratedItemsAttribute)] + public void Loader_ReturnsTrue_WhenOptionalTypeIsUnavailable(string type) + { + var compilationMock = new Mock( + string.Empty, + Array.Empty().ToImmutableArray(), + new Dictionary(), + false, + null!, + null!); + + compilationMock + .Protected() + .Setup("CommonGetTypeByMetadataName", ItExpr.Is(t => t != type)) + .Returns(Mock.Of()); + + compilationMock + .Protected() + .Setup("CommonGetTypeByMetadataName", ItExpr.Is(t => t == type)) + .Returns((INamedTypeSymbol?)null); + + var callbackMock = new Mock>(); + var result = SymbolLoader.TryLoad(compilationMock.Object, out var holder); + Assert.True(result); + Assert.NotNull(holder); + callbackMock.VerifyNoOtherCalls(); + } +} diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Directory.Build.props b/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Directory.Build.props new file mode 100644 index 0000000000..feb0e78d59 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Directory.Build.props @@ -0,0 +1,31 @@ + + + + + Microsoft.Gen.OptionsValidation.Test + Unit tests for Gen.OptionsValidation. + + + + true + true + true + + + + + + + + + + + + + + + + + + + diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Roslyn3.8/Microsoft.Gen.OptionsValidation.Roslyn3.8.Unit.Tests.csproj b/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Roslyn3.8/Microsoft.Gen.OptionsValidation.Roslyn3.8.Unit.Tests.csproj new file mode 100644 index 0000000000..eac2eac217 --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Roslyn3.8/Microsoft.Gen.OptionsValidation.Roslyn3.8.Unit.Tests.csproj @@ -0,0 +1,5 @@ + + + 3.8 + + diff --git a/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Roslyn4.0/Microsoft.Gen.OptionsValidation.Roslyn4.0.Unit.Tests.csproj b/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Roslyn4.0/Microsoft.Gen.OptionsValidation.Roslyn4.0.Unit.Tests.csproj new file mode 100644 index 0000000000..18ce9dd9ba --- /dev/null +++ b/test/Generators/Microsoft.Gen.OptionsValidation/Unit/Roslyn4.0/Microsoft.Gen.OptionsValidation.Roslyn4.0.Unit.Tests.csproj @@ -0,0 +1,6 @@ + + + 4.0 + $(DefineConstants);ROSLYN_4_0_OR_GREATER + + diff --git a/test/Generators/Shared/RoslynTestUtils.cs b/test/Generators/Shared/RoslynTestUtils.cs new file mode 100644 index 0000000000..7ae24eb56b --- /dev/null +++ b/test/Generators/Shared/RoslynTestUtils.cs @@ -0,0 +1,546 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Shared.Collections; +using Xunit; + +#pragma warning disable CA1716 +namespace Microsoft.Gen.Shared; +#pragma warning restore CA1716 + +internal static class RoslynTestUtils +{ +#if ROSLYN_4_0_OR_GREATER + internal const string RoslynVersion = "4.0"; +#else + internal const string RoslynVersion = "3.8"; +#endif + +#if DEBUG + internal const string BuildType = "Debug"; +#else + internal const string BuildType = "Release"; +#endif + + /// + /// Creates a canonical Roslyn project for testing. + /// + /// Assembly references to include in the project. + /// Whether to include references to the BCL assemblies. + public static Project CreateTestProject(IEnumerable? references, bool includeBaseReferences = true) + { + return CreateTestProject(references, Empty.Enumerable(), includeBaseReferences); + } + + /// + /// Creates a canonical Roslyn project for testing. + /// + /// Assembly references to include in the project. + /// Preprocessor symbols to run compilation with. + /// Whether to include references to the BCL assemblies. + public static Project CreateTestProject(IEnumerable? references, IEnumerable preprocessorSymbols, bool includeBaseReferences = true) + { + var corelib = Assembly.GetAssembly(typeof(object))!.Location; + var runtimeDir = Path.GetDirectoryName(corelib)!; + + var refs = new List(); + if (includeBaseReferences) + { + refs.Add(MetadataReference.CreateFromFile(corelib)); + refs.Add(MetadataReference.CreateFromFile(Path.Combine(runtimeDir, "netstandard.dll"))); + refs.Add(MetadataReference.CreateFromFile(Path.Combine(runtimeDir, "System.Runtime.dll"))); + } + + if (references != null) + { + foreach (var r in references) + { + refs.Add(MetadataReference.CreateFromFile(r.Location)); + } + } + +#pragma warning disable CA2000 // Dispose objects before losing scope + return new AdhocWorkspace() + .AddSolution(SolutionInfo.Create(SolutionId.CreateNewId(), VersionStamp.Create())) + .AddProject("Test", "test.dll", "C#") + .WithMetadataReferences(refs) + .WithParseOptions(new CSharpParseOptions().WithPreprocessorSymbols(preprocessorSymbols)) + .WithCompilationOptions( + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary).WithNullableContextOptions(NullableContextOptions.Enable)); +#pragma warning restore CA2000 // Dispose objects before losing scope + } + + public static void CommitChanges(this Project proj) + { + Assert.True(proj.Solution.Workspace.TryApplyChanges(proj.Solution)); + } + + public static Project WithDocument(this Project proj, string name, string text) + { + return proj.AddDocument(name, text).Project; + } + + public static Document FindDocument(this Project proj, string name) + { + foreach (var doc in proj.Documents) + { + if (doc.Name == name) + { + return doc; + } + } + + throw new FileNotFoundException(name); + } + + /// + /// Looks for /*N+*/ and /*-N*/ markers in a string and creates a TextSpan containing the enclosed text. + /// + public static TextSpan? MakeTextSpan(this string text, int spanNum) + { + var seq = $"/*{spanNum}+*/"; + int start = text.IndexOf(seq, StringComparison.Ordinal); + if (start < 0) + { + return null; + } + + start += seq.Length; + + int end = text.IndexOf($"/*-{spanNum}*/", StringComparison.Ordinal); + if (end < 0) + { + return null; + } + + return new TextSpan(start, end - start); + } + + public static void AssertDiagnostic(this string text, int spanNum, DiagnosticDescriptor expected, Diagnostic actual) + { + var expectedSpan = text.MakeTextSpan(spanNum); + if (expectedSpan != null) + { + Assert.True(expected.Id == actual.Id, + $"Span {spanNum} doesn't match: expected {expected.Id} but got {actual}"); + + Assert.True(expectedSpan.Equals(actual.Location.SourceSpan), + $"Span {spanNum} doesn't match: expected {expectedSpan} but got {actual.Location.SourceSpan}"); + } + else + { + Assert.True(false, $"Unexpected diagnostics {actual}"); + } + } + + public static void AssertDiagnostics(this string text, DiagnosticDescriptor expected, IEnumerable actual) + { + int spanNum = 0; + foreach (var d in actual) + { + TextSpan? expectedSpan = Location.None.SourceSpan; + if (d.Location != Location.None) + { + expectedSpan = text.MakeTextSpan(spanNum); + if (expectedSpan == null) + { + Assert.True(false, $"No span detected for diagnostic #{spanNum}, {d}"); + } + } + + Assert.True(expected.Id == d.Id, + $"Span {spanNum} doesn't match: expected {expected.Id} but got {d}"); + + Assert.True(expectedSpan.Equals(d.Location.SourceSpan), + $"Span {spanNum} doesn't match: expected {expectedSpan} but got {d.Location.SourceSpan}"); + + if (expectedSpan != Location.None.SourceSpan) + { + spanNum++; + } + } + + if (text.MakeTextSpan(spanNum) != null) + { + Assert.True(false, $"Diagnostic {spanNum} was not detected"); + } + } + + public static IReadOnlyList FilterDiagnostics(this IEnumerable diagnostics, params DiagnosticDescriptor[] filter) + { + var filtered = new List(); + foreach (Diagnostic diagnostic in diagnostics) + { + foreach (var f in filter) + { + if (diagnostic.Id.Equals(f.Id, StringComparison.Ordinal)) + { + filtered.Add(diagnostic); + break; + } + } + } + + return filtered; + } + + public static IReadOnlyList FilterOutDiagnostics(this IEnumerable diagnostics, params DiagnosticDescriptor[] filter) + { + var filtered = new List(); + foreach (Diagnostic diagnostic in diagnostics) + { + bool keep = true; + foreach (var f in filter) + { + if (diagnostic.Id.Equals(f.Id, StringComparison.Ordinal)) + { + keep = false; + break; + } + } + + if (keep) + { + filtered.Add(diagnostic); + } + } + + return filtered; + } + + /// + /// Runs a Roslyn generator over a set of source files. + /// + public static Task<(IReadOnlyList diagnostics, ImmutableArray generatedSources)> RunGenerator( + ISourceGenerator generator, + IEnumerable? references, + IEnumerable sources, + AnalyzerConfigOptionsProvider? optionsProvider = null, + bool includeBaseReferences = true, + CancellationToken cancellationToken = default) + { + return RunGenerator(generator, references, sources, Empty.Enumerable(), optionsProvider, includeBaseReferences, cancellationToken); + } + + /// + /// Runs a Roslyn generator over a set of source files. + /// + public static async Task<(IReadOnlyList diagnostics, ImmutableArray generatedSources)> RunGenerator( + ISourceGenerator generator, + IEnumerable? references, + IEnumerable sources, + IEnumerable preprocessorSymbols, + AnalyzerConfigOptionsProvider? optionsProvider = null, + bool includeBaseReferences = true, + CancellationToken cancellationToken = default) + { + var proj = CreateTestProject(references, preprocessorSymbols, includeBaseReferences); + + var count = 0; + foreach (var s in sources) + { + proj = proj.WithDocument($"src-{count++}.cs", s); + } + + proj.CommitChanges(); + var comp = await proj!.GetCompilationAsync(CancellationToken.None).ConfigureAwait(false); + + var cgd = CSharpGeneratorDriver.Create(new[] { generator }, optionsProvider: optionsProvider); + var gd = cgd.RunGenerators(comp!, cancellationToken); + + var r = gd.GetRunResult(); + return (Sort(r.Results[0].Diagnostics), r.Results[0].GeneratedSources); + } + + /// + /// Runs a Roslyn generator over a set of source files. + /// + public static Task<(IReadOnlyList diagnostics, ImmutableArray generatedSources)> RunGenerator( + IIncrementalGenerator generator, + IEnumerable? references, + IEnumerable sources, + AnalyzerConfigOptionsProvider? optionsProvider = null, + bool includeBaseReferences = true, + CancellationToken cancellationToken = default) + { + return RunGenerator(generator, references, sources, Empty.Enumerable(), optionsProvider, includeBaseReferences, cancellationToken); + } + + /// + /// Runs a Roslyn generator over a set of source files. + /// + public static async Task<(IReadOnlyList diagnostics, ImmutableArray generatedSources)> RunGenerator( + IIncrementalGenerator generator, + IEnumerable? references, + IEnumerable sources, + IEnumerable preprocessorSymbols, + AnalyzerConfigOptionsProvider? optionsProvider = null, + bool includeBaseReferences = true, + CancellationToken cancellationToken = default) + { +#if ROSLYN_4_0_OR_GREATER + preprocessorSymbols = preprocessorSymbols.Append("ROSLYN_4_0_OR_GREATER"); +#endif + + var proj = CreateTestProject(references, preprocessorSymbols, includeBaseReferences); + + var count = 0; + foreach (var s in sources) + { + proj = proj.WithDocument($"src-{count++}.cs", s); + } + + proj.CommitChanges(); + var comp = await proj!.GetCompilationAsync(CancellationToken.None).ConfigureAwait(false); + + CSharpParseOptions options = CSharpParseOptions.Default; + CSharpGeneratorDriver cgd = CSharpGeneratorDriver.Create(new[] { generator.AsSourceGenerator() }, parseOptions: options); + + var gd = cgd.RunGeneratorsAndUpdateCompilation(comp!, out var newComp, out var newDiags, cancellationToken); + var r = gd.GetRunResult(); + + if (r.Results[0].Diagnostics.Length == 0) + { + if (newDiags.Length > 0) + { + throw new InvalidDataException("Was unable to fully compile assembly with generated code"); + } + } + + return (Sort(r.Results[0].Diagnostics), r.Results[0].GeneratedSources); + } + + [Generator] + private sealed class Generator : ISourceGenerator + { + private readonly ISyntaxReceiver _receiver; + + public Generator(ISyntaxReceiver receiver) + { + _receiver = receiver; + } + + public void Execute(GeneratorExecutionContext context) + { + // Method intentionally left empty. + } + + public void Initialize(GeneratorInitializationContext context) => + context.RegisterForSyntaxNotifications(() => _receiver); + } + + /// + /// Runs a Roslyn generator over a set of source files. + /// + public static async Task RunSyntaxContextReceiver( + ISyntaxReceiver receiver, + IEnumerable? references, + IEnumerable sources, + bool includeBaseReferences = true) + { + var proj = CreateTestProject(references, includeBaseReferences); + var count = 0; + foreach (var s in sources) + { + proj = proj.WithDocument($"src-{count++}.cs", s); + } + + proj.CommitChanges(); + var comp = await proj.GetCompilationAsync().ConfigureAwait(false); + _ = CSharpGeneratorDriver.Create(new Generator(receiver)).RunGenerators(comp!); + return comp!; + } + + /// + /// Runs a Roslyn generator over a set of source files. + /// + public static async Task RunParser( + TReceiver receiver, + Func parser, + IEnumerable? references, + IEnumerable sources, + bool includeBaseReferences = true) + where TReceiver : ISyntaxReceiver + { + var comp = await RunSyntaxContextReceiver(receiver, references, sources, includeBaseReferences); + return parser(receiver, comp); + } + + /// + /// Runs a Roslyn analyzer over a set of source files. + /// + public static async Task> RunAnalyzer( + DiagnosticAnalyzer analyzer, + IEnumerable? references, + IEnumerable sources) + { + var proj = CreateTestProject(references); + + var count = 0; + foreach (var s in sources) + { + proj = proj.WithDocument($"src-{count++}.cs", s); + } + + proj.CommitChanges(); + + var analyzers = ImmutableArray.Create(analyzer); + + var comp = await proj!.GetCompilationAsync().ConfigureAwait(false); + var diags = await comp!.WithAnalyzers(analyzers).GetAllDiagnosticsAsync().ConfigureAwait(false); + return Sort(diags); + } + + private static IReadOnlyList Sort(ImmutableArray diags) + { + return diags.Sort((x, y) => + { + if (x.Location.SourceSpan.Start < y.Location.SourceSpan.Start) + { + return -1; + } + else if (x.Location.SourceSpan.Start > y.Location.SourceSpan.Start) + { + return 1; + } + + return 0; + }); + } + + /// + /// Runs a Roslyn analyzer and fixer. + /// + public static async Task> RunAnalyzerAndFixer( + DiagnosticAnalyzer analyzer, + CodeFixProvider fixer, + IEnumerable? references, + IEnumerable sources, + IEnumerable? sourceNames = null, + string? defaultNamespace = null, + string? extraFile = null) + { + var proj = CreateTestProject(references); + + var count = 0; + if (sourceNames != null) + { + var l = sourceNames.ToList(); + foreach (var s in sources) + { + proj = proj.WithDocument(l[count++], s); + } + } + else + { + foreach (var s in sources) + { + proj = proj.WithDocument($"src-{count++}.cs", s); + } + } + + if (defaultNamespace != null) + { + proj = proj.WithDefaultNamespace(defaultNamespace); + } + + proj.CommitChanges(); + + var analyzers = ImmutableArray.Create(analyzer); + + while (true) + { + var comp = await proj!.GetCompilationAsync().ConfigureAwait(false); + var diags = await comp!.WithAnalyzers(analyzers).GetAllDiagnosticsAsync().ConfigureAwait(false); + if (diags.IsEmpty) + { + // no more diagnostics reported by the analyzers + break; + } + + var actions = new List(); + foreach (var d in diags) + { + var doc = proj.GetDocument(d.Location.SourceTree); + + var context = new CodeFixContext(doc!, d, (action, _) => actions.Add(action), CancellationToken.None); + await fixer.RegisterCodeFixesAsync(context).ConfigureAwait(false); + } + + if (actions.Count == 0) + { + // nothing to fix + break; + } + + var operations = await actions[0].GetOperationsAsync(CancellationToken.None).ConfigureAwait(false); + var solution = operations.OfType().Single().ChangedSolution; + var changedProj = solution.GetProject(proj.Id); + if (changedProj != proj) + { + proj = await RecreateProjectDocumentsAsync(changedProj!).ConfigureAwait(false); + } + } + + var results = new List(); + + if (sourceNames != null) + { + var l = sourceNames.ToList(); + for (int i = 0; i < count; i++) + { + var s = await proj.FindDocument(l[i]).GetTextAsync().ConfigureAwait(false); + results.Add(Regex.Replace(s.ToString(), "\r\n", "\n", RegexOptions.IgnoreCase)); + } + } + else + { + for (int i = 0; i < count; i++) + { + var s = await proj.FindDocument($"src-{i}.cs").GetTextAsync().ConfigureAwait(false); + results.Add(Regex.Replace(s.ToString(), "\r\n", "\n", RegexOptions.IgnoreCase)); + } + } + + if (extraFile != null) + { + var s = await proj.FindDocument(extraFile).GetTextAsync().ConfigureAwait(false); + results.Add(Regex.Replace(s.ToString(), "\r\n", "\n", RegexOptions.IgnoreCase)); + } + + return results; + } + + private static async Task RecreateProjectDocumentsAsync(Project project) + { + foreach (var documentId in project.DocumentIds) + { + var document = project.GetDocument(documentId); + document = await RecreateDocumentAsync(document!).ConfigureAwait(false); + project = document.Project; + } + + return project; + } + + private static async Task RecreateDocumentAsync(Document document) + { + var newText = await document.GetTextAsync().ConfigureAwait(false); + return document.WithText(SourceText.From(newText.ToString(), newText.Encoding, newText.ChecksumAlgorithm)); + } +} diff --git a/test/Libraries/Directory.Build.props b/test/Libraries/Directory.Build.props new file mode 100644 index 0000000000..ef415739cb --- /dev/null +++ b/test/Libraries/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + $(NetCoreTargetFrameworks) + $(NetCoreTargetFrameworks)$(ConditionalNet462) + + diff --git a/test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/AsyncContextHttpContextOfTTests.cs b/test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/AsyncContextHttpContextOfTTests.cs new file mode 100644 index 0000000000..35e52c37f7 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/AsyncContextHttpContextOfTTests.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.AsyncState; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.AsyncState.Test; + +public class AsyncContextHttpContextOfTTests +{ + private readonly IHttpContextAccessor _accessorMock; + private readonly IAsyncState _asyncState; + private readonly IAsyncContext _context; + + public AsyncContextHttpContextOfTTests() + { + var serviceCollection = new ServiceCollection() + .AddHttpContextAccessor() + .AddAsyncStateHttpContext(); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + _accessorMock = serviceProvider.GetRequiredService(); + _accessorMock.HttpContext = new DefaultHttpContext(); + + _context = serviceProvider.GetRequiredService>(); + _asyncState = serviceProvider.GetRequiredService(); + _asyncState.Reset(); + } + + [Fact] + public void TryGetReturnsTrueWhenHttpContextPresent() + { + var value = new Thing(); + _context.Set(value); + + Assert.True(_context.TryGet(out Thing? stored)); + Assert.Same(value, stored); + } + + [Fact] + public void TryGetReturnsTrueWhenHttpContextPresentAndValueNotSet() + { + Assert.True(_context.TryGet(out Thing? stored)); + Assert.Null(stored); + } + + [Fact] + public void GetReturnsNullWhenHttpContextPresentAndValueNotSet() + { + Assert.Null(_context.Get()); + } + + [Fact] + public void TryGetReturnsFalseWhenHttpContextNotPresent() + { + _accessorMock.HttpContext = null; + + Assert.False(_context.TryGet(out Thing? stored)); + Assert.Null(stored); + } + + [Fact] + public void SetThrowsWhenHttpContextNotPresent() + { + _accessorMock.HttpContext = null; + var value = new Thing(); + + Assert.Throws(() => _context.Set(value)); + } + + [Fact] + public void GetThrowsWhenHttpContextNotPresent() + { + _accessorMock.HttpContext = null; + Assert.Throws(() => _context.Get()); + } + + [Fact] + public void TryGet_WhenAsyncStateIsUsed_ReturnsTrue() + { + _accessorMock.HttpContext = null; + _asyncState.Initialize(); + + var value = new Thing(); + _context.Set(value); + + Assert.True(_context.TryGet(out Thing? stored)); + Assert.Same(value, stored); + } + + [Fact] + public void TryGet_WhenAsyncStateIsUsedAndValueNotSet_ReturnsNull() + { + _accessorMock.HttpContext = null; + _asyncState.Initialize(); + + Assert.Null(_context.Get()); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/AsyncStateHttpContextExtensionsTests.cs b/test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/AsyncStateHttpContextExtensionsTests.cs new file mode 100644 index 0000000000..c4f6e34843 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/AsyncStateHttpContextExtensionsTests.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.AsyncState; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.AsyncState.Test; + +public class AsyncStateHttpContextExtensionsTests +{ + [Fact] + public void AddAsyncStateHttpContext_Throws_WhenNullService() + { + Assert.Throws(() => AsyncStateHttpContextExtensions.AddAsyncStateHttpContext(null!)); + } + + [Fact] + public void AddAsyncStateHttpContext_AddsWithCorrectLifetime() + { + var services = new ServiceCollection(); + + services.AddAsyncStateHttpContext(); + + var serviceDescriptor = services.First(x => x.ServiceType == typeof(IHttpContextAccessor)); + Assert.Equal(ServiceLifetime.Singleton, serviceDescriptor.Lifetime); + + serviceDescriptor = services.First(x => x.ServiceType == typeof(IAsyncContext<>)); + Assert.Equal(ServiceLifetime.Singleton, serviceDescriptor.Lifetime); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/Microsoft.AspNetCore.AsyncState.Tests.csproj b/test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/Microsoft.AspNetCore.AsyncState.Tests.csproj new file mode 100644 index 0000000000..9495fec22f --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/Microsoft.AspNetCore.AsyncState.Tests.csproj @@ -0,0 +1,14 @@ + + + Microsoft.AspNetCore.AsyncState.Test + Unit tests for Microsoft.AspNetCore.AsyncState. + + + + + + + + + + diff --git a/test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/Mock/IThing.cs b/test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/Mock/IThing.cs new file mode 100644 index 0000000000..a82d23111a --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/Mock/IThing.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.AsyncState.Test; + +public interface IThing +{ + string Hello(); +} diff --git a/test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/Mock/Thing.cs b/test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/Mock/Thing.cs new file mode 100644 index 0000000000..c6e0e2d9a9 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.AsyncState.Tests/Mock/Thing.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.AsyncState.Test; + +public class Thing : IThing +{ + public string Hello() + { + return "Hello World!"; + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/AcceptanceTest.cs b/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/AcceptanceTest.cs new file mode 100644 index 0000000000..22a02b4517 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/AcceptanceTest.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Connections.Test; + +[Collection(nameof(StaticFakeClockExecution))] +public sealed class AcceptanceTest +{ + private readonly FakeTimeProvider _fakeTimeProvider; + + public AcceptanceTest() + { + _fakeTimeProvider = new FakeTimeProvider(); + } + + [Fact] + public async Task ConnectionTimeout_KeepsConnection_BeforeTimeout() + { + var shutdownTimeout = TimeSpan.FromMinutes(3); + using var host = await FakeHost.CreateBuilder() + .ConfigureWebHost(webHostBuilder => webHostBuilder + .ConfigureServices(services => + { + services.Configure(options => options.Timeout = shutdownTimeout); + }) + .ListenHttpOnAnyPort() + .UseKestrel((_, serverOptions) => + { + serverOptions.ConfigureEndpointDefaults(listenOptions => + { + listenOptions.UseConnectionTimeout(); + }); + }) + .UseStartup()) + .StartAsync(); + + var address = host.GetListenUris().Single(); + + using var httpClientHandler = new SocketsHttpHandler + { + MaxConnectionsPerServer = 1 + }; + + string? initialConnectionId; + using var httpClient = new HttpClient(httpClientHandler); + using (var response = await httpClient.GetAsync(address)) + { + response.StatusCode.Should().Be(HttpStatusCode.OK); + initialConnectionId = response.Headers.GetValues("ConnectionId").FirstOrDefault(); + initialConnectionId.Should().NotBeNullOrEmpty(); + } + + _fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(16)); + + using (var response = await httpClient.GetAsync(address)) + { + response.StatusCode.Should().Be(HttpStatusCode.OK); + var connectionId = response.Headers.GetValues("ConnectionId").FirstOrDefault(); + connectionId.Should().NotBeNullOrEmpty(); + connectionId.Should().Be(initialConnectionId); + } + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/ConnectionTimeoutDelegateTests.cs b/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/ConnectionTimeoutDelegateTests.cs new file mode 100644 index 0000000000..a0088f7755 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/ConnectionTimeoutDelegateTests.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Time.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Connections.Test; + +[Collection(nameof(StaticFakeClockExecution))] +public sealed class ConnectionTimeoutDelegateTests +{ + private readonly FakeTimeProvider _fakeTimeProvider; + + public ConnectionTimeoutDelegateTests() + { + _fakeTimeProvider = new FakeTimeProvider(); + } + + [Fact] + public async Task OnConnectionAsync_ShouldThrow_WhenNoFeatures() + { + var next = Mock.Of(); + var connectionTimeoutDelegate = new ConnectionTimeoutDelegate(next, Options.Create(new ConnectionTimeoutOptions())) { TimeProvider = _fakeTimeProvider }; + + var featureCollection = new FeatureCollection(); + var connectionContext = new Mock(); + connectionContext + .Setup(context => context.Features) + .Returns(featureCollection); + + await Assert.ThrowsAsync(() => + connectionTimeoutDelegate.OnConnectionAsync(connectionContext.Object)); + } + + [Fact] + public async Task OnConnectionAsync_ShouldCallNext_WhenNotificationFeature() + { + var next = Mock.Of(); + var connectionTimeoutDelegate = new ConnectionTimeoutDelegate(next, Options.Create(new ConnectionTimeoutOptions())) { TimeProvider = _fakeTimeProvider }; + + var notificationFeature = new Mock(); + var featureCollection = new FeatureCollection(); + featureCollection.Set(notificationFeature.Object); + + var connectionContext = new Mock(); + connectionContext + .Setup(context => context.Features) + .Returns(featureCollection); + + await connectionTimeoutDelegate.OnConnectionAsync(connectionContext.Object); + + Mock.Get(next).Verify(c => c.Invoke(connectionContext.Object), Times.Exactly(1)); + } + + [Fact] + public async Task OnConnectionAsync_ShouldCloseConnection_WhenTimeout() + { + using var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(15)); + + using var semaphore = new SemaphoreSlim(0, 1); + bool requestedClose = false; + + var connectionContext = new Mock(); + var next = new Mock(); + next + .Setup(c => c.Invoke(connectionContext.Object)) + .Returns(async () => Assert.True(await semaphore.WaitAsync(TimeSpan.FromSeconds(15), cts.Token))); + + var connectionOptions = Options.Create(new ConnectionTimeoutOptions { Timeout = TimeSpan.FromMilliseconds(10) }); + var connectionTimeoutDelegate = new ConnectionTimeoutDelegate(next.Object, connectionOptions); + + var notificationFeature = new Mock(); + notificationFeature + .Setup(feature => feature.RequestClose()) + .Callback(() => + { + requestedClose = true; + semaphore.Release(); + }); + + var featureCollection = new FeatureCollection(); + featureCollection.Set(notificationFeature.Object); + + connectionContext + .Setup(context => context.Features) + .Returns(featureCollection); + + connectionContext + .Setup(context => context.ConnectionClosed) + .Returns(() => CancellationToken.None); + + var connectionTask = connectionTimeoutDelegate.OnConnectionAsync(connectionContext.Object); + + while (!requestedClose) + { + cts.Token.ThrowIfCancellationRequested(); + _fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(10)); + } + + await connectionTask; + + Assert.True(requestedClose); + next.Verify(c => c.Invoke(connectionContext.Object), Times.Exactly(1)); + notificationFeature.Verify(n => n.RequestClose(), Times.Exactly(1)); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/ConnectionTimeoutExtensionsTests.cs b/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/ConnectionTimeoutExtensionsTests.cs new file mode 100644 index 0000000000..55a613645e --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/ConnectionTimeoutExtensionsTests.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Connections.Test; + +public class ConnectionTimeoutExtensionsTests +{ + [Fact] + public async Task AddConnectionTimeout_ShouldAddOptionsValidators_WhenUsingFunc() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddConnectionTimeout(c => c.Timeout = TimeSpan.FromDays(100))) + .Build(); + + var ex = await Record.ExceptionAsync(() => host.StartAsync()).ConfigureAwait(false); + Assert.IsType(ex); + } + + [Fact] + public async Task AddConnectionTimeout_ShouldAddOptionsValidators_WhenUsingConfig() + { + var configuration = + new ConfigurationBuilder() + .AddInMemoryCollection(new List> + { + new("Timeout:Timeout", "01:00:01") // One hour and one second. + }) + .Build() + .GetSection("Timeout"); + + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddConnectionTimeout(configuration)) + .Build(); + + var ex = await Record.ExceptionAsync(() => host.StartAsync()).ConfigureAwait(false); + Assert.IsType(ex); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/ConnectionTimeoutValidatorTests.cs b/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/ConnectionTimeoutValidatorTests.cs new file mode 100644 index 0000000000..be17ead8e9 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/ConnectionTimeoutValidatorTests.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.AspNetCore.Connections.Test; + +public class ConnectionTimeoutValidatorTests +{ + [Theory] + [InlineData(0)] + [InlineData(60001)] + [InlineData(99999)] + public void ConnectionTimeoutValidator_GivenOutOfAllowedRangeTimeout_ReturnsValidationFailed(int seconds) + { + var validator = new ConnectionTimeoutValidator(); + var options = new ConnectionTimeoutOptions + { + Timeout = TimeSpan.FromSeconds(seconds) + }; + + var validationResult = validator.Validate(nameof(ConnectionTimeoutOptions), options); + + Assert.True(validationResult.Failed); + Assert.Contains(nameof(ConnectionTimeoutOptions.Timeout), validationResult.FailureMessage); + } + + [Theory] + [InlineData(1)] + [InlineData(3600)] + public void ConnectionTimeoutValidator_GivenAllowedRangeTimeout_ReturnsValidationSucceeded(int seconds) + { + var validator = new ConnectionTimeoutValidator(); + var options = new ConnectionTimeoutOptions + { + Timeout = TimeSpan.FromSeconds(seconds) + }; + + var validationResult = validator.Validate(nameof(ConnectionTimeoutOptions), options); + + Assert.True(validationResult.Succeeded, validationResult.FailureMessage); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/Microsoft.AspNetCore.ConnectionTimeout.Tests.csproj b/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/Microsoft.AspNetCore.ConnectionTimeout.Tests.csproj new file mode 100644 index 0000000000..dc3501165d --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/Microsoft.AspNetCore.ConnectionTimeout.Tests.csproj @@ -0,0 +1,17 @@ + + + Microsoft.AspNetCore.Connections.Test + Unit tests for Microsoft.AspNetCore.ConnectionTimeout. + + + + $(NetCoreTargetFrameworks) + + + + + + + + + diff --git a/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/Startup.cs b/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/Startup.cs new file mode 100644 index 0000000000..44958c3851 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/Startup.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Connections.Test; + +[SuppressMessage("Design", "CA1052:Static holder types should be Static or NotInheritable", Justification = "Used through reflection")] +public class Startup +{ + public static void ConfigureServices(IServiceCollection services) + { + services.AddRouting(); + } + + public static void Configure(IApplicationBuilder app) + { + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapGet("/", async context => + { + var connectionFeature = context.Features.Get()!; + context.Response.Headers.Add("ConnectionId", connectionFeature!.ConnectionId); + await context.Response.WriteAsync("Hello World!"); + }); + }); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/StaticFakeClockExecution.cs b/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/StaticFakeClockExecution.cs new file mode 100644 index 0000000000..7d21f8cb6f --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.ConnectionTimeout.Tests/StaticFakeClockExecution.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.AspNetCore.Connections.Test; + +[CollectionDefinition(nameof(StaticFakeClockExecution), DisableParallelization = true)] +public class StaticFakeClockExecution +{ +} diff --git a/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/CommonHeadersTests.cs b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/CommonHeadersTests.cs new file mode 100644 index 0000000000..5d28c24ac2 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/CommonHeadersTests.cs @@ -0,0 +1,147 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.HeaderParsing.Parsers; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.HeaderParsing.Test; + +public class CommonHeadersTests +{ + [Fact] + public void Host_header_has_correct_setup() + { + Assert.Equal(HeaderNames.Host, CommonHeaders.Host.HeaderName); + Assert.Equal(HostHeaderValueParser.Instance, CommonHeaders.Host.ParserInstance); + Assert.Null(CommonHeaders.Host.ParserType); + } + + [Fact] + public void Accept_header_has_correct_setup() + { + Assert.Equal(HeaderNames.Accept, CommonHeaders.Accept.HeaderName); + Assert.Equal(MediaTypeHeaderValueListParser.Instance, CommonHeaders.Accept.ParserInstance); + Assert.Null(CommonHeaders.Accept.ParserType); + } + + [Fact] + public void AcceptEncoding_header_has_correct_setup() + { + Assert.Equal(HeaderNames.AcceptEncoding, CommonHeaders.AcceptEncoding.HeaderName); + Assert.Equal(StringWithQualityHeaderValueListParser.Instance, CommonHeaders.AcceptEncoding.ParserInstance); + Assert.Null(CommonHeaders.AcceptEncoding.ParserType); + } + + [Fact] + public void AcceptLanguage_header_has_correct_setup() + { + Assert.Equal(HeaderNames.AcceptLanguage, CommonHeaders.AcceptLanguage.HeaderName); + Assert.Equal(StringWithQualityHeaderValueListParser.Instance, CommonHeaders.AcceptLanguage.ParserInstance); + Assert.Null(CommonHeaders.AcceptLanguage.ParserType); + } + + [Fact] + public void CacheControl_header_has_correct_setup() + { + Assert.Equal(HeaderNames.CacheControl, CommonHeaders.CacheControl.HeaderName); + Assert.Equal(CacheControlHeaderValueParser.Instance, CommonHeaders.CacheControl.ParserInstance); + Assert.Null(CommonHeaders.CacheControl.ParserType); + } + + [Fact] + public void ContentDisposition_header_has_correct_setup() + { + Assert.Equal(HeaderNames.ContentDisposition, CommonHeaders.ContentDisposition.HeaderName); + Assert.Equal(ContentDispositionHeaderValueParser.Instance, CommonHeaders.ContentDisposition.ParserInstance); + Assert.Null(CommonHeaders.ContentDisposition.ParserType); + } + + [Fact] + public void ContentType_header_has_correct_setup() + { + Assert.Equal(HeaderNames.ContentType, CommonHeaders.ContentType.HeaderName); + Assert.Equal(MediaTypeHeaderValueParser.Instance, CommonHeaders.ContentType.ParserInstance); + Assert.Null(CommonHeaders.ContentType.ParserType); + } + + [Fact] + public void Cookie_header_has_correct_setup() + { + Assert.Equal(HeaderNames.Cookie, CommonHeaders.Cookie.HeaderName); + Assert.Equal(CookieHeaderValueListParser.Instance, CommonHeaders.Cookie.ParserInstance); + Assert.Null(CommonHeaders.Cookie.ParserType); + } + + [Fact] + public void Date_header_has_correct_setup() + { + Assert.Equal(HeaderNames.Date, CommonHeaders.Date.HeaderName); + Assert.Equal(DateTimeOffsetParser.Instance, CommonHeaders.Date.ParserInstance); + Assert.Null(CommonHeaders.Date.ParserType); + } + + [Fact] + public void IfMatch_header_has_correct_setup() + { + Assert.Equal(HeaderNames.IfMatch, CommonHeaders.IfMatch.HeaderName); + Assert.Equal(EntityTagHeaderValueListParser.Instance, CommonHeaders.IfMatch.ParserInstance); + Assert.Null(CommonHeaders.IfMatch.ParserType); + } + + [Fact] + public void IfModifiedSince_header_has_correct_setup() + { + Assert.Equal(HeaderNames.IfModifiedSince, CommonHeaders.IfModifiedSince.HeaderName); + Assert.Equal(EntityTagHeaderValueListParser.Instance, CommonHeaders.IfModifiedSince.ParserInstance); + Assert.Null(CommonHeaders.IfModifiedSince.ParserType); + } + + [Fact] + public void IfNoneMatch_header_has_correct_setup() + { + Assert.Equal(HeaderNames.IfNoneMatch, CommonHeaders.IfNoneMatch.HeaderName); + Assert.Equal(EntityTagHeaderValueListParser.Instance, CommonHeaders.IfNoneMatch.ParserInstance); + Assert.Null(CommonHeaders.IfNoneMatch.ParserType); + } + + [Fact] + public void IfRange_header_has_correct_setup() + { + Assert.Equal(HeaderNames.IfRange, CommonHeaders.IfRange.HeaderName); + Assert.Equal(RangeConditionHeaderValueParser.Instance, CommonHeaders.IfRange.ParserInstance); + Assert.Null(CommonHeaders.IfRange.ParserType); + } + + [Fact] + public void IfUnmodifiedSince_header_has_correct_setup() + { + Assert.Equal(HeaderNames.IfUnmodifiedSince, CommonHeaders.IfUnmodifiedSince.HeaderName); + Assert.Equal(DateTimeOffsetParser.Instance, CommonHeaders.IfUnmodifiedSince.ParserInstance); + Assert.Null(CommonHeaders.IfUnmodifiedSince.ParserType); + } + + [Fact] + public void Range_header_has_correct_setup() + { + Assert.Equal(HeaderNames.Range, CommonHeaders.Range.HeaderName); + Assert.Equal(RangeHeaderValueParser.Instance, CommonHeaders.Range.ParserInstance); + Assert.Null(CommonHeaders.Range.ParserType); + } + + [Fact] + public void Referer_header_has_correct_setup() + { + Assert.Equal(HeaderNames.Referer, CommonHeaders.Referer.HeaderName); + Assert.Equal(UriParser.Instance, CommonHeaders.Referer.ParserInstance); + Assert.Null(CommonHeaders.Referer.ParserType); + } + + [Fact] + public void XForwardedFor_header_has_correct_setup() + { + Assert.Equal("X-Forwarded-For", CommonHeaders.XForwardedFor.HeaderName); + Assert.Equal(IPAddressListParser.Instance, CommonHeaders.XForwardedFor.ParserInstance); + Assert.Null(CommonHeaders.XForwardedFor.ParserType); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderKeyTests.cs b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderKeyTests.cs new file mode 100644 index 0000000000..b07e9f026e --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderKeyTests.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net; +using Microsoft.AspNetCore.HeaderParsing.Parsers; +using Xunit; + +namespace Microsoft.AspNetCore.HeaderParsing.Test; + +public class HeaderKeyTests +{ + private const string TestHeaderName = "Test-Header"; + private const int TestHeaderPosition = 5; + private static readonly DateTimeOffset _testHeaderDefaultValue = DateTimeOffset.Now; + + [Fact] + public void ToString_returns_header_name() + { + var sut = new HeaderKey(TestHeaderName, DateTimeOffsetParser.Instance, TestHeaderPosition); + Assert.Equal(TestHeaderName, sut.ToString()); + } + + [Fact] + public void Ctor_propagates_arguments_to_properties() + { + var sut = new HeaderKey(TestHeaderName, DateTimeOffsetParser.Instance, TestHeaderPosition); + + Assert.Equal(TestHeaderName, sut.Name); + Assert.Equal(DateTimeOffsetParser.Instance, sut.Parser); + Assert.Equal(TestHeaderPosition, sut.Position); + } + + [Fact] + public void DefaultValue_returns_default_when_no_value_set() + { + var referenceTimeDefault = new HeaderKey>(TestHeaderName, IPAddressListParser.Instance, TestHeaderPosition); + var valueTypeDefault = new HeaderKey(TestHeaderName, DateTimeOffsetParser.Instance, TestHeaderPosition); + + Assert.False(referenceTimeDefault.HasDefaultValue); + Assert.Null(referenceTimeDefault.DefaultValue); + + Assert.False(valueTypeDefault.HasDefaultValue); + Assert.Equal(default, valueTypeDefault.DefaultValue); + } + + [Fact] + public void DefaultValue_returns_default_value() + { + var sut = new HeaderKey(TestHeaderName, DateTimeOffsetParser.Instance, TestHeaderPosition, 0, _testHeaderDefaultValue); + + Assert.True(sut.HasDefaultValue); + Assert.Equal(_testHeaderDefaultValue, sut.DefaultValue); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingExtensionsTests.cs b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingExtensionsTests.cs new file mode 100644 index 0000000000..832f737748 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingExtensionsTests.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.HeaderParsing.Test; + +public class HeaderParsingExtensionsTests +{ + [Fact] + public void AddHeaderParsing_configures_options_using_delegate() + { + var services = new ServiceCollection() + .AddHeaderParsing(options => options.DefaultValues.Add("Test", "9")) + .BuildServiceProvider(); + + var options = services.GetRequiredService>().Value; + + Assert.True(options.DefaultValues.ContainsKey("Test")); + } + + [Fact] + public void AddHeaderParsing_configures_invalid_options() + { + var services = new ServiceCollection() + .AddHeaderParsing(options => options.DefaultMaxCachedValuesPerHeader = -1) + .BuildServiceProvider(); + + Assert.Throws(() => services.GetRequiredService>().Value); + } + + [Fact] + public void AddHeaderParsing_configures_invalid_options_MaxCachedValuesPerHeader() + { + var services = new ServiceCollection() + .AddHeaderParsing(options => options.MaxCachedValuesPerHeader["Date"] = -1) + .BuildServiceProvider(); + + Assert.Throws(() => services.GetRequiredService>().Value); + } + + [Fact] + public void AddHeaderParsing_section() + { + var services = new ServiceCollection() + .AddHeaderParsing(GetConfigurationSection()) + .BuildServiceProvider(); + + var options = services.GetRequiredService>().Value; + + Assert.Equal(123, options.DefaultMaxCachedValuesPerHeader); + + static IConfigurationSection GetConfigurationSection() + { + HeaderParsingOptions options; + + return new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { + $"{nameof(HeaderParsingOptions)}:{nameof(options.DefaultMaxCachedValuesPerHeader)}", + "123" + }, + }) + .Build() + .GetSection($"{nameof(HeaderParsingOptions)}"); + } + } + + [Fact] + public void Register_header_registers_given_header() + { + var headerKey = new HeaderKey("Test", CommonHeaders.Date.ParserInstance!, 0); + + var headerSetup = CommonHeaders.Date; + var headerRegistry = new Mock(); + headerRegistry.Setup(x => x.Register(headerSetup)).Returns(headerKey); + + var context = CreateContext(new ServiceCollection().AddSingleton(headerRegistry.Object)); + Assert.Equal(headerKey, RegisterHeader(context, headerSetup)); + } + + [Fact] + public void GetHeaderParsing_caches_and_returns_header_parsing_feature() + { + var context = CreateContext(new ServiceCollection().AddHeaderParsing()); + + var feature = context.Request.GetHeaderParsing(); + + Assert.NotNull(feature); + Assert.Equal(context, feature.Context); + Assert.Equal(feature, context.Request.GetHeaderParsing()); + } + + [Fact] + public void TryParseHeader_parses_a_header() + { + var date = DateTimeOffset.UtcNow.ToString("R", CultureInfo.InvariantCulture); + + var context = CreateContext(new ServiceCollection().AddHeaderParsing()); + var dateHeaderKey = RegisterHeader(context, CommonHeaders.Date); + context.Request.Headers["Date"] = date; + + Assert.True(context.Request.TryGetHeaderValue(dateHeaderKey, out var parsedDate, out var result)); + Assert.Equal(date, parsedDate.ToString("R", CultureInfo.InvariantCulture)); + Assert.Equal(ParsingResult.Success, result); + + Assert.True(context.Request.TryGetHeaderValue(dateHeaderKey, out parsedDate)); + Assert.Equal(date, parsedDate.ToString("R", CultureInfo.InvariantCulture)); + Assert.Equal(ParsingResult.Success, result); + } + + private static HttpContext CreateContext(IServiceCollection? services = null) + { + services ??= new ServiceCollection(); + services.AddFakeLogging(); + + return new DefaultHttpContext + { + RequestServices = services.BuildServiceProvider() + }; + } + + private static HeaderKey RegisterHeader(HttpContext context, HeaderSetup setup) + where T : notnull + { + return context + .RequestServices + .GetRequiredService() + .Register(setup); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingFeatureTests.cs b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingFeatureTests.cs new file mode 100644 index 0000000000..76b076883a --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingFeatureTests.cs @@ -0,0 +1,202 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using FluentAssertions; +using Microsoft.AspNetCore.HeaderParsing.Parsers; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Microsoft.Extensions.Telemetry.Testing.Metering; +using Microsoft.Net.Http.Headers; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.HeaderParsing.Test; + +public sealed class HeaderParsingFeatureTests +{ + private readonly IOptions _options; + private readonly IServiceCollection _services; + private readonly FakeLogger _logger = new(); + private IHeaderRegistry? _registry; + private HttpContext? _context; + + private IHeaderRegistry Registry => _registry ??= new HeaderRegistry(_services.BuildServiceProvider(), _options); + private HttpContext Context => _context ??= new DefaultHttpContext { RequestServices = _services.BuildServiceProvider() }; + + public HeaderParsingFeatureTests() + { + _options = Options.Create(new HeaderParsingOptions()); + _services = new ServiceCollection(); + } + + [Fact] + public void Parses_header() + { + using var meter = new Meter(); + + var date = DateTimeOffset.Now.ToString("R", CultureInfo.InvariantCulture); + + var key = Registry.Register(CommonHeaders.Date); + Context.Request.Headers["Date"] = date; + + var feature = new HeaderParsingFeature(Registry, _logger, meter) { Context = Context }; + + Assert.True(feature.TryGetHeaderValue(key, out var value, out var _)); + Assert.Equal(date, value.ToString("R", CultureInfo.InvariantCulture)); + } + + [Fact] + public void Parses_multiple_headers() + { + using var meter = new Meter(); + + var currentDate = DateTimeOffset.Now.ToString("R", CultureInfo.InvariantCulture); + var futureDate = DateTimeOffset.Now.AddHours(1).ToString("R", CultureInfo.InvariantCulture); + + var key = Registry.Register(CommonHeaders.Date); + Context.Request.Headers["Date"] = currentDate; + Context.Request.Headers["Test"] = futureDate; + + var feature = new HeaderParsingFeature(Registry, _logger, meter) { Context = Context }; + + Assert.True(feature.TryGetHeaderValue(key, out var value, out var result)); + Assert.Equal(currentDate, value.ToString("R", CultureInfo.InvariantCulture)); + Assert.Equal(ParsingResult.Success, result); + + var key2 = Registry.Register(new HeaderSetup("Test", DateTimeOffsetParser.Instance)); + + Assert.True(feature.TryGetHeaderValue(key2, out var textValue, out result)); + Assert.Equal(futureDate, textValue.ToString("R", CultureInfo.InvariantCulture)); + Assert.Equal(ParsingResult.Success, result); + } + + [Fact] + public void Parses_with_late_binding() + { + using var meter = new Meter(); + var date = DateTimeOffset.Now.ToString("R", CultureInfo.InvariantCulture); + + Context.Request.Headers["Date"] = date; + + var feature = new HeaderParsingFeature(Registry, _logger, meter) { Context = Context }; + + var key = Registry.Register(CommonHeaders.Date); + + Assert.True(feature.TryGetHeaderValue(key, out var value, out var result)); + Assert.Equal(date, value.ToString("R", CultureInfo.InvariantCulture)); + Assert.Equal(ParsingResult.Success, result); + } + + [Fact] + public void TryParse_returns_false_on_header_not_found() + { + using var meter = new Meter(); + var feature = new HeaderParsingFeature(Registry, _logger, meter) { Context = Context }; + var key = Registry.Register(CommonHeaders.Date); + + Assert.False(feature.TryGetHeaderValue(key, out var value, out var _)); + + Assert.Equal("Header 'Date' not found.", _logger.Collector.LatestRecord.Message); + } + + [Fact] + public void TryParse_returns_default_on_header_not_found() + { + using var meter = new Meter(); + + var date = DateTimeOffset.Now.ToString("R", CultureInfo.InvariantCulture); + _options.Value.DefaultValues.Add("Date", date); + + var feature = new HeaderParsingFeature(Registry, _logger, meter) { Context = Context }; + var key = Registry.Register(CommonHeaders.Date); + + Assert.True(feature.TryGetHeaderValue(key, out var value, out var result)); + Assert.Equal(date, value.ToString("R", CultureInfo.InvariantCulture)); + Assert.Equal(ParsingResult.Success, result); + + Assert.Equal("Using a default value for header 'Date'.", _logger.Collector.LatestRecord.Message); + } + + [Fact] + public void TryParse_returns_false_on_error() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + Context.Request.Headers["Date"] = "Not a date."; + + var feature = new HeaderParsingFeature(Registry, _logger, meter) { Context = Context }; + var key = Registry.Register(CommonHeaders.Date); + + Assert.False(feature.TryGetHeaderValue(key, out var value, out var result)); + Assert.Equal(default, value); + Assert.Equal(ParsingResult.Error, result); + + Assert.Equal("Can't parse header 'Date' due to 'Unable to parse date time offset value.'.", _logger.Collector.LatestRecord.Message); + + var latest = metricCollector.GetCounterValues(@"R9.HeaderParsing.ParsingErrors")!.LatestWritten!; + latest.Value.Should().Be(1); + latest.GetDimension("HeaderName").Should().Be("Date"); + latest.GetDimension("Kind").Should().Be("Unable to parse date time offset value."); + } + + [Fact] + public void Dispose_resets_state_and_returns_to_pool() + { + using var meter = new Meter(); + + var pool = new Mock>(MockBehavior.Strict); + var helper = new HeaderParsingFeature.PoolHelper(pool.Object, Registry, _logger, meter); + helper.Feature.Context = Context; + pool.Setup(x => x.Return(helper)); + + var firstHeaderKey = Registry.Register(new HeaderSetup("FirstHeader", DateTimeOffsetParser.Instance)); + var secondHeaderKey = Registry.Register(new HeaderSetup("SecondHeader", DateTimeOffsetParser.Instance)); + var thirdHeaderKey = Registry.Register(new HeaderSetup("ThirdHeader", DateTimeOffsetParser.Instance)); + + Assert.False(helper.Feature.TryGetHeaderValue(firstHeaderKey, out _)); + Assert.False(helper.Feature.TryGetHeaderValue(thirdHeaderKey, out _)); + + helper.Dispose(); + Assert.Null(helper.Feature.Context); + + Context.Request.Headers[firstHeaderKey.Name] = DateTimeOffset.Now.ToString("R", CultureInfo.InvariantCulture); + Context.Request.Headers[thirdHeaderKey.Name] = DateTimeOffset.Now.ToString("R", CultureInfo.InvariantCulture); + + helper.Feature.Context = Context; + + Assert.True(helper.Feature.TryGetHeaderValue(firstHeaderKey, out _)); + Assert.True(helper.Feature.TryGetHeaderValue(thirdHeaderKey, out _)); + + pool.VerifyAll(); + } + + [Fact] + public void CachingWorks() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + Context.Request.Headers[HeaderNames.CacheControl] = "max-age=604800"; + + var feature = new HeaderParsingFeature(Registry, _logger, meter) { Context = Context }; + var feature2 = new HeaderParsingFeature(Registry, _logger, meter) { Context = Context }; + var key = Registry.Register(CommonHeaders.CacheControl); + + Assert.True(feature.TryGetHeaderValue(key, out var value1, out var error1)); + Assert.True(feature.TryGetHeaderValue(key, out var value2, out var error2)); + Assert.True(feature2.TryGetHeaderValue(key, out var value3, out var error3)); + Assert.Same(value1, value2); + Assert.Same(value1, value3); + + var latest = metricCollector.GetCounterValues(@"R9.HeaderParsing.CacheAccess")!.LatestWritten!; + latest.Value.Should().Be(1); + latest.GetDimension("HeaderName").Should().Be(HeaderNames.CacheControl); + latest.GetDimension("Type").Should().Be("Hit"); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingOptionsTests.cs b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingOptionsTests.cs new file mode 100644 index 0000000000..d45ca63725 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderParsingOptionsTests.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.HeaderParsing.Test; + +public class HeaderParsingOptionsTests +{ + [Fact] + public void ReadWrite() + { + var defValue = new Dictionary(); + var maxCachedValues = new Dictionary(); + + var options = new HeaderParsingOptions + { + DefaultValues = defValue, + DefaultMaxCachedValuesPerHeader = 123, + MaxCachedValuesPerHeader = maxCachedValues + }; + + Assert.Equal(defValue, options.DefaultValues); + Assert.Equal(123, options.DefaultMaxCachedValuesPerHeader); + Assert.Equal(maxCachedValues, options.MaxCachedValuesPerHeader); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderRegistryTests.cs b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderRegistryTests.cs new file mode 100644 index 0000000000..f7cda87ac0 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderRegistryTests.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using Microsoft.AspNetCore.HeaderParsing.Parsers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.HeaderParsing.Test; + +public class HeaderRegistryTests +{ + private readonly IOptions _options; + private readonly IServiceCollection _services; + + public HeaderRegistryTests() + { + _options = Options.Create(new HeaderParsingOptions()); + _services = new ServiceCollection(); + } + + [Fact] + public void Registers_header_without_default_value() + { + var registry = new HeaderRegistry(_services.BuildServiceProvider(), _options); + + var key = registry.Register(CommonHeaders.Date); + + Assert.Equal(0, key.Position); + Assert.Equal(CommonHeaders.Date.ParserInstance, key.Parser); + Assert.Equal(CommonHeaders.Date.HeaderName, key.Name); + Assert.False(key.HasDefaultValue); + } + + [Fact] + public void Registers_header_with_typed_default_value() + { + var date = DateTimeOffset.Now.ToString("R", CultureInfo.InvariantCulture); + + _options.Value.DefaultValues.Add("Date", date); + + var registry = new HeaderRegistry(_services.BuildServiceProvider(), _options); + + var key = registry.Register(CommonHeaders.Date); + + Assert.True(key.HasDefaultValue); + Assert.Equal(date, key.DefaultValue.ToString("R", CultureInfo.InvariantCulture)); + } + + [Fact] + public void Registers_header_with_bad_default_value() + { + var date = "BOGUS"; + + _options.Value.DefaultValues.Add("Date", date); + + var registry = new HeaderRegistry(_services.BuildServiceProvider(), _options); + + Assert.Throws(() => registry.Register(CommonHeaders.Date)); + } + + [Fact] + public void Registers_multiple_headers_with_rising_index() + { + var registry = new HeaderRegistry(_services.BuildServiceProvider(), _options); + + var key1 = registry.Register(CommonHeaders.Date); + var key2 = registry.Register(CommonHeaders.Accept); + var key3 = registry.Register(CommonHeaders.AcceptLanguage); + + Assert.Equal(0, key1.Position); + Assert.Equal(1, key2.Position); + Assert.Equal(2, key3.Position); + } + + [Fact] + public void Registers_same_header_with_same_index() + { + var registry = new HeaderRegistry(_services.BuildServiceProvider(), _options); + + var key1 = registry.Register(CommonHeaders.Date); + var key2 = registry.Register(CommonHeaders.Date); + + Assert.Equal(key1.Position, key2.Position); + } + + [Fact] + public void Registers_header_with_parser_type() + { + _services.AddSingleton(DateTimeOffsetParser.Instance); + + var registry = new HeaderRegistry(_services.BuildServiceProvider(), _options); + + var key = registry.Register(new HeaderSetup("MyDate", typeof(DateTimeOffsetParser))); + + Assert.IsType(key.Parser); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderSetupTests.cs b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderSetupTests.cs new file mode 100644 index 0000000000..2c312683c5 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HeaderSetupTests.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.HeaderParsing.Parsers; +using Xunit; + +namespace Microsoft.AspNetCore.HeaderParsing.Test; + +public class HeaderSetupTests +{ + private const string TestHeaderName = "Test-Header"; + + [Fact] + public void New_with_parser_instance() + { + var sut = new HeaderSetup(TestHeaderName, DateTimeOffsetParser.Instance); + + Assert.Equal(TestHeaderName, sut.HeaderName); + Assert.Equal(DateTimeOffsetParser.Instance, sut.ParserInstance); + Assert.Null(sut.ParserType); + } + + [Fact] + public void New_with_parser_type() + { + var sut = new HeaderSetup(TestHeaderName, typeof(DateTimeOffsetParser)); + + Assert.Equal(TestHeaderName, sut.HeaderName); + Assert.Equal(typeof(DateTimeOffsetParser), sut.ParserType); + Assert.Null(sut.ParserInstance); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HostHeaderValueTests.cs b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HostHeaderValueTests.cs new file mode 100644 index 0000000000..3ce1fca265 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/HostHeaderValueTests.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.AspNetCore.HeaderParsing.Test; + +public class HostHeaderValueTests +{ + [Fact] + public void EqualsTest() + { + var host1 = new HostHeaderValue("localhost", 80); + var sameAsHost1 = new HostHeaderValue("localhost", 80); + var differentHost = new HostHeaderValue("127.0.0.1", 80); + var differentPort = new HostHeaderValue("localhost", 443); + + Assert.Equal(sameAsHost1, host1); + Assert.NotEqual(differentHost, host1); + Assert.NotEqual(differentPort, host1); + } + + [Fact] + public void ObjectEqualsTest() + { + var host1 = new HostHeaderValue("localhost", 80); + object sameAsHost1 = new HostHeaderValue("localhost", 80); + object differentHost = new HostHeaderValue("127.0.0.1", 80); + object differentPort = new HostHeaderValue("localhost", 443); + +#pragma warning disable IDE0004 // Remove Unnecessary Cast + Assert.True(sameAsHost1.Equals((object)host1)); + Assert.False(differentHost.Equals((object)host1)); + Assert.False(differentPort.Equals((object)host1)); + Assert.False(differentPort.Equals(new object())); +#pragma warning restore IDE0004 // Remove Unnecessary Cast + + Assert.NotEqual(new object(), host1); + } + + [Fact] + public void EqualsOperatorsTest() + { + var host1 = new HostHeaderValue("localhost", 80); + var sameAsHost1 = new HostHeaderValue("localhost", 80); + var differentHost = new HostHeaderValue("127.0.0.1", 80); + var differentPort = new HostHeaderValue("localhost", 443); + + Assert.True(host1 == sameAsHost1); + Assert.False(host1 == differentHost); + Assert.False(host1 == differentPort); + + Assert.False(host1 != sameAsHost1); + Assert.True(host1 != differentHost); + Assert.True(host1 != differentPort); + } + + [Fact] + public void GetHashCodeTest() + { + var host1HashCode = new HostHeaderValue("localhost", 80).GetHashCode(); + var sameAsHost1HashCode = new HostHeaderValue("localhost", 80).GetHashCode(); + var differentHostHashCode = new HostHeaderValue("127.0.0.1", 80).GetHashCode(); + var differentPortHashCode = new HostHeaderValue("localhost", 443).GetHashCode(); + + Assert.Equal(sameAsHost1HashCode, host1HashCode); + Assert.NotEqual(differentHostHashCode, host1HashCode); + Assert.NotEqual(differentPortHashCode, host1HashCode); + } + + [Fact] + public void ToStringTest() + { + var hhv = new HostHeaderValue("foo", null); + Assert.Equal("foo", hhv.ToString()); + + hhv = new HostHeaderValue("foo", 82); + Assert.Equal("foo:82", hhv.ToString()); + } + + [Fact] + public void Invalid() + { + Assert.False(HostHeaderValue.TryParse(string.Empty, out var _)); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/Microsoft.AspNetCore.HeaderParsing.Tests.csproj b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/Microsoft.AspNetCore.HeaderParsing.Tests.csproj new file mode 100644 index 0000000000..49763f958a --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/Microsoft.AspNetCore.HeaderParsing.Tests.csproj @@ -0,0 +1,17 @@ + + + Microsoft.AspNetCore.HeaderParsing.Test + Unit tests for Microsoft.AspNetCore.HeaderParsing. + + + + $(NetCoreTargetFrameworks) + + + + + + + + + diff --git a/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/ParserTests.cs b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/ParserTests.cs new file mode 100644 index 0000000000..c5637869c3 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.HeaderParsing.Tests/ParserTests.cs @@ -0,0 +1,431 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using Microsoft.AspNetCore.HeaderParsing.Parsers; +using Microsoft.Extensions.Primitives; +using Xunit; +using UriParser = Microsoft.AspNetCore.HeaderParsing.Parsers.UriParser; + +namespace Microsoft.AspNetCore.HeaderParsing.Test; + +public class ParserTests +{ + [Fact] + public void Host_ReturnsParsedValue() + { + var sv = new StringValues("web.vortex.data.microsoft.com"); + Assert.True(HostHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Equal("web.vortex.data.microsoft.com", result.Host); + Assert.Null(result.Port); + Assert.Null(error); + } + + [Fact] + public void Host_Multi() + { + var sv = new StringValues(new[] { "Hello", "World" }); + Assert.False(HostHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void Date_ReturnsParsedValue() + { + var sv = new StringValues("Wed, 21 Oct 2015 07:28:14 GMT"); + Assert.True(DateTimeOffsetParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Equal(DayOfWeek.Wednesday, result.DayOfWeek); + Assert.Equal(21, result.Day); + Assert.Equal(10, result.Month); + Assert.Equal(2015, result.Year); + Assert.Equal(7, result.Hour); + Assert.Equal(28, result.Minute); + Assert.Equal(14, result.Second); + Assert.Null(error); + } + + [Fact] + public void Date_ReturnsNullForEmptyValue() + { + var sv = new StringValues(string.Empty); + Assert.False(DateTimeOffsetParser.Instance.TryParse(sv, out var result, out var error)); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void Date_ReturnsNullForInvalidValue() + { + var sv = new StringValues("Hello World"); + Assert.False(DateTimeOffsetParser.Instance.TryParse(sv, out var result, out var error)); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void Date_Multi() + { + var sv = new StringValues(new[] { "Hello", "World" }); + Assert.False(DateTimeOffsetParser.Instance.TryParse(sv, out var result, out var error)); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void Cookkie_ReturnsParsedValue() + { + var sv = new StringValues("csrftoken=u32t4o3tb3gg43"); + Assert.True(CookieHeaderValueListParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Equal(1, result.Count); + Assert.Equal("csrftoken", result[0].Name.Value); + Assert.Equal("u32t4o3tb3gg43", result[0].Value.Value); + Assert.Null(error); + } + + [Fact] + public void Cookie_ReturnsMultipleCookiesForMultipleCookies() + { + var sv = new StringValues("csrftoken=u32t4o3tb3gg43; _gat=1"); + Assert.True(CookieHeaderValueListParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Equal(2, result.Count); + Assert.Equal("csrftoken", result[0].Name.Value); + Assert.Equal("u32t4o3tb3gg43", result[0].Value.Value); + Assert.Equal("_gat", result[1].Name.Value); + Assert.Equal("1", result[1].Value.Value); + Assert.Null(error); + } + + [Fact] + public void Cookie_ReturnsNullForEmptyValue() + { + var sv = new StringValues(string.Empty); + Assert.False(CookieHeaderValueListParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Null(result); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void Cookie_ReturnsNullForInvalidValue() + { + var sv = new StringValues("HelloWorld"); + Assert.False(CookieHeaderValueListParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Null(result); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void CacheControl_ReturnsParsedValue() + { + var sv = new StringValues("public, max-age=604800"); + Assert.True(CacheControlHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); + Assert.True(result.Public); + Assert.Equal(TimeSpan.FromSeconds(604800), result.MaxAge); + Assert.Null(error); + } + + [Fact] + public void CacheControl_ReturnsNullWhenInvalid() + { + var sv = new StringValues("ZZZ=ZZZ=ZZZ=ZZZ"); + Assert.False(CacheControlHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Null(result); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void CacheControl_Multi() + { + var sv = new StringValues(new[] { "Hello", "World" }); + Assert.False(CacheControlHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Null(result); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void ContentDisposition_ReturnsParsedValue() + { + var sv = new StringValues("attachment; filename=\"cool.html\""); + Assert.True(ContentDispositionHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Equal("cool.html", result.FileName); + Assert.Equal("attachment", result.DispositionType); + Assert.Null(error); + } + + [Fact] + public void ContentDisposition_Multi() + { + var sv = new StringValues(new[] { "attachment; filename=\"cool.html\"", "attachment; filename=\"cool.html\"" }); + Assert.False(ContentDispositionHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Null(result); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void ContentDisposition_ReturnsNullWhenInvalid() + { + var sv = new StringValues("zz=zz=zz"); + Assert.False(ContentDispositionHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Null(result); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void MediaType_ReturnsParsedValue() + { + var sv = new StringValues("text/html; charset=UTF-8"); + Assert.True(MediaTypeHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Equal("text/html", result.MediaType); + Assert.Equal("UTF-8", result.Charset); + Assert.Null(error); + } + + [Fact] + public void MediaType_Multi() + { + var sv = new StringValues(new[] { "text/html; charset=UTF-8", "text/html; charset=UTF-8" }); + Assert.False(MediaTypeHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Null(result); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void MediaType_ReturnsNullWhenInvalid() + { + var sv = new StringValues(string.Empty); + Assert.False(MediaTypeHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Null(result); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void MediaTypes_ReturnsParsedValue() + { + var sv = new StringValues("text/html; charset=UTF-8"); + Assert.True(MediaTypeHeaderValueListParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Equal(1, result.Count); + Assert.Equal("text/html", result[0].MediaType); + Assert.Equal("UTF-8", result[0].Charset); + Assert.Null(error); + } + + [Fact] + public void MediaTypes_ReturnsNullWhenInvalid() + { + var sv = new StringValues(string.Empty); + Assert.False(MediaTypeHeaderValueListParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Null(result); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void EntityTag_ReturnsParsedValue() + { + var sv = new StringValues("\"HelloWorld\""); + Assert.True(EntityTagHeaderValueListParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Equal(1, result!.Count); + Assert.Equal("\"HelloWorld\"", result[0].Tag); + Assert.Null(error); + } + + [Fact] + public void EntityTag_ReturnsNullWhenInvalid() + { + var sv = new StringValues(string.Empty); + Assert.False(EntityTagHeaderValueListParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Null(result); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void StringQuality_ReturnsParsedValue() + { + var sv = new StringValues("en-US"); + Assert.True(StringWithQualityHeaderValueListParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Equal(1, result!.Count); + Assert.Equal("en-US", result[0].Value); + Assert.Null(error); + } + + [Fact] + public void StringQuality_Multi() + { + var sv = new StringValues("en-US,en;q=0.5"); + Assert.True(StringWithQualityHeaderValueListParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Equal(2, result.Count); + Assert.Equal("en-US", result[0].Value); + Assert.Equal("en", result[1].Value); + Assert.Equal(0.5, result[1].Quality); + Assert.Null(error); + } + + [Fact] + public void StringQuality_ReturnsNullWhenInvalid() + { + var sv = new StringValues(string.Empty); + Assert.False(StringWithQualityHeaderValueListParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Null(result); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void Uri_ReturnsParsedValue() + { + var sv = new StringValues("https://foo.com:81"); + Assert.True(UriParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Equal("foo.com", result!.Host); + Assert.Equal(81, result.Port); + Assert.Null(error); + } + + [Fact] + public void Uri_Multi() + { + var sv = new StringValues(new[] { "http://foo.com", "http://bar.com" }); + Assert.False(UriParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Null(result); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void Uri_ReturnsNullWhenInvalid() + { + var sv = new StringValues("http://localhost:XXX"); + Assert.False(UriParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Null(result); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void Range_ReturnsParsedValue() + { + var sv = new StringValues("bytes=200-1000"); + Assert.True(RangeHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Equal("bytes", result!.Unit); + Assert.Equal(1, result.Ranges.Count); + Assert.Equal(200, result.Ranges.Single().From); + Assert.Equal(1000, result.Ranges.Single().To); + Assert.Null(error); + } + + [Fact] + public void Range_Multi() + { + var sv = new StringValues(new[] { "bytes=200-1000", "bytes=3000-4000" }); + Assert.False(RangeHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Null(result); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void Range_ReturnsNullWhenInvalid() + { + var sv = new StringValues("Hello World"); + Assert.False(RangeHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Null(result); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void RangeCondition_ReturnsParsedValue() + { + var sv = new StringValues("Wed, 21 Oct 2015 07:28:14 GMT"); + Assert.True(RangeConditionHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Equal(DayOfWeek.Wednesday, result!.LastModified!.Value.DayOfWeek); + Assert.Equal(21, result!.LastModified.Value.Day); + Assert.Equal(10, result!.LastModified.Value.Month); + Assert.Equal(2015, result!.LastModified.Value.Year); + Assert.Equal(7, result!.LastModified.Value.Hour); + Assert.Equal(28, result!.LastModified!.Value.Minute); + Assert.Equal(14, result!.LastModified.Value.Second); + Assert.Null(error); + + sv = new StringValues("\"67ab43\""); + Assert.True(RangeConditionHeaderValueParser.Instance.TryParse(sv, out result, out error)); + Assert.Equal("\"67ab43\"", result!.EntityTag!.Tag); + Assert.Null(error); + } + + [Fact] + public void RangeCondition_Multi() + { + var sv = new StringValues(new[] { "\"67ab43\"", "\"67ab43\"" }); + Assert.False(RangeConditionHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Null(result); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void RangeCondition_ReturnsNullWhenInvalid() + { + var sv = new StringValues("Hello World"); + Assert.False(RangeConditionHeaderValueParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Null(result); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void IpAddressesList_WithSpaces_ReturnsParsedValue() + { + var sv = new StringValues(new[] { " 1.1.1.1 , 192.168.1.100 ", " 3.3.3.3 " }); + Assert.True(IPAddressListParser.Instance.TryParse(sv, out var result, out var error)); + Assert.NotNull(result); + Assert.Equal(new List { IPAddress.Parse("1.1.1.1"), IPAddress.Parse("192.168.1.100"), IPAddress.Parse("3.3.3.3") }, result); + Assert.Null(error); + } + + [Fact] + public void IpAddressesList_NoSpaces_ReturnsParsedValue() + { + var sv = new StringValues(new[] { "1.1.1.1,192.168.1.100", "3.3.3.3" }); + Assert.True(IPAddressListParser.Instance.TryParse(sv, out var result, out var error)); + Assert.NotNull(result); + Assert.Equal(new List { IPAddress.Parse("1.1.1.1"), IPAddress.Parse("192.168.1.100"), IPAddress.Parse("3.3.3.3") }, result); + Assert.Null(error); + } + + [Theory] + [InlineData(" 1.1.1.1")] + [InlineData("1.1.1.1")] + public void IpAddressesList_SingleIpV4_ReturnsParsedValue(string testValue) + { + var sv = new StringValues(new[] { testValue }); + Assert.True(IPAddressListParser.Instance.TryParse(sv, out var result, out var error)); + Assert.NotNull(result); + Assert.Equal(new List { IPAddress.Parse("1.1.1.1") }, result); + Assert.Null(error); + } + + [Theory] + [InlineData(" 1::ffff")] + [InlineData("1::ffff")] + public void IpAddressesList_SingleIpV6_ReturnsParsedValue(string testValue) + { + var sv = new StringValues(new[] { testValue }); + Assert.True(IPAddressListParser.Instance.TryParse(sv, out var result, out var error)); + Assert.NotNull(result); + Assert.Equal(new List { IPAddress.Parse("1::ffff") }, result); + Assert.Null(error); + } + + [Theory] + [InlineData("1.1.1.1, 2.2.2.2", "a.b.c.d")] + [InlineData("1.1.1.1,,2.2.2.2")] + [InlineData("1.1.1.1,")] + public void IpAddressesList_Malformed_ReturnsNull(params string[] values) + { + var sv = new StringValues(values); + Assert.False(IPAddressListParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Null(result); + Assert.False(string.IsNullOrEmpty(error)); + } + + [Fact] + public void IpAddressesList_FirstEmptyIp_ReturnsErrorAboutEmptyIp() + { + var sv = new StringValues(",1.1.1.1"); + Assert.False(IPAddressListParser.Instance.TryParse(sv, out var result, out var error)); + Assert.Null(result); + Assert.Equal("IP address cannot be empty.", error); + } +} + diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/AcceptanceTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/AcceptanceTest.cs new file mode 100644 index 0000000000..2b3f988638 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/AcceptanceTest.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Telemetry.Latency; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Latency.Test; +public class AcceptanceTest +{ + [Fact] + public async Task RequestLatency_LatencyContextIsStarted() + { + bool isInLambda = false; + string checkpointName = "testc"; + string tagName = "testt"; + string measureName = "testm"; + + using var host = await FakeHost.CreateBuilder() + .ConfigureWebHost(webBuilder => webBuilder + .UseTestServer() + .ConfigureServices(services => + { + services + .AddRouting() + .AddLatencyContext() + .AddRequestLatencyTelemetry() + .RegisterCheckpointNames(new[] { checkpointName }) + .RegisterTagNames(new[] { tagName }) + .RegisterMeasureNames(new[] { measureName }); + }) + .Configure(app => + { + app.UseRouting(); + app.UseRequestLatencyTelemetry(); + app.Use(async (context, next) => + { + string tagValue = "testVal"; + int measureValue = 17; + int taskTimeMs = 20; + + isInLambda = true; + var latencyContext = context.RequestServices.GetRequiredService(); + var tokenIssuer = context.RequestServices.GetRequiredService(); + Assert.NotNull(latencyContext); + latencyContext.AddCheckpoint(tokenIssuer.GetCheckpointToken(checkpointName)); + latencyContext.SetTag(tokenIssuer.GetTagToken(tagName), tagValue); + latencyContext.RecordMeasure(tokenIssuer.GetMeasureToken(measureName), measureValue); + await Task.Delay(taskTimeMs + 10); // Adding 10 ms buffer + await next.Invoke().ConfigureAwait(false); + + var ld = latencyContext!.LatencyData; + Assert.True(IsMatchByName(ld.Checkpoints, (c) => c.Name == checkpointName)); + Assert.True(IsMatchByName(ld.Measures, (m) => m.Name == measureName)); + Assert.True(IsMatchByName(ld.Tags, (t) => t.Name == tagName)); + Assert.True(((double)ld.DurationTimestamp / ld.DurationTimestampFrequency) * 1000 >= taskTimeMs); + }); + + app.UseEndpoints(endpoints => + { + endpoints.MapGet("/", async context => + { + await context.Response.WriteAsync("Hello World!"); + }); + }); + })) + .StartAsync().ConfigureAwait(false); + + _ = await host.GetTestClient().GetAsync("/").ConfigureAwait(false); + await host.StopAsync(); + + Assert.True(isInLambda); + } + + private static bool IsMatchByName(in ReadOnlySpan span, Func isMatch) + { + for (int i = 0; i < span.Length; i++) + { + if (isMatch(span[i])) + { + return true; + } + } + + return false; + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Checkpoint/AcceptanceTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Checkpoint/AcceptanceTest.cs new file mode 100644 index 0000000000..254050431f --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Checkpoint/AcceptanceTest.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Primitives; +using Microsoft.Extensions.Telemetry.Latency; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Test; + +public class AcceptanceTest +{ + private static void SetupServices(IHostBuilder builder) + { + builder.ConfigureWebHost(webBuilder => webBuilder + .UseTestServer() + .ConfigureServices(services => services + .AddRouting() + .AddLatencyContext() + .AddRequestCheckpoint() + .AddScoped(p => p.GetRequiredService().CreateContext()))); + } + + [Fact] + public async Task RequestCheckpoint_CanMeasureMiddlewarePipeTime() + { + var reachedLambda = false; + var exitPipelineValue = 0d; + var responseProcessedValue = 0d; + + using var host = await FakeHost.CreateBuilder() + .Configure(SetupServices) + .ConfigureWebHost(webBuilder => webBuilder.Configure(app => + { + app.Use(async (context, next) => + { + var latencyContext = context.RequestServices.GetRequiredService(); + await next.Invoke().ConfigureAwait(false); + latencyContext.TryGetCheckpoint(RequestCheckpointConstants.ElapsedTillPipelineExitMiddleware, out var exitPipeline, out var exitPipelineFreq); + latencyContext.TryGetCheckpoint(RequestCheckpointConstants.ElapsedResponseProcessed, out var responseProcessed, out var responseProcessedFreq); + + reachedLambda = true; + exitPipelineValue = ((double)exitPipeline / exitPipelineFreq) * 1000; + responseProcessedValue = ((double)responseProcessed / responseProcessedFreq) * 1000; + }); + + app.UseRouting(); + app.UseRequestCheckpoint(); + app.UseEndpoints(endpoints => endpoints.MapGet("/", async context => await context.Response.WriteAsync("Hello World!"))); + })) + .StartAsync(); + + _ = await host.GetTestClient().GetAsync("/").ConfigureAwait(false); + + Assert.True(reachedLambda); + Assert.InRange(exitPipelineValue, 1, 10_000); + Assert.InRange(responseProcessedValue, 1, 10_000); + } + + [Fact] + public async Task RequestCheckpointMiddleware_Does_Not_Throw_When_ServerTiming_Header_Is_Already_Set() + { + var exitPipelineValue = 0d; + var responseProcessedValue = 0d; + var alreadySetServerTimingHeader = new StringValues("Already-Set-Some-Header;blabla"); + + using var host = await FakeHost.CreateBuilder() + .Configure(SetupServices) + .ConfigureWebHost(webBuilder => webBuilder.Configure(app => + { + app.Use(async (context, next) => + { + var latencyContext = context.RequestServices.GetRequiredService(); + await next.Invoke().ConfigureAwait(false); + latencyContext.TryGetCheckpoint(RequestCheckpointConstants.ElapsedTillPipelineExitMiddleware, out var exitPipeline, out var exitPipelineFreq); + latencyContext.TryGetCheckpoint(RequestCheckpointConstants.ElapsedResponseProcessed, out var responseProcessed, out var responsedProcessedFreq); + exitPipelineValue = ((double)exitPipeline / exitPipelineFreq) * 1000; + responseProcessedValue = ((double)responseProcessed / responsedProcessedFreq) * 1000; + }); + + app.Use((ctx, next) => + { + ctx.Response.Headers.Add("Server-Timing", alreadySetServerTimingHeader); + + return next(); + }); + + app.UseRouting(); + app.UseRequestCheckpoint(); + app.UseEndpoints(endpoints => endpoints.MapGet("/", async context => await context.Response.WriteAsync("Hello World!"))); + })) + .StartAsync(); + + HttpResponseMessage? response = null; + + var e = await Record.ExceptionAsync(async () => response = await host.GetTestClient().GetAsync("/").ConfigureAwait(false)); + + Assert.Null(e); + Assert.NotNull(response); + + var h = response.Headers + .GetValues("Server-Timing") + .FirstOrDefault(); + + Assert.NotNull(h); + Assert.NotEmpty(h); + Assert.Contains(alreadySetServerTimingHeader!, h); + } + + [Fact] + public async Task MiddlewareTest_ReturnsNotFoundForRequest() + { + using var host = await FakeHost.CreateBuilder() + .Configure(SetupServices) + .ConfigureWebHost(webBuilder => webBuilder.Configure(app => app.UseRequestCheckpoint())) + .StartAsync(); + + using var response = await host.GetTestServer().CreateClient().GetAsync("/Path"); + + var timeHeaders = response.Headers.GetValues("Server-Timing").ToArray(); + var metricFragments = Assert.Single(timeHeaders).Split('=', 2); + Assert.Equal(2, metricFragments.Length); + Assert.Equal("reqlatency;dur", metricFragments[0]); + Assert.True(long.TryParse(metricFragments[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)); + Assert.InRange(value, 0, 10_000); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Checkpoint/AddServerTimingHeaderMiddlewareTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Checkpoint/AddServerTimingHeaderMiddlewareTest.cs new file mode 100644 index 0000000000..e8ba38fc6b --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Checkpoint/AddServerTimingHeaderMiddlewareTest.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Telemetry; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Telemetry.Latency; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Test; + +public class AddServerTimingHeaderMiddlewareTest +{ + private static readonly RequestDelegate _stubRequestDelegate = + static _ => Task.CompletedTask; + + [Fact] + public async Task Middleware_ReturnsTotalMillisecondsElapsed_InsteadOfFraction() + { + const long TimeAdvanceMs = 1500L; // We need to use any value greater than 1000 (1 second) + + using FakeLatencyContext fakeLatencyContextController = new(); + Checkpoint checkpoint = new(RequestCheckpointConstants.ElapsedTillHeaders, TimeAdvanceMs, 1000); + ArraySegment checkpoints = new(new[] { checkpoint }); + fakeLatencyContextController.LatencyData = new LatencyData(default, checkpoints, default, default, default); + + using var serviceProvider = new ServiceCollection() + .AddSingleton(_ => fakeLatencyContextController) + .BuildServiceProvider(); + + var context = new DefaultHttpContext + { + RequestServices = serviceProvider + }; + + var fakeHttpResponseFeature = new FakeHttpResponseFeature(); + context.Features.Set(fakeHttpResponseFeature); + + var middleware = new AddServerTimingHeaderMiddleware(); + await middleware.InvokeAsync(context, _stubRequestDelegate); + await fakeHttpResponseFeature.StartAsync(); + + var header = context.Response.Headers[AddServerTimingHeaderMiddleware.ServerTimingHeaderName]; + Assert.NotEmpty(header); + Assert.Equal($"reqlatency;dur={TimeAdvanceMs}", header[0]); + } + + private sealed class FakeHttpResponseFeature : HttpResponseFeature + { + private Func _responseStartingAsync = + static () => Task.CompletedTask; + + public override void OnStarting(Func callback, object state) + { + var prior = _responseStartingAsync; + _responseStartingAsync = async () => + { + await callback(state); + await prior(); + }; + } + + public async Task StartAsync() => await _responseStartingAsync(); + } + + private sealed class FakeLatencyContext : ILatencyContext + { + public LatencyData LatencyData { get; set; } + + public void AddCheckpoint(CheckpointToken token) => throw new NotSupportedException(); + public void AddMeasure(MeasureToken token, long value) => throw new NotSupportedException(); + public void Dispose() + { + // Method intentionally left empty. + } + + public void Freeze() => throw new NotSupportedException(); + public void RecordMeasure(MeasureToken _0, long _1) => throw new NotSupportedException(); + public void SetTag(TagToken _0, string _1) => throw new NotSupportedException(); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Checkpoint/LatencyContextControlExtensionsTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Checkpoint/LatencyContextControlExtensionsTest.cs new file mode 100644 index 0000000000..3562a4e382 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Checkpoint/LatencyContextControlExtensionsTest.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.Telemetry; +using Microsoft.Extensions.Telemetry.Latency; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Test; + +public class LatencyContextControlExtensionsTest +{ + [Fact] + public void TryGetCheckpoint_ReturnsTrue_WhenPresent() + { + var cc = new Mock(); + var ld = GetLatencyData(); + cc.Setup(cc => cc.LatencyData).Returns(ld); + + Assert.True(cc.Object.TryGetCheckpoint("ca", out var elapsed1, out var elapsed1Freq)); + } + + [Fact] + public void TryGetCheckpoint_ReturnsFalse_WhenAbsent() + { + var cc = new Mock(); + var ld = GetLatencyData(); + cc.Setup(cc => cc.LatencyData).Returns(ld); + + Assert.False(cc.Object.TryGetCheckpoint("not", out var elapsed2, out var elapsed2Freq)); + } + + private static LatencyData GetLatencyData() + { + var checkpoints = new[] { new Checkpoint("ca", default, default) }; + var chkSegment = new ArraySegment(checkpoints); + return new LatencyData(default, chkSegment, default, default, default); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Checkpoint/RequestCheckpointExtensionsTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Checkpoint/RequestCheckpointExtensionsTest.cs new file mode 100644 index 0000000000..fe3735b1a2 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Checkpoint/RequestCheckpointExtensionsTest.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.Telemetry; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Test; + +public class RequestCheckpointExtensionsTest +{ + [Fact] + public void AddRequestCheckpoint_Throws_WhenNullBuilder() + { + Assert.Throws(() => RequestCheckpointExtensions.AddRequestCheckpoint(null!)); + } + + [Fact] + public void UseRequestCheckpoint_Throws_WhenNullBuilder() + { + Assert.Throws(() => RequestCheckpointExtensions.UseRequestCheckpoint(null!)); + } + + [Fact] + public void AddPipelineEntryCheckpoint_Throws_WhenNullBuilder() + { + Assert.Throws(() => RequestCheckpointExtensions.AddPipelineEntryCheckpoint(null!)); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Internal/RequestLatencyTelemetryMiddlewareTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Internal/RequestLatencyTelemetryMiddlewareTest.cs new file mode 100644 index 0000000000..42164f09d9 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Internal/RequestLatencyTelemetryMiddlewareTest.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Telemetry.Internal; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Latency; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Test.Internal; + +public class RequestLatencyTelemetryMiddlewareTest +{ + [Fact] + public async Task RequestLatency_GivenContext_InvokesOperations() + { + var ex1 = new TestExporter(); + var ex2 = new TestExporter(); + var m = new RequestLatencyTelemetryMiddleware(Options.Create(new RequestLatencyTelemetryOptions()), new List { ex1, ex2 }); + + var lc = GetMockLatencyContext(); + var httpContextMock = GetHttpContext(lc.Object); + + var nextInvoked = false; + await m.InvokeAsync(httpContextMock, (_) => + { + nextInvoked = true; + return Task.CompletedTask; + }); + + lc.Verify(c => c.Freeze()); + Assert.True(nextInvoked); + Assert.True(ex1.Invoked == 1); + Assert.True(ex2.Invoked == 1); + } + + [Fact] + public async Task RequestLatency_NoExporter() + { + var lc = GetMockLatencyContext(); + var httpContextMock = GetHttpContext(lc.Object); + var m = new RequestLatencyTelemetryMiddleware(Options.Create(new RequestLatencyTelemetryOptions()), Array.Empty()); + + var nextInvoked = false; + await m.InvokeAsync(httpContextMock, (_) => + { + nextInvoked = true; + return Task.CompletedTask; + }); + + lc.Verify(c => c.Freeze()); + Assert.True(nextInvoked); + } + + [Fact] + public async Task RequestLatency_GivenTimeout_PassedToExport() + { + var exportTimeout = TimeSpan.FromSeconds(1); + var ex1 = new TimeConsumingExporter(TimeSpan.FromSeconds(5)); + + var m = new RequestLatencyTelemetryMiddleware( + Options.Create(new RequestLatencyTelemetryOptions { LatencyDataExportTimeout = exportTimeout }), + new List { ex1 }); + + var lc = GetMockLatencyContext(); + var httpContextMock = GetHttpContext(lc.Object); + + var nextInvoked = false; + await m.InvokeAsync(httpContextMock, (_) => + { + nextInvoked = true; + return Task.CompletedTask; + }); + await httpContextMock.Response.CompleteAsync(); + + lc.Verify(c => c.Freeze()); + Assert.True(nextInvoked); + } + + private static HttpContext GetHttpContext(ILatencyContext latencyContext) + { + var httpContextMock = new DefaultHttpContext(); + + var feature = new Mock(); + feature.Setup(m => m.OnCompleted(It.IsAny>(), It.IsAny())) + .Callback, object>((c, o) => c(o)); + httpContextMock.Features.Set(feature.Object); + + var serviceProviderMock = new Mock(); + serviceProviderMock + .Setup(serviceProvider => serviceProvider.GetService(typeof(ILatencyContext))) + .Returns(latencyContext); + httpContextMock.RequestServices = serviceProviderMock.Object; + + return httpContextMock; + } + + private static Mock GetMockLatencyContext() + { + var cc = new Mock(); + return cc; + } + + private class TestExporter : ILatencyDataExporter + { + public int Invoked { get; private set; } + + public async Task ExportAsync(LatencyData latencyData, CancellationToken cancellationToken) + { + Invoked++; + await Task.CompletedTask; + } + } + + private class TimeConsumingExporter : ILatencyDataExporter + { + public int Invoked { get; private set; } + + private readonly TimeSpan _timeSpanToDelay; + + public TimeConsumingExporter(TimeSpan timeSpanToDelay) + { + _timeSpanToDelay = timeSpanToDelay; + } + + public async Task ExportAsync(LatencyData latencyData, CancellationToken cancellationToken) + { + Invoked++; + + var e = await Record.ExceptionAsync(() => Task.Delay(_timeSpanToDelay, cancellationToken)); + Assert.IsAssignableFrom(e); + } + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Internal/RequestLatencyTelemetryOptionsValidatorTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Internal/RequestLatencyTelemetryOptionsValidatorTest.cs new file mode 100644 index 0000000000..d4a12c68e3 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/Internal/RequestLatencyTelemetryOptionsValidatorTest.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.Telemetry.Internal; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Test.Internal; + +public class RequestLatencyTelemetryOptionsValidatorTest +{ + [Fact] + public void RequestLatencyOptionsValidator_BadConfig_ReturnsFail() + { + var validator = new RequestLatencyTelemetryOptionsValidator(); + var options = new RequestLatencyTelemetryOptions { LatencyDataExportTimeout = TimeSpan.FromSeconds(0) }; + + Assert.True(validator.Validate(nameof(RequestLatencyTelemetryOptions), options).Failed); + } + + [Fact] + public void RequestLatencyOptionsValidator_CoreectConfig_ReturnsSucess() + { + var validator = new RequestLatencyTelemetryOptionsValidator(); + var options = new RequestLatencyTelemetryOptions { LatencyDataExportTimeout = TimeSpan.FromSeconds(1) }; + var validationResult = validator.Validate(nameof(RequestLatencyTelemetryOptions), options); + Assert.True(validationResult.Succeeded); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/RequestLatencyTelemetryExtensionsTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/RequestLatencyTelemetryExtensionsTest.cs new file mode 100644 index 0000000000..03ae1aa3fd --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Latency/RequestLatencyTelemetryExtensionsTest.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Telemetry.Internal; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Latency; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Test; + +public class RequestLatencyTelemetryExtensionsTest +{ + [Fact] + public void RequestLatencyExtensions_NullArguments() + { + Assert.Throws(() => + RequestLatencyTelemetryExtensions.AddRequestLatencyTelemetry(null!)); + Assert.Throws(() => + RequestLatencyTelemetryExtensions.AddRequestLatencyTelemetry(new ServiceCollection(), configure: null!)); + Assert.Throws(() => + RequestLatencyTelemetryExtensions.AddRequestLatencyTelemetry(new ServiceCollection(), section: null!)); + Assert.Throws(() => + RequestLatencyTelemetryExtensions.UseRequestLatencyTelemetry(null!)); + } + + [Fact] + public void RequestLatencyExtensions_AddRequestLatency_AddsMiddleware() + { + using var serviceProvider = new ServiceCollection() + .AddLatencyContext() + .AddRequestLatencyTelemetry() + .BuildServiceProvider(); + + Assert.NotNull(serviceProvider.GetService()); + } + + [Fact] + public void RequestLatencyExtensions_AddRequestLatency_AddsLatencyContext() + { + using var serviceProvider = new ServiceCollection() + .AddLatencyContext() + .AddRequestLatencyTelemetry() + .BuildServiceProvider(); + + Assert.NotNull(serviceProvider.GetService()); + + using var scope1 = serviceProvider.CreateScope(); + using var scope2 = serviceProvider.CreateScope(); + + Assert.Equal(scope1.ServiceProvider.GetService(), + scope1.ServiceProvider.GetService()); + Assert.NotEqual(scope1.ServiceProvider.GetService(), + scope2.ServiceProvider.GetService()); + } + + [Fact] + public void RequestLatencyExtensions_AddRequestLatency_InvokesConfig() + { + bool invoked = false; + using var serviceProvider = new ServiceCollection() + .AddLatencyContext() + .AddRequestLatencyTelemetry(a => { invoked = true; }) + .BuildServiceProvider(); + + Assert.NotNull(serviceProvider.GetService()); + Assert.True(invoked); + } + + [Fact] + public void RequestLatencyExtensions_Add_BindsToConfigSection() + { + RequestLatencyTelemetryOptions expectedOptions = new() + { + LatencyDataExportTimeout = TimeSpan.FromSeconds(2) + }; + var config = GetConfigSection(expectedOptions); + + using var serviceProvider = new ServiceCollection() + .AddRequestLatencyTelemetry(config) + .BuildServiceProvider(); + + var actualOptions = serviceProvider.GetRequiredService>(); + + Assert.True(actualOptions.Value.LatencyDataExportTimeout == expectedOptions.LatencyDataExportTimeout); + } + + private static IConfigurationSection GetConfigSection(RequestLatencyTelemetryOptions options) + { + return new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { $"{nameof(RequestLatencyTelemetryOptions)}:{nameof(options.LatencyDataExportTimeout)}", options.LatencyDataExportTimeout.ToString() }, + }) + .Build() + .GetSection($"{nameof(RequestLatencyTelemetryOptions)}"); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/AcceptanceTest.Mvc.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/AcceptanceTest.Mvc.cs new file mode 100644 index 0000000000..7b44a693c3 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/AcceptanceTest.Mvc.cs @@ -0,0 +1,253 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Microsoft.Shared.Text; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test; + +public partial class AcceptanceTest +{ + private const string RedactedFormat = ""; + + private const string UserIdParamName = "userId"; + private const string NoDataClassParamName = "noDataClassification"; + private const string QueryParamName = "noRedaction"; + + internal const string ActionRouteTemplate = "api/users/{userId}/{noDataClassification}"; + internal const int ControllerProcessingTimeMs = 1_000; + + private class TestStartupWithControllers + { + [SuppressMessage("Major Code Smell", "S1144:Unused private types or members should be removed", Justification = "Used through reflection")] + public static void ConfigureServices(IServiceCollection services) + => services + .AddFakeRedaction(x => x.RedactionFormat = RedactedFormat) + .AddRouting() // Adds routing middleware. + .AddControllers(); // Allows to read routes from classes annotated with [ApiController] attribute. + + [SuppressMessage("Major Code Smell", "S1144:Unused private types or members should be removed", Justification = "Used through reflection")] + public static void Configure(IApplicationBuilder app) + => app + .UseRouting() + .UseHttpLoggingMiddleware() + .UseEndpoints(endpoints => endpoints.MapControllers()); + } + + private static Task RunControllerAsync(LogLevel level, Action configure, Func func) + => RunAsync(level, configure, func); + + [Theory] + [InlineData(HttpRouteParameterRedactionMode.Strict, $"api/users//{TelemetryConstants.Redacted}")] + [InlineData(HttpRouteParameterRedactionMode.Loose, "api/users//someTestData")] + public async Task TestServer_WhenController_RedactPath(HttpRouteParameterRedactionMode mode, string redactedPath) + { + await RunControllerAsync( + LogLevel.Information, + services => services.AddHttpLogging(o => o.RequestPathParameterRedactionMode = mode), + async (logCollector, client) => + { + const string UserId = "testUserId"; + using var response = await client.GetAsync($"/api/users/{UserId}/someTestData?{QueryParamName}=foo").ConfigureAwait(false); + Assert.True(response.IsSuccessStatusCode); + + await WaitForLogRecordsAsync(logCollector, TimeSpan.FromSeconds(30)); + + Assert.Equal(1, logCollector.Count); + + var logRecord = logCollector.LatestRecord; + Assert.Null(logRecord.Exception); + Assert.Equal(LoggingCategory, logRecord.Category); + Assert.Equal(LogLevel.Information, logRecord.Level); + + var responseStatus = ((int)response.StatusCode).ToInvariantString(); + var state = logRecord.StructuredState!; + + Assert.DoesNotContain(state, x => x.Key == QueryParamName); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Host && !string.IsNullOrEmpty(x.Value)); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Path && x.Value == redactedPath); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.StatusCode && x.Value == responseStatus); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Method && x.Value == HttpMethod.Get.ToString()); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Duration && + x.Value != null && + int.Parse(x.Value, CultureInfo.InvariantCulture) == ControllerProcessingTimeMs); + }); + } + + [Fact] + public async Task TestServer_WhenControllerWithPathRoute_RedactParameters() + { + await RunControllerAsync( + LogLevel.Information, + services => services.AddHttpLogging(x => x.RequestPathLoggingMode = IncomingPathLoggingMode.Structured), + async (logCollector, client) => + { + const string UserId = "testUserId"; + using var response = await client.GetAsync($"/api/users/{UserId}/someTestData?{QueryParamName}=foo").ConfigureAwait(false); + Assert.True(response.IsSuccessStatusCode); + + await WaitForLogRecordsAsync(logCollector, TimeSpan.FromSeconds(30)); + + Assert.Equal(1, logCollector.Count); + + var logRecord = logCollector.LatestRecord; + Assert.Null(logRecord.Exception); + Assert.Equal(LoggingCategory, logRecord.Category); + Assert.Equal(LogLevel.Information, logRecord.Level); + + var responseStatus = ((int)response.StatusCode).ToInvariantString(); + var state = logRecord.StructuredState!; + + string redactedUserId = string.Format(CultureInfo.InvariantCulture, RedactedFormat, UserId); + Assert.Single(state, x => x.Key == UserIdParamName && x.Value == redactedUserId); + Assert.Single(state, x => x.Key == NoDataClassParamName && x.Value == TelemetryConstants.Redacted); + Assert.DoesNotContain(state, x => x.Key == QueryParamName); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Host && !string.IsNullOrEmpty(x.Value)); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Path && x.Value == ActionRouteTemplate); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.StatusCode && x.Value == responseStatus); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Method && x.Value == HttpMethod.Get.ToString()); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Duration && + x.Value != null && + int.Parse(x.Value, CultureInfo.InvariantCulture) == ControllerProcessingTimeMs); + }); + } + + [Theory] + [CombinatorialData] + public async Task TestServer_WhenControllerWithPathRoute_HonorRouteParamDataClassMap(bool routeParameterRedactionModeNone) + { + await RunControllerAsync( + LogLevel.Information, + services => services.AddHttpLogging(x => + { + x.RouteParameterDataClasses.Add(new(NoDataClassParamName, DataClassification.None)); + x.RequestPathLoggingMode = IncomingPathLoggingMode.Structured; + x.RequestPathParameterRedactionMode = routeParameterRedactionModeNone + ? HttpRouteParameterRedactionMode.None : HttpRouteParameterRedactionMode.Strict; + }), + async (logCollector, client) => + { + const string UserId = "testUserId"; + const string NoDataClassParamValue = "someTestData"; + using var response = await client.GetAsync($"/api/users/{UserId}/{NoDataClassParamValue}?{QueryParamName}=foo").ConfigureAwait(false); + Assert.True(response.IsSuccessStatusCode); + + await WaitForLogRecordsAsync(logCollector, TimeSpan.FromSeconds(30)); + + Assert.Equal(1, logCollector.Count); + + var logRecord = logCollector.LatestRecord; + Assert.Null(logRecord.Exception); + Assert.Equal(LoggingCategory, logRecord.Category); + Assert.Equal(LogLevel.Information, logRecord.Level); + + var responseStatus = ((int)response.StatusCode).ToInvariantString(); + var state = logRecord.StructuredState!; + + string redactedUserId = string.Format(CultureInfo.InvariantCulture, RedactedFormat, UserId); + + if (!routeParameterRedactionModeNone) + { + Assert.Single(state, x => x.Key == UserIdParamName && x.Value == redactedUserId); + Assert.Single(state, x => x.Key == NoDataClassParamName && x.Value == NoDataClassParamValue); + } + + Assert.DoesNotContain(state, x => x.Key == QueryParamName); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Host && !string.IsNullOrEmpty(x.Value)); + var expectedPath = routeParameterRedactionModeNone ? $"/api/users/{UserId}/{NoDataClassParamValue}" : ActionRouteTemplate; + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Path && x.Value == expectedPath); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.StatusCode && x.Value == responseStatus); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Method && x.Value == HttpMethod.Get.ToString()); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Duration && + x.Value != null && + int.Parse(x.Value, CultureInfo.InvariantCulture) == ControllerProcessingTimeMs); + }); + } + + [Fact] + public async Task TestServer_WhenControllerWithPathRoute_RedactionModeNone() + { + await RunControllerAsync( + LogLevel.Information, + services => services.AddHttpLogging(x => x.RequestPathParameterRedactionMode = HttpRouteParameterRedactionMode.None), + async (logCollector, client) => + { + const string UserId = "testUserId"; + using var response = await client.GetAsync($"/api/users/{UserId}/someTestData?{QueryParamName}=foo").ConfigureAwait(false); + Assert.True(response.IsSuccessStatusCode); + + await WaitForLogRecordsAsync(logCollector, TimeSpan.FromSeconds(30)); + + Assert.Equal(1, logCollector.Count); + + var logRecord = logCollector.LatestRecord; + Assert.Null(logRecord.Exception); + Assert.Equal(LoggingCategory, logRecord.Category); + Assert.Equal(LogLevel.Information, logRecord.Level); + + var responseStatus = ((int)response.StatusCode).ToInvariantString(); + var state = logRecord.StructuredState!; + + Assert.DoesNotContain(state, x => x.Key == QueryParamName); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Host && !string.IsNullOrEmpty(x.Value)); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Path && x.Value == $"/api/users/testUserId/someTestData"); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.StatusCode && x.Value == responseStatus); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Method && x.Value == HttpMethod.Get.ToString()); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Duration && + x.Value != null && + int.Parse(x.Value, CultureInfo.InvariantCulture) == ControllerProcessingTimeMs); + }); + } + + [Theory] + [CombinatorialData] + public async Task TestServer_WhenControllerWithoutPathRoute_LogPath(bool routeParameterRedactionModeNone) + { + const string RequestPath = $"/api/test/1/2/3"; + + await RunControllerAsync( + LogLevel.Information, + services => services.AddHttpLogging(x => + { + x.RequestPathParameterRedactionMode = routeParameterRedactionModeNone + ? HttpRouteParameterRedactionMode.None : HttpRouteParameterRedactionMode.Strict; + }), + async (logCollector, client) => + { + using var response = await client.GetAsync(RequestPath).ConfigureAwait(false); + Assert.False(response.IsSuccessStatusCode); + + await WaitForLogRecordsAsync(logCollector, TimeSpan.FromSeconds(30)); + + Assert.Equal(1, logCollector.Count); + + var logRecord = logCollector.LatestRecord; + Assert.Null(logRecord.Exception); + Assert.Equal(LoggingCategory, logRecord.Category); + Assert.Equal(LogLevel.Information, logRecord.Level); + + var responseStatus = ((int)response.StatusCode).ToInvariantString(); + var state = logRecord.StructuredState!; + + var expectedPath = routeParameterRedactionModeNone ? RequestPath : TelemetryConstants.Unknown; + + Assert.DoesNotContain(state, x => x.Key == QueryParamName); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Host && !string.IsNullOrEmpty(x.Value)); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Path && x.Value == expectedPath); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.StatusCode && x.Value == responseStatus); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Method && x.Value == HttpMethod.Get.ToString()); + }); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/AcceptanceTest.Routing.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/AcceptanceTest.Routing.cs new file mode 100644 index 0000000000..318f04deff --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/AcceptanceTest.Routing.cs @@ -0,0 +1,196 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Telemetry.Http.Logging.Test.Controllers; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Text; +using Xunit; +using static Microsoft.Extensions.Http.Telemetry.HttpRouteParameterRedactionMode; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test; + +public partial class AcceptanceTest +{ + private class TestStartupWithRouting + { + [SuppressMessage("Major Code Smell", "S1144:Unused private types or members should be removed", Justification = "Used through reflection")] + public static void ConfigureServices(IServiceCollection services) + => services + .AddFakeRedaction(x => x.RedactionFormat = RedactedFormat) + .AddRouting() + .AddControllers(); + + [SuppressMessage("Major Code Smell", "S1144:Unused private types or members should be removed", Justification = "Used through reflection")] + public static void Configure(IApplicationBuilder app) + => app + .UseRouting() + .UseHttpLoggingMiddleware() + .UseEndpoints(endpoints => + { + endpoints.MapControllers(); + + endpoints.MapControllerRoute( + name: "default", + pattern: ConventionalRoutingController.Route); + + endpoints.MapControllerRoute( + name: "mixed-routing", + pattern: "mixed/conventional-routing", + defaults: new { controller = "MixedRouting", action = "ConventionalRouting" }); + }); + } + + private static Task RunRoutingTestAsync( + string httpPath, + Action configureHttpLogging, + Action> validateRequestState) + where TStartup : class + { + return RunAsync( + LogLevel.Information, + configureHttpLogging, + async (logCollector, client) => + { + using var response = await client.GetAsync(httpPath).ConfigureAwait(false); + Assert.True(response.IsSuccessStatusCode); + + await WaitForLogRecordsAsync(logCollector, TimeSpan.FromSeconds(30)); + + Assert.Equal(1, logCollector.Count); + + var logRecord = logCollector.LatestRecord; + Assert.Null(logRecord.Exception); + Assert.Equal(LoggingCategory, logRecord.Category); + Assert.Equal(LogLevel.Information, logRecord.Level); + + var responseStatus = ((int)response.StatusCode).ToInvariantString(); + var state = logRecord.StructuredState!; + validateRequestState(new Dictionary(state)); + }); + } + + [Theory] + + // Conventional routing. + [InlineData(Strict, "", "")] + [InlineData(Loose, "", "")] + [InlineData(Strict, "ConventionalRouting", "ConventionalRouting")] + [InlineData(Loose, "ConventionalRouting", "ConventionalRouting")] + [InlineData(Strict, "ConventionalRouting/GetEntity/12345", "ConventionalRouting/GetEntity/")] + [InlineData(Loose, "ConventionalRouting/GetEntity/12345", "ConventionalRouting/GetEntity/")] + [InlineData(Strict, "ConventionalRouting/GetData/12345", $"ConventionalRouting/GetData/{TelemetryConstants.Redacted}")] + [InlineData(Loose, "ConventionalRouting/GetData/12345", "ConventionalRouting/GetData/12345")] + + // Attribute routing. + [InlineData(Strict, "AttributeRouting", "AttributeRouting")] + [InlineData(Loose, "AttributeRouting", "AttributeRouting")] + [InlineData(Strict, "AttributeRouting/all", "AttributeRouting/all")] + [InlineData(Loose, "AttributeRouting/all", "AttributeRouting/all")] + [InlineData(Strict, "AttributeRouting/get-1/12345", "AttributeRouting/get-1/")] + [InlineData(Loose, "AttributeRouting/get-1/12345", "AttributeRouting/get-1/")] + [InlineData(Strict, "AttributeRouting/get-2", "AttributeRouting/get-2")] + [InlineData(Loose, "AttributeRouting/get-2", "AttributeRouting/get-2")] + [InlineData(Strict, "AttributeRouting/get-2/12345", "AttributeRouting/get-2/")] + [InlineData(Loose, "AttributeRouting/get-2/12345", "AttributeRouting/get-2/")] + [InlineData(Strict, "AttributeRouting/get-3", "AttributeRouting/get-3")] + [InlineData(Loose, "AttributeRouting/get-3", "AttributeRouting/get-3")] + [InlineData(Strict, "AttributeRouting/get-3/top10", "AttributeRouting/get-3/top10")] + [InlineData(Loose, "AttributeRouting/get-3/top10", "AttributeRouting/get-3/top10")] + [InlineData(Strict, "AttributeRouting/get-4/top10", $"AttributeRouting/get-4/{TelemetryConstants.Redacted}")] + [InlineData(Loose, "AttributeRouting/get-4/top10", "AttributeRouting/get-4/top10")] + + // Mixed routing. + [InlineData(Strict, "mixed/conventional-routing", "mixed/conventional-routing")] + [InlineData(Loose, "mixed/conventional-routing", "mixed/conventional-routing")] + [InlineData(Strict, "mixed/attribute-routing-1/12345", "mixed/attribute-routing-1/")] + [InlineData(Loose, "mixed/attribute-routing-1/12345", "mixed/attribute-routing-1/")] + [InlineData(Strict, "mixed/attribute-routing-2", "mixed/attribute-routing-2")] + [InlineData(Loose, "mixed/attribute-routing-2", "mixed/attribute-routing-2")] + [InlineData(Strict, "mixed/attribute-routing-2/12345", "mixed/attribute-routing-2/")] + [InlineData(Loose, "mixed/attribute-routing-2/12345", "mixed/attribute-routing-2/")] + [InlineData(Strict, "mixed/attribute-routing-3", "mixed/attribute-routing-3")] + [InlineData(Loose, "mixed/attribute-routing-3", "mixed/attribute-routing-3")] + [InlineData(Strict, "mixed/attribute-routing-3/top10", "mixed/attribute-routing-3/top10")] + [InlineData(Loose, "mixed/attribute-routing-3/top10", "mixed/attribute-routing-3/top10")] + [InlineData(Strict, "mixed/attribute-routing-4/test1234", $"mixed/attribute-routing-4/{TelemetryConstants.Redacted}")] + [InlineData(Loose, "mixed/attribute-routing-4/test1234", "mixed/attribute-routing-4/test1234")] + public async Task Routing_WithFormattedPath_RedactPath( + HttpRouteParameterRedactionMode mode, string httpPath, string expectedHttpPath) + { + await RunRoutingTestAsync( + httpPath, + configureHttpLogging: services => + { + services.AddHttpLogging(o => o.RequestPathParameterRedactionMode = mode); + }, + validateRequestState: state => + { + Assert.Equal(expectedHttpPath, state[HttpLoggingDimensions.Path]); + }); + } + + [Theory] + + // Conventional routing. + [InlineData("", ConventionalRoutingController.Route, "ConventionalRouting", "Index", "")] + [InlineData("ConventionalRouting", ConventionalRoutingController.Route, "ConventionalRouting", "Index", "")] + [InlineData("ConventionalRouting/GetEntity/12345", ConventionalRoutingController.Route, "ConventionalRouting", "GetEntity", "")] + + // Attribute routing. + [InlineData("AttributeRouting", "AttributeRouting", null, null, null)] + [InlineData("AttributeRouting/all", "AttributeRouting/all", null, null, null)] + [InlineData("AttributeRouting/get-1/12345", "AttributeRouting/get-1/{param:int:min(1)}", null, null, "")] + [InlineData("AttributeRouting/get-2", "AttributeRouting/get-2/{param?}", null, null, "")] + [InlineData("AttributeRouting/get-2/12345", "AttributeRouting/get-2/{param?}", null, null, "")] + [InlineData("AttributeRouting/get-3", "AttributeRouting/get-3/{param=all}", null, null, "all")] + [InlineData("AttributeRouting/get-3/top10", "AttributeRouting/get-3/{param=all}", null, null, "top10")] + + // Mixed routing. + [InlineData("mixed/conventional-routing", "mixed/conventional-routing", null, null, null)] + [InlineData("mixed/attribute-routing-1/12345", "mixed/attribute-routing-1/{param:int:min(1)}", null, null, "")] + [InlineData("mixed/attribute-routing-2", "mixed/attribute-routing-2/{param?}", null, null, "")] + [InlineData("mixed/attribute-routing-2/12345", "mixed/attribute-routing-2/{param?}", null, null, "")] + [InlineData("mixed/attribute-routing-3", "mixed/attribute-routing-3/{param=all}", null, null, "all")] + [InlineData("mixed/attribute-routing-3/top10", "mixed/attribute-routing-3/{param=all}", null, null, "top10")] + public async Task Routing_WithStructuredPath_RedactParameters( + string httpPath, string httpRoute, string? controller, string? action, string? param) + { + await RunRoutingTestAsync( + httpPath, + configureHttpLogging: services => services.AddHttpLogging(options => + { + options.RequestPathLoggingMode = IncomingPathLoggingMode.Structured; + }), + validateRequestState: state => + { + Assert.Equal(httpRoute, state[HttpLoggingDimensions.Path]); + + if (controller != null) + { + Assert.Equal(controller, state["controller"]); + } + + if (action != null) + { + Assert.Equal(action, state["action"]); + } + + if (param == null) + { + Assert.DoesNotContain("param", state.Keys); + } + else + { + Assert.Equal(param, state["param"]); + } + }); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/AcceptanceTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/AcceptanceTest.cs new file mode 100644 index 0000000000..7dd3324373 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/AcceptanceTest.cs @@ -0,0 +1,878 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Telemetry.Http.Logging.Test.Internal; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Microsoft.Extensions.Time.Testing; +using Microsoft.Net.Http.Headers; +using Microsoft.Shared.Text; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test; + +public partial class AcceptanceTest +{ + private const string LoggingCategory = "Microsoft.AspNetCore.Telemetry.Http.Logging.HttpLoggingMiddleware"; + private const int ErrorRouteProcessingTimeMs = 1_000; + private const int SlashRouteProcessingTimeMs = 2_000; + private static readonly TimeSpan _defaultLogTimeout = TimeSpan.FromSeconds(5); + + private class TestStartup + { + [SuppressMessage("Major Code Smell", "S1144:Unused private types or members should be removed", Justification = "Used through reflection")] + public static void ConfigureServices(IServiceCollection services) + { + services.AddRouting(); + services.AddRedaction(x => + { + x.SetFallbackRedactor(); + x.SetRedactor(SimpleClassifications.PublicData); + }); + services.AddHttpLogging(); + } + + [SuppressMessage("Major Code Smell", "S1144:Unused private types or members should be removed", Justification = "Used through reflection")] + public static void Configure(IApplicationBuilder app) + { + app.UseRouting(); + app.UseMiddleware(); + app.UseHttpLoggingMiddleware(); + + app.Map("/error", static x => + x.Run(static async context => + { + if (context.Request.QueryString.HasValue) + { + if (context.Request.QueryString.Value!.Contains("status")) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + } + + if (context.Request.QueryString.Value!.Contains("body")) + { + context.Response.ContentType = MediaTypeNames.Text.Plain; + await context.Response.WriteAsync("test body"); + } + } + + var middleware = context.RequestServices.GetRequiredService(); + var fakeTimeProvider = (FakeTimeProvider)middleware.TimeProvider; + fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(ErrorRouteProcessingTimeMs)); + throw new InvalidOperationException("Test exception"); + })); + + app.Run(static async context => + { + var middleware = context.RequestServices.GetRequiredService(); + var fakeTimeProvider = (FakeTimeProvider)middleware.TimeProvider; + fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(SlashRouteProcessingTimeMs)); + + context.Response.ContentType = MediaTypeNames.Text.Plain; + context.Response.Headers.Add(HeaderNames.TransferEncoding, "chunked"); + await context.Response.WriteAsync("Server: hello!").ConfigureAwait(false); + + // Writing response twice so header is sent as 'transfer chunked-encoding' + await context.Response.WriteAsync("Server: world!").ConfigureAwait(false); + }); + } + } + + private static Task RunAsync(LogLevel level, Action configure, Func func) + => RunAsync(level, configure, func); + + private static async Task RunAsync( + LogLevel level, + Action configure, + Func func, + Action? configureMiddleware = null) + where TStartup : class + { + using var host = await FakeHost.CreateBuilder(options => options.FakeLogging = false) + .ConfigureLogging(loggingBuilder => loggingBuilder + .AddFilter("Microsoft.Hosting", LogLevel.Warning) + .AddFilter("Microsoft.AspNetCore", LogLevel.Warning) + .AddFilter("Microsoft.AspNetCore.Telemetry", level) + .SetMinimumLevel(level) + .AddFakeLogging()) + .ConfigureServices(x => x.AddSingleton()) + .ConfigureServices(configure) + .ConfigureWebHost(static builder => builder + .UseStartup() + .UseTestServer()) + .StartAsync(); + + var logCollector = host.Services.GetFakeLogCollector(); + var fakeClock = new FakeTimeProvider(); + var middleware = host.Services.GetRequiredService(); + middleware.TimeProvider = fakeClock; + configureMiddleware?.Invoke(middleware); + + using var client = host.GetTestClient(); + + await func(logCollector, client).ConfigureAwait(false); + await host.StopAsync(); + } + + private static async Task WaitForLogRecordsAsync(FakeLogCollector logCollector, TimeSpan timeout, int expectedRecords = 1) + { + var totalTimeWaiting = TimeSpan.Zero; + var spinTime = TimeSpan.FromMilliseconds(50); + while (totalTimeWaiting < timeout) + { + if (logCollector.Count >= expectedRecords) + { + return; + } + + await Task.Delay(spinTime); + totalTimeWaiting += spinTime; + } + + throw new TimeoutException("No log records were emitted, timeout was reached"); + } + + [Theory] + [InlineData(MediaTypeNames.Text.Plain, true)] + [InlineData(MediaTypeNames.Text.Html, false)] + [InlineData(MediaTypeNames.Text.RichText, false)] + [InlineData(MediaTypeNames.Text.Xml, false)] + public async Task HttpLogging_WhenLogLevelInfo_LogResponseBody(string responseContentTypeToLog, bool shouldLog) + { + await RunAsync( + LogLevel.Information, + services => services.AddHttpLogging(x => + { + x.ResponseBodyContentTypes.Add(responseContentTypeToLog); + x.LogBody = true; + }), + async (logCollector, client) => + { + const string Content = "Client: hello!"; + + using var content = new StringContent(Content); + using var response = await client.PostAsync("/", content).ConfigureAwait(false); + Assert.True(response.IsSuccessStatusCode); + + await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout); + + Assert.Equal(1, logCollector.Count); + Assert.Null(logCollector.LatestRecord.Exception); + Assert.Equal(LogLevel.Information, logCollector.LatestRecord.Level); + Assert.Equal(LoggingCategory, logCollector.LatestRecord.Category); + + var responseStatus = ((int)response.StatusCode).ToInvariantString(); + var state = logCollector.LatestRecord.StructuredState!; + + Assert.DoesNotContain(state, x => x.Key == HttpLoggingDimensions.RequestBody); + Assert.DoesNotContain(state, x => x.Key.StartsWith(HttpLoggingDimensions.RequestHeaderPrefix)); + Assert.DoesNotContain(state, x => x.Key.StartsWith(HttpLoggingDimensions.ResponseHeaderPrefix)); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Host && !string.IsNullOrEmpty(x.Value)); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Path && x.Value == TelemetryConstants.Unknown); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.StatusCode && x.Value == responseStatus); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Method && x.Value == HttpMethod.Post.ToString()); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Duration && + x.Value != null && + int.Parse(x.Value, CultureInfo.InvariantCulture) == SlashRouteProcessingTimeMs); + + if (shouldLog) + { + Assert.Single(state, x => x.Key == HttpLoggingDimensions.ResponseBody && x.Value == "Server: hello!Server: world!"); + Assert.Equal(6, state!.Count); + } + else + { + Assert.DoesNotContain(state, x => x.Key == HttpLoggingDimensions.ResponseBody); + Assert.Equal(5, state!.Count); + } + }); + } + + [Theory] + [InlineData(MediaTypeNames.Text.Plain, true)] + [InlineData(MediaTypeNames.Text.Html, true)] + [InlineData(MediaTypeNames.Text.RichText, true)] + [InlineData(MediaTypeNames.Text.Xml, true)] + [InlineData(MediaTypeNames.Application.Json, false)] + [InlineData(MediaTypeNames.Application.Xml, false)] + [InlineData(MediaTypeNames.Image.Jpeg, false)] + public async Task HttpLogging_WhenLogLevelInfo_LogRequestBody(string requestContentType, bool shouldLog) + { + await RunAsync( + LogLevel.Information, + services => services.AddHttpLogging(x => + { + x.RequestBodyContentTypes.Add("text/*"); + x.LogBody = true; + }), + async (logCollector, client) => + { + const string Content = "Client: hello!"; + + using var content = new StringContent(Content, null, requestContentType); + using var response = await client.PostAsync("/", content).ConfigureAwait(false); + Assert.True(response.IsSuccessStatusCode); + + await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout); + + Assert.Equal(1, logCollector.Count); + Assert.Null(logCollector.LatestRecord.Exception); + Assert.Equal(LogLevel.Information, logCollector.LatestRecord.Level); + Assert.Equal(LoggingCategory, logCollector.LatestRecord.Category); + + var responseStatus = ((int)response.StatusCode).ToInvariantString(); + var state = logCollector.LatestRecord.StructuredState!; + + Assert.DoesNotContain(state, x => x.Key == HttpLoggingDimensions.ResponseBody); + Assert.DoesNotContain(state, x => x.Key.StartsWith(HttpLoggingDimensions.RequestHeaderPrefix)); + Assert.DoesNotContain(state, x => x.Key.StartsWith(HttpLoggingDimensions.ResponseHeaderPrefix)); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Host && !string.IsNullOrEmpty(x.Value)); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Path && x.Value == TelemetryConstants.Unknown); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.StatusCode && x.Value == responseStatus); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Method && x.Value == HttpMethod.Post.ToString()); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Duration && + x.Value != null && + int.Parse(x.Value, CultureInfo.InvariantCulture) == SlashRouteProcessingTimeMs); + + if (shouldLog) + { + Assert.Single(state, x => x.Key == HttpLoggingDimensions.RequestBody && x.Value == Content); + Assert.Equal(6, state!.Count); + } + else + { + Assert.DoesNotContain(state, x => x.Key == HttpLoggingDimensions.RequestBody); + Assert.Equal(5, state!.Count); + } + }); + } + + [Fact] + public async Task HttpLogging_WhenMultiSegmentRequestPipe_LogRequestBody() + { + await RunAsync( + LogLevel.Information, + services => services.AddHttpLogging(x => + { + x.RequestBodyContentTypes.Add("text/*"); + x.LogBody = true; + }), + async (logCollector, client) => + { + const string Content = "Whatever..."; + + using var content = new StringContent(Content, null, MediaTypeNames.Text.Plain); + using var response = await client.PostAsync("/multi-segment-pipe", content).ConfigureAwait(false); + Assert.True(response.IsSuccessStatusCode); + + await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout); + + Assert.Equal(1, logCollector.Count); + Assert.Null(logCollector.LatestRecord.Exception); + Assert.Equal(LogLevel.Information, logCollector.LatestRecord.Level); + Assert.Equal(LoggingCategory, logCollector.LatestRecord.Category); + + var responseStatus = ((int)response.StatusCode).ToInvariantString(); + var state = logCollector.LatestRecord.StructuredState; + + Assert.Equal(6, state!.Count); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.StatusCode && x.Value == responseStatus); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Method && x.Value == HttpMethod.Post.ToString()); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.RequestBody && x.Value == "Test Segment"); + }); + } + + [Fact] + public async Task HttpLogging_WhenLogLevelInfo_LogRequestStart() + { + await RunAsync( + LogLevel.Information, + static services => services.AddHttpLogging(static x => + { + x.LogRequestStart = true; + x.LogBody = true; + x.RequestHeadersDataClasses.Add(HeaderNames.Accept, DataClassification.None); + x.ResponseHeadersDataClasses.Add(HeaderNames.TransferEncoding, DataClassification.None); + x.RequestBodyContentTypes.Add(MediaTypeNames.Text.Plain); + x.ResponseBodyContentTypes.Add(MediaTypeNames.Text.Plain); + }), + async static (logCollector, client) => + { + const string Content = "Client: hello!"; + + using var request = new HttpRequestMessage(HttpMethod.Post, "/") + { + Content = new StringContent(Content) + }; + + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + using var response = await client.SendAsync(request).ConfigureAwait(false); + Assert.True(response.IsSuccessStatusCode); + + await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout, expectedRecords: 2); + + var logRecords = logCollector.GetSnapshot(); + Assert.Equal(2, logRecords.Count); + Assert.All(logRecords, x => Assert.Null(x.Exception)); + Assert.All(logRecords, x => Assert.Equal(LogLevel.Information, x.Level)); + Assert.All(logRecords, x => Assert.Equal(LoggingCategory, x.Category)); + + var responseStatus = ((int)response.StatusCode).ToInvariantString(); + var firstState = logRecords[0].StructuredState; + var secondState = logRecords[1].StructuredState; + + Assert.Equal(5, firstState!.Count); + Assert.DoesNotContain(firstState, x => x.Key == HttpLoggingDimensions.ResponseBody); + Assert.DoesNotContain(firstState, x => x.Key.StartsWith(HttpLoggingDimensions.ResponseHeaderPrefix)); + Assert.DoesNotContain(firstState, x => x.Key == HttpLoggingDimensions.StatusCode); + Assert.DoesNotContain(firstState, x => x.Key == HttpLoggingDimensions.Duration); + Assert.Single(firstState, x => x.Key == HttpLoggingDimensions.RequestBody && x.Value == Content); + Assert.Single(firstState, x => x.Key == HttpLoggingDimensions.RequestHeaderPrefix + HeaderNames.Accept); + Assert.Single(firstState, x => x.Key == HttpLoggingDimensions.Host && !string.IsNullOrEmpty(x.Value)); + Assert.Single(firstState, x => x.Key == HttpLoggingDimensions.Path && x.Value == TelemetryConstants.Unknown); + Assert.Single(firstState, x => x.Key == HttpLoggingDimensions.Method && x.Value == HttpMethod.Post.ToString()); + + Assert.Equal(9, secondState!.Count); + Assert.Single(secondState, x => x.Key == HttpLoggingDimensions.RequestHeaderPrefix + HeaderNames.Accept); + Assert.Single(secondState, x => x.Key == HttpLoggingDimensions.ResponseHeaderPrefix + HeaderNames.TransferEncoding); + Assert.Single(secondState, x => x.Key == HttpLoggingDimensions.RequestBody && x.Value == Content); + Assert.Single(secondState, x => x.Key == HttpLoggingDimensions.ResponseBody && x.Value == "Server: hello!Server: world!"); + Assert.Single(secondState, x => x.Key == HttpLoggingDimensions.Host && !string.IsNullOrEmpty(x.Value)); + Assert.Single(secondState, x => x.Key == HttpLoggingDimensions.Path && x.Value == TelemetryConstants.Unknown); + Assert.Single(secondState, x => x.Key == HttpLoggingDimensions.StatusCode && x.Value == responseStatus); + Assert.Single(secondState, x => x.Key == HttpLoggingDimensions.Method && x.Value == HttpMethod.Post.ToString()); + Assert.Single(secondState, x => x.Key == HttpLoggingDimensions.Duration && x.Value != null); + }); + } + + [Fact] + public async Task HttpLogging_WhenLogLevelInfo_LogHeaders() + { + await RunAsync( + LogLevel.Information, + static services => services.AddHttpLogging(static x => + { + x.RequestHeadersDataClasses.Add(HeaderNames.Accept, DataClassification.None); + x.ResponseHeadersDataClasses.Add(HeaderNames.TransferEncoding, DataClassification.None); + }), + async static (logCollector, client) => + { + using var httpMessage = new HttpRequestMessage(HttpMethod.Get, "/"); + httpMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + + using var response = await client.SendAsync(httpMessage).ConfigureAwait(false); + Assert.True(response.IsSuccessStatusCode); + + await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout); + + Assert.Equal(1, logCollector.Count); + + var lastRecord = logCollector.LatestRecord; + Assert.Null(lastRecord.Exception); + Assert.Equal(LogLevel.Information, lastRecord.Level); + Assert.Equal(LoggingCategory, lastRecord.Category); + + var responseStatus = ((int)response.StatusCode).ToInvariantString(); + var state = lastRecord.StructuredState; + + Assert.Equal(7, state!.Count); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.RequestHeaderPrefix + HeaderNames.Accept); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.ResponseHeaderPrefix + HeaderNames.TransferEncoding); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Host && !string.IsNullOrEmpty(x.Value)); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Path && x.Value == TelemetryConstants.Unknown); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.StatusCode && x.Value == responseStatus); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Duration && + x.Value != null && + int.Parse(x.Value, CultureInfo.InvariantCulture) == SlashRouteProcessingTimeMs); + + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Method && x.Value == HttpMethod.Get.ToString()); + Assert.DoesNotContain(state, x => x.Key == HttpLoggingDimensions.RequestBody); + Assert.DoesNotContain(state, x => x.Key == HttpLoggingDimensions.ResponseBody); + Assert.DoesNotContain(state, x => + x.Key.StartsWith(HttpLoggingDimensions.RequestHeaderPrefix) && !x.Key.EndsWith(HeaderNames.Accept)); + + Assert.DoesNotContain(state, x => + x.Key.StartsWith(HttpLoggingDimensions.ResponseHeaderPrefix) && !x.Key.EndsWith(HeaderNames.TransferEncoding)); + }); + } + + [Fact] + public async Task HttpLogging_WhenEnricherAdded_LogAdditionalProps() + { + await RunAsync( + LogLevel.Information, + static x => + { + x.AddHttpLogEnricher(); + x.AddHttpLogging(); + }, + async static (logCollector, client) => + { + using var response = await client.DeleteAsync("/").ConfigureAwait(false); + Assert.True(response.IsSuccessStatusCode); + + await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout); + + Assert.Equal(1, logCollector.Count); + Assert.Null(logCollector.LatestRecord.Exception); + Assert.Equal(LogLevel.Information, logCollector.LatestRecord.Level); + Assert.Equal(LoggingCategory, logCollector.LatestRecord.Category); + + var responseStatus = ((int)response.StatusCode).ToInvariantString(); + var state = logCollector.LatestRecord.StructuredState; + + Assert.Equal(7, state!.Count); + Assert.Single(state, x => x.Key == TestHttpLogEnricher.Key1 && x.Value == TestHttpLogEnricher.Value1); + Assert.Single(state, x => x.Key == TestHttpLogEnricher.Key2 && x.Value == TestHttpLogEnricher.Value2.ToString(CultureInfo.CurrentCulture)); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Method && x.Value == HttpMethod.Delete.ToString()); + Assert.DoesNotContain(state, x => x.Key == HttpLoggingDimensions.RequestBody); + Assert.DoesNotContain(state, x => x.Key == HttpLoggingDimensions.ResponseBody); + Assert.DoesNotContain(state, x => x.Key.StartsWith(HttpLoggingDimensions.RequestHeaderPrefix)); + Assert.DoesNotContain(state, x => x.Key.StartsWith(HttpLoggingDimensions.ResponseHeaderPrefix)); + }); + } + + [Theory] + [InlineData(IncomingPathLoggingMode.Structured)] + [InlineData(IncomingPathLoggingMode.Formatted)] + public async Task HttpLogging_WhenRedactionModeNone_LogIncomingRequestPath(IncomingPathLoggingMode pathLoggingMode) + { + await RunAsync( + LogLevel.Information, + x => + { + x.AddHttpLogging(options => + { + options.RequestPathParameterRedactionMode = HttpRouteParameterRedactionMode.None; + options.RequestPathLoggingMode = pathLoggingMode; + }); + }, + async static (logCollector, client) => + { + const string RequestPath = "/api/users/123/add-task/345"; + using var response = await client.GetAsync(RequestPath).ConfigureAwait(false); + Assert.True(response.IsSuccessStatusCode); + + await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout); + + Assert.Equal(1, logCollector.Count); + Assert.Null(logCollector.LatestRecord.Exception); + Assert.Equal(LogLevel.Information, logCollector.LatestRecord.Level); + Assert.Equal(LoggingCategory, logCollector.LatestRecord.Category); + + var responseStatus = ((int)response.StatusCode).ToInvariantString(); + var state = logCollector.LatestRecord.StructuredState; + + Assert.Equal(5, state!.Count); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Path && x.Value == RequestPath); + }); + } + + [Fact] + public async Task HttpLogging_WhenLogRequestStart_SkipEnrichingFirstLogRecord() + { + await RunAsync( + LogLevel.Information, + static x => + { + x.AddHttpLogEnricher(); + x.AddHttpLogging(x => x.LogRequestStart = true); + }, + async static (logCollector, client) => + { + using var response = await client.DeleteAsync("/").ConfigureAwait(false); + Assert.True(response.IsSuccessStatusCode); + + await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout, expectedRecords: 2); + + var logRecords = logCollector.GetSnapshot(); + Assert.Equal(2, logRecords.Count); + Assert.All(logRecords, x => Assert.Null(x.Exception)); + Assert.All(logRecords, x => Assert.Equal(LogLevel.Information, x.Level)); + Assert.All(logRecords, x => Assert.Equal(LoggingCategory, x.Category)); + + var responseStatus = ((int)response.StatusCode).ToInvariantString(); + var firstState = logRecords[0].StructuredState; + var secondState = logRecords[1].StructuredState; + + Assert.Equal(3, firstState!.Count); + Assert.DoesNotContain(firstState, x => x.Key == TestHttpLogEnricher.Key1 && x.Value == TestHttpLogEnricher.Value1); + Assert.DoesNotContain(firstState, x => x.Key == TestHttpLogEnricher.Key2 && x.Value == TestHttpLogEnricher.Value2.ToString(CultureInfo.CurrentCulture)); + + Assert.Equal(7, secondState!.Count); + Assert.Single(secondState, x => x.Key == TestHttpLogEnricher.Key1 && x.Value == TestHttpLogEnricher.Value1); + Assert.Single(secondState, x => x.Key == TestHttpLogEnricher.Key2 && x.Value == TestHttpLogEnricher.Value2.ToString(CultureInfo.CurrentCulture)); + }); + } + + [Fact] + public async Task HttpLogging_WhenSecondLogRequestStart_DontLogDurationAndStatus() + { + await RunAsync( + LogLevel.Information, + static x => x.AddHttpLogging(x => x.LogRequestStart = true), + async static (logCollector, client) => + { + using var firstResponse = await client.DeleteAsync("/").ConfigureAwait(false); + Assert.True(firstResponse.IsSuccessStatusCode); + + await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout, expectedRecords: 2); + + using var secondResponse = await client.DeleteAsync("/").ConfigureAwait(false); + Assert.True(secondResponse.IsSuccessStatusCode); + + await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout, expectedRecords: 4); + + var logRecords = logCollector.GetSnapshot(); + Assert.Equal(4, logRecords.Count); + Assert.All(logRecords, x => Assert.Null(x.Exception)); + Assert.All(logRecords, x => Assert.Equal(LogLevel.Information, x.Level)); + Assert.All(logRecords, x => Assert.Equal(LoggingCategory, x.Category)); + + var responseStatus = ((int)firstResponse.StatusCode).ToInvariantString(); + var firstRecord = logRecords[0].StructuredState; + var secondRecord = logRecords[1].StructuredState; + var thirdRecord = logRecords[2].StructuredState; + var fourthRecord = logRecords[3].StructuredState; + + Assert.Equal(3, firstRecord!.Count); + Assert.Equal(3, thirdRecord!.Count); + Assert.DoesNotContain(firstRecord, x => x.Key == HttpLoggingDimensions.StatusCode); + Assert.DoesNotContain(firstRecord, x => x.Key == HttpLoggingDimensions.Duration); + Assert.DoesNotContain(thirdRecord, x => x.Key == HttpLoggingDimensions.StatusCode); + Assert.DoesNotContain(thirdRecord, x => x.Key == HttpLoggingDimensions.Duration); + + Assert.Equal(5, secondRecord!.Count); + Assert.Equal(5, fourthRecord!.Count); + Assert.Single(secondRecord, x => x.Key == HttpLoggingDimensions.StatusCode && x.Value == responseStatus); + Assert.Single(secondRecord, x => x.Key == HttpLoggingDimensions.Duration && x.Value != null); + Assert.Single(fourthRecord, x => x.Key == HttpLoggingDimensions.StatusCode && x.Value == responseStatus); + Assert.Single(fourthRecord, x => x.Key == HttpLoggingDimensions.Duration && x.Value != null); + }); + } + + [Theory] + [InlineData("/error", "0")] + [InlineData("/error?status=1", "400")] + public async Task HttpLogging_WhenException_LogError(string requestPath, string expectedStatus) + { + await RunAsync( + LogLevel.Information, + static services => services.AddHttpLogging(), + async (logCollector, client) => + { + var ex = await Assert.ThrowsAsync(() => client.GetAsync(requestPath)); + Assert.Equal("Test exception", ex.Message); + + Assert.Equal(1, logCollector.Count); + Assert.Equal(LogLevel.Error, logCollector.LatestRecord.Level); + Assert.Equal(LoggingCategory, logCollector.LatestRecord.Category); + Assert.NotNull(logCollector.LatestRecord.Exception); + Assert.Same(ex, logCollector.LatestRecord.Exception); + + var state = logCollector.LatestRecord.StructuredState; + + Assert.Equal(5, state!.Count); + Assert.DoesNotContain(state, x => x.Key.StartsWith(HttpLoggingDimensions.RequestHeaderPrefix)); + Assert.DoesNotContain(state, x => x.Key.StartsWith(HttpLoggingDimensions.ResponseHeaderPrefix)); + Assert.DoesNotContain(state, x => x.Key == HttpLoggingDimensions.RequestBody); + Assert.DoesNotContain(state, x => x.Key == HttpLoggingDimensions.ResponseBody); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Method && x.Value == HttpMethod.Get.ToString()); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Host && !string.IsNullOrEmpty(x.Value)); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Path && x.Value == TelemetryConstants.Unknown); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.StatusCode && x.Value == expectedStatus); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Duration && + x.Value != null && + int.Parse(x.Value, CultureInfo.InvariantCulture) == ErrorRouteProcessingTimeMs); + }); + } + + [Fact] + public async Task HttpLogging_WhenException_LogBody() + { + await RunAsync( + LogLevel.Information, + static services => services.AddHttpLogging(static x => + { + x.RequestBodyContentTypes.Add(MediaTypeNames.Text.Plain); + x.ResponseBodyContentTypes.Add(MediaTypeNames.Text.Plain); + x.LogBody = true; + }), + async (logCollector, client) => + { + const string Content = "Client: hello!"; + + using var content = new StringContent(Content); + var ex = await Assert.ThrowsAsync(() => client.PutAsync("/error?body=true", content)); + + var originalException = ex.InnerException?.InnerException; + Assert.NotNull(originalException); + Assert.Equal("Test exception", originalException!.Message); + + Assert.Equal(1, logCollector.Count); + Assert.Equal(LogLevel.Error, logCollector.LatestRecord.Level); + Assert.Equal(LoggingCategory, logCollector.LatestRecord.Category); + Assert.NotNull(logCollector.LatestRecord.Exception); + Assert.Same(originalException, logCollector.LatestRecord.Exception); + + var state = logCollector.LatestRecord.StructuredState; + + Assert.Equal(7, state!.Count); + Assert.DoesNotContain(state, x => x.Key.StartsWith(HttpLoggingDimensions.RequestHeaderPrefix)); + Assert.DoesNotContain(state, x => x.Key.StartsWith(HttpLoggingDimensions.ResponseHeaderPrefix)); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Method && x.Value == HttpMethod.Put.ToString()); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.RequestBody && x.Value == Content); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.ResponseBody && x.Value == "test body"); + }); + } + + [Fact] + public async Task HttpLogging_WhenException_DontLogResponseBody() + { + await RunAsync( + LogLevel.Information, + static services => services.AddHttpLogging(static x => x.LogBody = false), + async (logCollector, client) => + { + const string Content = "Client: hello!"; + + using var content = new StringContent(Content); + var ex = await Assert.ThrowsAsync(() => client.PutAsync("/error?body=true", content)); + + var originalException = ex.InnerException?.InnerException; + Assert.NotNull(originalException); + Assert.Equal("Test exception", originalException!.Message); + + Assert.Equal(1, logCollector.Count); + Assert.Equal(LogLevel.Error, logCollector.LatestRecord.Level); + Assert.Equal(LoggingCategory, logCollector.LatestRecord.Category); + Assert.NotNull(logCollector.LatestRecord.Exception); + Assert.Same(originalException, logCollector.LatestRecord.Exception); + + var state = logCollector.LatestRecord.StructuredState; + + Assert.Equal(5, state!.Count); + Assert.DoesNotContain(state, x => x.Key.StartsWith(HttpLoggingDimensions.RequestHeaderPrefix)); + Assert.DoesNotContain(state, x => x.Key.StartsWith(HttpLoggingDimensions.ResponseHeaderPrefix)); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Method && x.Value == HttpMethod.Put.ToString()); + Assert.DoesNotContain(state, x => x.Key == HttpLoggingDimensions.RequestBody); + Assert.DoesNotContain(state, x => x.Key == HttpLoggingDimensions.ResponseBody); + }); + } + + [Fact(Skip = "Fleky, uses real-time clock")] + public async Task HttpLogging_WhenRequestBodyReadTimeout_LogException() + { + await RunAsync( + LogLevel.Information, + static services => services.AddHttpLogging(static x => + { + x.RequestBodyContentTypes.Add(MediaTypeNames.Text.Plain); + x.LogBody = true; + }), + async (logCollector, client) => + { + using var stream = new InfiniteStream('A'); + using var content = new StreamContent(stream); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(MediaTypeNames.Text.Plain); + + using var cts = new CancellationTokenSource(millisecondsDelay: 1); + var ex = await Assert.ThrowsAnyAsync(() => client.PutAsync("/", content, cts.Token)); + Assert.NotNull(ex); + + Assert.Equal(1, logCollector.Count); + Assert.Equal(LogLevel.Error, logCollector.LatestRecord.Level); + Assert.Equal(LoggingCategory, logCollector.LatestRecord.Category); + Assert.NotNull(logCollector.LatestRecord.Exception); + + var state = logCollector.LatestRecord.StructuredState; + + Assert.Equal(5, state!.Count); + Assert.DoesNotContain(state, x => x.Key.StartsWith(HttpLoggingDimensions.RequestHeaderPrefix)); + Assert.DoesNotContain(state, x => x.Key.StartsWith(HttpLoggingDimensions.ResponseHeaderPrefix)); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Method && x.Value == HttpMethod.Put.ToString()); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.StatusCode && x.Value == "0"); + Assert.DoesNotContain(state, x => x.Key == HttpLoggingDimensions.RequestBody); + Assert.DoesNotContain(state, x => x.Key == HttpLoggingDimensions.ResponseBody); + }, + configureMiddleware: static x => x.BodyReadSizeLimit = int.MaxValue); + } + + [Fact] + public async Task HttpLogging_WhenRequestBodyReadError_LogException() + { + await RunAsync( + LogLevel.Information, + static services => services.AddHttpLogging(static x => + { + x.RequestBodyContentTypes.Add(MediaTypeNames.Text.Plain); + x.LogBody = true; + }), + async (logCollector, client) => + { + const string Content = "Client: hello!"; + + using var content = new StringContent(Content); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(MediaTypeNames.Text.Plain); + + using var response = await client.PutAsync("/err-pipe", content); + + await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout, expectedRecords: 2); + + var records = logCollector.GetSnapshot(); + Assert.Equal(2, records.Count); + var firstRecord = records[0]; + var secondRecord = records[1]; + + Assert.Equal(LogLevel.Error, firstRecord.Level); + Assert.All(records, x => Assert.Equal(LoggingCategory, x.Category)); + Assert.NotNull(firstRecord.Exception); + Assert.Equal(Log.ReadingRequestBodyError, firstRecord.Message); + Assert.Equal(RequestBodyErrorPipeFeature.ErrorMessage, firstRecord.Exception!.Message); + + var state = secondRecord.StructuredState; + + Assert.Equal(5, state!.Count); + Assert.DoesNotContain(state, x => x.Key == HttpLoggingDimensions.RequestBody); + Assert.DoesNotContain(state, x => x.Key == HttpLoggingDimensions.ResponseBody); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Method && x.Value == HttpMethod.Put.ToString()); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.StatusCode && x.Value == ((int)response.StatusCode).ToInvariantString()); + }); + } + + [Fact] + public async Task HttpLogging_WhenResponseBodyReadError_LogException() + { + const string ExceptionMessage = "Exception on response body intercepting"; + + static ReadOnlyMemory SyntheticInterseptingDataGetter(ResponseInterceptingStream _) + => throw new InvalidOperationException(ExceptionMessage); + + await RunAsync( + LogLevel.Information, + static services => services.AddHttpLogging(static x => + { + x.ResponseBodyContentTypes.Add(MediaTypeNames.Text.Plain); + x.LogBody = true; + }), + async (logCollector, client) => + { + using var response = await client.GetAsync("/"); + + await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout, expectedRecords: 2); + + var records = logCollector.GetSnapshot(); + Assert.Equal(2, records.Count); + var firstRecord = records[0]; + var secondRecord = records[1]; + + Assert.Equal(LogLevel.Error, firstRecord.Level); + Assert.All(records, x => Assert.Equal(LoggingCategory, x.Category)); + Assert.NotNull(firstRecord.Exception); + Assert.Equal(Log.ReadingResponseBodyError, firstRecord.Message); + Assert.Equal(ExceptionMessage, firstRecord.Exception!.Message); + + var state = secondRecord.StructuredState; + + Assert.Equal(5, state!.Count); + Assert.DoesNotContain(state, x => x.Key == HttpLoggingDimensions.RequestBody); + Assert.DoesNotContain(state, x => x.Key == HttpLoggingDimensions.ResponseBody); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.Method && x.Value == HttpMethod.Get.ToString()); + Assert.Single(state, x => x.Key == HttpLoggingDimensions.StatusCode && x.Value == ((int)response.StatusCode).ToInvariantString()); + }, + configureMiddleware: static x => x.GetResponseBodyInterceptedData = SyntheticInterseptingDataGetter); + } + + [Fact] + public async Task HttpLogging_WhenLogLevelWarning_NoLogHttp_ButWarning() + { + await RunAsync( + LogLevel.Warning, + static _ => { }, + static (logCollector, _) => + { + Assert.Equal(1, logCollector.Count); + + var latestRecord = logCollector.LatestRecord; + Assert.Equal(LoggingCategory, latestRecord.Category); + Assert.Equal(LogLevel.Warning, latestRecord.Level); + Assert.StartsWith("HttpLogging middleware is injected into application pipeline, but LogLevel", latestRecord.Message); + return Task.CompletedTask; + }); + } + + [Fact] + public async Task HttpLogging_WhenLogLevelError_NoLogHttp() + { + await RunAsync( + LogLevel.Error, + static _ => { }, + async static (logCollector, client) => + { + using var content = new StringContent("Client: hello!"); + using var response = await client.PostAsync("/", content).ConfigureAwait(false); + + Assert.True(response.IsSuccessStatusCode); + + Assert.Equal(0, logCollector.Count); + }); + } + + [Theory] + [InlineData("/home/api", "/home", true)] + [InlineData("/HOME/API", "/home/api", true)] + [InlineData("/home/api", "/home/users", false)] + [InlineData("/Home/Chats", "/home/chats", true)] + [InlineData("/home/chats/123", "/home/chats", true)] + [InlineData("/home/users/", "/home", true)] + [InlineData("/HOME/users", "/home", true)] + [InlineData("/home/users/foo", "/home/api", false)] + [InlineData("/", "/home", false)] + [InlineData("", "/home", false)] + [InlineData("/home", "/", true)] + public async Task HttpLogging_LogRecordIsNotCreated_If_isFiltered_True(string httpPath, string excludedPath, bool isFiltered) + { + await RunAsync( + LogLevel.Information, + services => services.AddHttpLogging(x => + { + x.ExcludePathStartsWith.Add(excludedPath); + }), + async (logCollector, client) => + { + using var response = await client.GetAsync(httpPath).ConfigureAwait(false); + + Assert.True(response.IsSuccessStatusCode); + + if (isFiltered) + { + Assert.Equal(0, logCollector.Count); + } + else + { + await WaitForLogRecordsAsync(logCollector, _defaultLogTimeout); + Assert.Equal(1, logCollector.Count); + Assert.Equal(5, logCollector.GetSnapshot()[0].StructuredState!.Count); + } + }); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Controllers/ApiRoutingController.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Controllers/ApiRoutingController.cs new file mode 100644 index 0000000000..71c1fa27a9 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Controllers/ApiRoutingController.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Time.Testing; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test.Controllers; + +[ApiController] +[Route("api/users")] +public class ApiRoutingController : ControllerBase +{ + [HttpGet("{userId}/{noDataClassification}")] + public ActionResult GetUser( + [FromRoute][PrivateData] string userId, + [FromRoute] string noDataClassification, + [FromQuery] string noRedaction) + { + Debug.Assert(userId != null, "Test"); + Debug.Assert(noDataClassification != null, "Test"); + Debug.Assert(noRedaction != null, "Test"); + + // Request processing imitation: + var middleware = HttpContext.RequestServices.GetRequiredService(); + var fakeTimeProvider = (FakeTimeProvider)middleware.TimeProvider; + fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(AcceptanceTest.ControllerProcessingTimeMs)); + + return "User info..."; + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Controllers/AttributeRoutingController.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Controllers/AttributeRoutingController.cs new file mode 100644 index 0000000000..b036a476f5 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Controllers/AttributeRoutingController.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Testing; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test.Controllers; + +[Route("[controller]")] +public class AttributeRoutingController : Controller +{ + [HttpGet("")] + [HttpGet("all")] + public IActionResult GetWithoutParams() => Ok(); + + [HttpGet("get-1/{param:int:min(1)}")] + public IActionResult GetWithConstraint([PrivateData] string param) => Ok(param); + + [HttpGet("get-2/{param?}")] + public IActionResult GetWithNullableConstraint([PrivateData] int? param) => Ok(param); + + [HttpGet("get-3/{param=all}")] + public IActionResult GetWithDefaultValue([NoDataClassification] string param) => Ok(param); + + [HttpGet("get-4/{param=all}")] + public IActionResult GetWithUnclassifiedParam(string param) => Ok(param); +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Controllers/ConventionalRoutingController.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Controllers/ConventionalRoutingController.cs new file mode 100644 index 0000000000..f205fa3882 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Controllers/ConventionalRoutingController.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Compliance.Testing; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test.Controllers; + +public class ConventionalRoutingController : Controller +{ + public const string Route = "{controller=ConventionalRouting}/{action=Index}/{param?}"; + + public IActionResult Index() => Ok(); + + public IActionResult GetEntity([PrivateData] int param) => Ok(param); + + public IActionResult GetData(int param) => Ok(param); +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Controllers/MixedRoutingController.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Controllers/MixedRoutingController.cs new file mode 100644 index 0000000000..c7e8a221a6 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Controllers/MixedRoutingController.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Testing; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test.Controllers; + +public class MixedRoutingController : Controller +{ + public IActionResult ConventionalRouting() => Ok(); + + [HttpGet("mixed/attribute-routing-1/{param:int:min(1)}")] + public IActionResult AttributeRoutingWithConstraint([PrivateData] int param) => Ok(param); + + [HttpGet("mixed/attribute-routing-2/{param?}")] + public IActionResult AttributeRoutingWithNullableConstraint([PrivateData] int? param) => Ok(param); + + [HttpGet("mixed/attribute-routing-3/{param=all}")] + public IActionResult AttributeRoutingWithDefaultValue([NoDataClassification] string param) => Ok(param); + + [HttpGet("mixed/attribute-routing-4/{param=all}")] + public IActionResult AttributeRoutingWithUnclassifiedParam(string param) => Ok(param); +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/HeaderReaderTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/HeaderReaderTest.cs new file mode 100644 index 0000000000..9d4e73be25 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/HeaderReaderTest.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Net.Mime; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test; + +public class HeaderReaderTest +{ + [Fact] + public void ShouldNotAddHeaders_WhenFilteringSetEmpty() + { + var reader = new HeaderReader(new Dictionary(), null!); + var listToFill = new List>(); + var headers = new HeaderDictionary(new Dictionary { [HeaderNames.Accept] = MediaTypeNames.Text.Plain }); + reader.Read(headers, listToFill); + Assert.Empty(listToFill); + } + + [Fact] + public void ShouldNotAddHeaders_WhenHeadersCollectionEmpty() + { + var reader = new HeaderReader(new Dictionary { [HeaderNames.Accept] = DataClassification.Unknown }, null!); + var listToFill = new List>(); + reader.Read(new HeaderDictionary(), listToFill); + Assert.Empty(listToFill); + } + + [Fact] + public void ShouldAddHeaders_WhenHeadersCollectionNotEmpty() + { + var headersToLog = new Dictionary { [HeaderNames.Accept] = DataClassification.Unknown }; + var reader = new HeaderReader(headersToLog, new FakeRedactorProvider(new FakeRedactorOptions { RedactionFormat = "" })); + var headers = new Dictionary + { + [HeaderNames.Accept] = MediaTypeNames.Text.Xml, + [HeaderNames.ContentType] = MediaTypeNames.Application.Pdf + }; + + var listToFill = new List>(); + reader.Read(new HeaderDictionary(headers), listToFill); + + Assert.Single(listToFill); + + var redacted = listToFill[0]; + Assert.Equal(HeaderNames.Accept, redacted.Key); + Assert.Equal($"", redacted.Value); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/HttpLoggingServiceExtensionsTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/HttpLoggingServiceExtensionsTest.cs new file mode 100644 index 0000000000..7a46d1475b --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/HttpLoggingServiceExtensionsTest.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +#if FIXME +using System.Net.Mime; +using Microsoft.Extensions.Compliance.Classification; +#endif +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +#if FIXME +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using Microsoft.Extensions.Telemetry; +#endif +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test; + +public class HttpLoggingServiceExtensionsTest +{ + [Fact] + public void ShouldThrow_WhenArgsNull() + { + var services = Mock.Of(); + + Assert.Throws(static () => HttpLoggingServiceExtensions.AddHttpLogging(null!)); + Assert.Throws(static () => HttpLoggingServiceExtensions.AddHttpLogEnricher(null!)); + Assert.Throws( + () => HttpLoggingServiceExtensions.AddHttpLogging(services, (Action)null!)); + + Assert.Throws( + () => HttpLoggingServiceExtensions.AddHttpLogging(services, (IConfigurationSection)null!)); + } + +#if FIXME + [Fact] + public void AddHttpLogging_WhenConfiguredUsingConfigurationSection_IsCorrect() + { + var services = new ServiceCollection(); + var builder = new ConfigurationBuilder().AddJsonFile("appsettings.json"); + var configuration = builder.Build(); + + services.AddHttpLogging(configuration.GetSection("HttpLogging")); + + using var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().Value; + + Assert.True(options.LogRequestStart); + Assert.True(options.RequestPathParameterRedactionMode == HttpRouteParameterRedactionMode.None); + Assert.Equal(64 * 1024, options.BodySizeLimit); + Assert.Equal(TimeSpan.FromSeconds(5), options.RequestBodyReadTimeout); + Assert.Equal(IncomingPathLoggingMode.Structured, options.RequestPathLoggingMode); + Assert.Equal(HttpRouteParameterRedactionMode.None, options.RequestPathParameterRedactionMode); + Assert.Collection(options.RequestHeadersDataClasses, static x => Assert.Equal(HeaderNames.Accept, x.Key)); + Assert.Collection(options.RequestHeadersDataClasses, static x => Assert.Equal(DataClassification.None, x.Value)); + Assert.Collection(options.ResponseHeadersDataClasses, static x => Assert.Equal(HeaderNames.ContentType, x.Key)); + Assert.Collection(options.ResponseHeadersDataClasses, static x => Assert.Equal(SimpleClassifications.PrivateData, x.Value)); + Assert.Collection(options.RequestBodyContentTypes, static x => Assert.Equal(MediaTypeNames.Text.Plain, x)); + Assert.Collection(options.ResponseBodyContentTypes, static x => Assert.Equal(MediaTypeNames.Application.Json, x)); + + Assert.Equal(2, options.RouteParameterDataClasses.Count); + Assert.Contains(options.RouteParameterDataClasses, static x => x.Key == "userId" && x.Value == DataClass.EUII); + Assert.Contains(options.RouteParameterDataClasses, static x => x.Key == "userContent" && x.Value == SimpleClassifications.PrivateData); + } +#endif +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/HttpRequestBodyReaderTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/HttpRequestBodyReaderTest.cs new file mode 100644 index 0000000000..27915eaf67 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/HttpRequestBodyReaderTest.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Telemetry.Http.Logging.Test.Internal; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test; + +public class HttpRequestBodyReaderTest +{ + [Fact] + public async Task Should_ThrowOnCancellation() + { + var context = new DefaultHttpContext(); + using var stream = CreateMemoryStream("test"); + context.Request.Body = stream; + context.Request.ContentType = "text/plain"; + + var ex = await Assert.ThrowsAsync( + async () => + await HttpRequestBodyReader.ReadBodyAsync(context.Request, TimeSpan.FromMinutes(100), int.MaxValue, new(canceled: true))); + + Assert.True(ex.CancellationToken.IsCancellationRequested); + Assert.Equal(0, stream.Position); + } + + [Fact] + public async Task FormatAsync_WhenTimeoutReading_ErrorMessage() + { + var context = new DefaultHttpContext(); + using var stream = new InfiniteStream('A'); + context.Request.Body = stream; + context.Request.ContentType = "text/plain"; + + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(100)); + + var body = await HttpRequestBodyReader.ReadBodyAsync(context.Request, TimeSpan.FromMilliseconds(100), int.MaxValue, cts.Token); + + Assert.Equal(HttpRequestBodyReader.ReadCancelled, Encoding.UTF8.GetString(body.FirstSpan)); + Assert.Equal(0, stream.Position); + } + + private static MemoryStream CreateMemoryStream(string value) + => new(Encoding.UTF8.GetBytes(value)); +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/IncomingHttpDimensionsTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/IncomingHttpDimensionsTest.cs new file mode 100644 index 0000000000..f96985e1e1 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/IncomingHttpDimensionsTest.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test; + +public class IncomingHttpDimensionsTest +{ + [Fact] + public void Should_ReturnList_AllDimensions() + { + var dimensions = HttpLoggingDimensions.DimensionNames; + Assert.Equal(9, dimensions.Count); + + var names = new HashSet(dimensions); + Assert.Equal(names.Count, dimensions.Count); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/IncomingRequestLogRecordTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/IncomingRequestLogRecordTest.cs new file mode 100644 index 0000000000..18e192b940 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/IncomingRequestLogRecordTest.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test; + +public class IncomingRequestLogRecordTest +{ + [Fact] + public void ShouldInitializeProps() + { + // This is kill Stryker's mutants: + var logRecord = new IncomingRequestLogRecord(); + Assert.Empty(logRecord.Host); + Assert.Empty(logRecord.Method); + Assert.Empty(logRecord.Path); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/IncomingRequestStructTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/IncomingRequestStructTest.cs new file mode 100644 index 0000000000..8f17afa436 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/IncomingRequestStructTest.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Collections.Generic; +using Microsoft.Extensions.Telemetry.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test; + +public class IncomingRequestStructTest +{ + [Fact] + public void Should_ReturnEnumerator_WithElements() + { + var helper = LogMethodHelper.GetHelper(); + helper.Add("prop1", "value1"); + helper.Add("prop_2", "value_2"); + + var reqStruct = new Log.IncomingRequestStruct(helper); + var enumerable = (IEnumerable)reqStruct; + var list = new List(); + foreach (var item in enumerable) + { + list.Add(item); + } + + Assert.Collection(list, + x => Assert.Equal(helper[0], x), + x => Assert.Equal(helper[1], x)); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/InfiniteStream.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/InfiniteStream.cs new file mode 100644 index 0000000000..aecb827b72 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/InfiniteStream.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test.Internal; + +internal sealed class InfiniteStream : Stream +{ + private readonly byte _charToFill; + + public InfiniteStream(char charToFill) + { + _charToFill = Convert.ToByte(charToFill); + } + + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanWrite => false; + + public override long Length => long.MaxValue; + + public override long Position { get; set; } + + public override void Flush() + { + // empty by design + } + + public override int Read(byte[] buffer, int offset, int count) + { + for (int i = 0; i < count; i++) + { + buffer[i + offset] = _charToFill; + } + + return count; + } + + public override long Seek(long offset, SeekOrigin origin) + => offset; + + public override void SetLength(long value) + => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/RequestBodyErrorPipeFeature.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/RequestBodyErrorPipeFeature.cs new file mode 100644 index 0000000000..46ae63b7d1 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/RequestBodyErrorPipeFeature.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test.Internal; + +internal sealed class RequestBodyErrorPipeFeature : IRequestBodyPipeFeature +{ + internal const string ErrorMessage = "TestPipeReader synthetic error"; + + public PipeReader Reader => new ErrorPipeReader(); + + private sealed class ErrorPipeReader : PipeReader + { + public override void AdvanceTo(SequencePosition consumed) => throw new InvalidOperationException(ErrorMessage); + public override void AdvanceTo(SequencePosition consumed, SequencePosition examined) => throw new InvalidOperationException(ErrorMessage); + public override void CancelPendingRead() => throw new InvalidOperationException(ErrorMessage); + public override void Complete(Exception? exception = null) => throw new InvalidOperationException(ErrorMessage); + public override ValueTask ReadAsync(CancellationToken cancellationToken = default) => throw new InvalidOperationException(ErrorMessage); + public override bool TryRead(out ReadResult result) => throw new InvalidOperationException(ErrorMessage); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/RequestBodyMultiSegmentPipeFeature.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/RequestBodyMultiSegmentPipeFeature.cs new file mode 100644 index 0000000000..f34fd11a79 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/RequestBodyMultiSegmentPipeFeature.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test.Internal; + +internal sealed class RequestBodyMultiSegmentPipeFeature : IRequestBodyPipeFeature +{ + public PipeReader Reader => new ErrorPipeReader(); + + private sealed class ErrorPipeReader : PipeReader + { + private readonly ReadOnlySequence _buffer; + private bool _isFirstReturn = true; + + public ErrorPipeReader() + { + var memory = new byte[] { 84, 101, 115, 116, 32, 83, 101, 103, 109, 101, 110, 116 }; // "Test Segment" + + var secondSegment = new SequenceSegment(memory, null, memory.Length); + var firstSegment = new SequenceSegment(memory, secondSegment, 0); + + _buffer = new ReadOnlySequence(firstSegment, 0, secondSegment, memory.Length); + } + + public override void CancelPendingRead() => throw new NotSupportedException(); + public override void Complete(Exception? exception = null) => throw new NotSupportedException(); + public override bool TryRead(out ReadResult result) => throw new NotSupportedException(); + public override void AdvanceTo(SequencePosition consumed) => throw new NotSupportedException(); + public override void AdvanceTo(SequencePosition consumed, SequencePosition examined) + { + // do nothing + } + + public override ValueTask ReadAsync(CancellationToken cancellationToken = default) + { + var result = new ReadResult(_buffer, isCanceled: false, isCompleted: !_isFirstReturn); + + _isFirstReturn = false; + return new ValueTask(result); + } + + private class SequenceSegment : ReadOnlySequenceSegment + { + public SequenceSegment(ReadOnlyMemory memory, ReadOnlySequenceSegment? next, long runningIndex) + { + Memory = memory; + Next = next; + RunningIndex = runningIndex; + } + } + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/TestBodyPipeFeatureMiddleware.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/TestBodyPipeFeatureMiddleware.cs new file mode 100644 index 0000000000..f797e62efa --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/TestBodyPipeFeatureMiddleware.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test.Internal; + +internal sealed class TestBodyPipeFeatureMiddleware : IMiddleware +{ + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + if (context.Request.Path.StartsWithSegments("/err-pipe")) + { + context.Features.Set(new RequestBodyErrorPipeFeature()); + } + + if (context.Request.Path.StartsWithSegments("/multi-segment-pipe")) + { + context.Features.Set(new RequestBodyMultiSegmentPipeFeature()); + } + + await next(context); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/TestHttpLogEnricher.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/TestHttpLogEnricher.cs new file mode 100644 index 0000000000..6d546735ca --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/Internal/TestHttpLogEnricher.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test; + +internal sealed class TestHttpLogEnricher : IHttpLogEnricher +{ + internal const string Key1 = "MyEnrichedProperty_1"; + internal const string Value1 = "my_value"; + + internal const string Key2 = "MyEnrichedProperty_2"; + internal const double Value2 = 1.75; + + public void Enrich(IEnrichmentPropertyBag bag, HttpRequest request, HttpResponse response) + { + bag.Add(Key1, Value1); + bag.Add(Key2, Value2); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/LoggingMiddlewareTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/LoggingMiddlewareTest.cs new file mode 100644 index 0000000000..681f67531c --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/LoggingMiddlewareTest.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.AspNetCore.Telemetry.Internal; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Shared.Diagnostics; +using Moq; +using Xunit; +using IOptionsFactory = Microsoft.Extensions.Options.Options; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test; + +public class LoggingMiddlewareTest +{ + [Fact] + public void HttpLoggingMiddleware_While_Having_Attached_Debugger_Has_Infinite_Timeout_For_Reading_A_Body() + { + var middleware = new HttpLoggingMiddleware( + options: IOptionsFactory.Create(new LoggingOptions()), + logger: NullLogger.Instance, + httpLogEnrichers: Array.Empty(), + httpRouteParser: new Mock().Object, + httpRouteFormatter: new Mock().Object, + redactorProvider: NullRedactorProvider.Instance, + httpRouteUtility: new Mock().Object, + debugger: DebuggerState.Attached); + + Assert.Equal(middleware.RequestBodyReadTimeout, Timeout.InfiniteTimeSpan); + } + + [Fact] + public void HttpLoggingMiddleware_While_Having_Attached_Detached_Has_Timeout_Set_By_Options_For_Reading_A_Body() + { + var options = new LoggingOptions(); + + var middleware = new HttpLoggingMiddleware( + options: IOptionsFactory.Create(options), + logger: NullLogger.Instance, + httpLogEnrichers: Array.Empty(), + httpRouteParser: new Mock().Object, + httpRouteFormatter: new Mock().Object, + redactorProvider: NullRedactorProvider.Instance, + httpRouteUtility: new Mock().Object, + debugger: DebuggerState.Detached); + + Assert.Equal(middleware.RequestBodyReadTimeout, options.RequestBodyReadTimeout); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/LoggingOptionsValidationTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/LoggingOptionsValidationTest.cs new file mode 100644 index 0000000000..3f792fbc5b --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/LoggingOptionsValidationTest.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Options; +using Xunit; +using TOpt = System.Action; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test; + +public class LoggingOptionsValidationTest +{ + [Theory] + [InlineData(2)] + [InlineData(3)] + [InlineData(-1)] + public void Should_Throw_OnInvalidPathLoggingMode(int mode) + { + using var services = new ServiceCollection() + .AddLogging() + .AddFakeRedaction() + .AddHttpLogging(x => x.RequestPathLoggingMode = (IncomingPathLoggingMode)mode) + .BuildServiceProvider(); + + var ex = Assert.Throws( + () => services.GetRequiredService()); + + Assert.Equal($"Unsupported value '{mode}' for enum type 'IncomingPathLoggingMode'", ex.Message); + } + + [Theory] + [CombinatorialData] + public void Should_NotThrow_OnValidPathLoggingMode(IncomingPathLoggingMode mode) + { + using var services = new ServiceCollection() + .AddLogging() + .AddFakeRedaction() + .AddHttpLogging(x => x.RequestPathLoggingMode = mode) + .BuildServiceProvider(); + + var ex = Record.Exception( + () => services.GetRequiredService()); + + Assert.Null(ex); + } + + [Theory] + [CombinatorialData] + public void Should_NotThrow_OnValidPathParameterRedactionMode(HttpRouteParameterRedactionMode mode) + { + using var services = new ServiceCollection() + .AddLogging() + .AddFakeRedaction() + .AddHttpLogging(x => x.RequestPathParameterRedactionMode = mode) + .BuildServiceProvider(); + + var ex = Record.Exception( + () => services.GetRequiredService()); + + Assert.Null(ex); + } + + [Theory] + [MemberData(nameof(OptionsConfigureActions))] + public void Should_Throw_OnInvalidOption(string fieldName, TOpt configure) + { + using var services = new ServiceCollection() + .AddLogging() + .AddFakeRedaction() + .AddHttpLogging(configure) + .BuildServiceProvider(); + + var ex = Assert.Throws( + () => services.GetRequiredService()); + + Assert.Contains($"{nameof(LoggingOptions)}.{fieldName}", ex.Message); + } + + public static IEnumerable OptionsConfigureActions => + new List + { + new object[] { "BodySizeLimit", (TOpt) (x => x.BodySizeLimit = 0) }, + new object[] { "BodySizeLimit", (TOpt) (x => x.BodySizeLimit = -1) }, + new object[] { "BodySizeLimit", (TOpt) (x => x.BodySizeLimit = 1_572_865) }, + new object[] { "BodySizeLimit", (TOpt) (x => x.BodySizeLimit = int.MaxValue) }, + new object[] { "BodySizeLimit", (TOpt) (x => x.BodySizeLimit = int.MinValue) }, + new object[] { "RequestBodyReadTimeout", (TOpt) (x => x.RequestBodyReadTimeout = TimeSpan.Zero) }, + new object[] { "RequestBodyReadTimeout", (TOpt) (x => x.RequestBodyReadTimeout = TimeSpan.FromMilliseconds(-1)) }, + new object[] { "RequestBodyReadTimeout", (TOpt) (x => x.RequestBodyReadTimeout = TimeSpan.FromMilliseconds(60_001)) }, + new object[] { "RequestBodyReadTimeout", (TOpt) (x => x.RequestBodyReadTimeout = TimeSpan.FromMinutes(2)) }, + new object[] { "RequestBodyReadTimeout", (TOpt) (x => x.RequestBodyReadTimeout = TimeSpan.MaxValue) }, + new object[] { "RequestBodyReadTimeout", (TOpt) (x => x.RequestBodyReadTimeout = TimeSpan.MinValue) }, + new object[] { "RequestBodyContentTypes", (TOpt) (x => x.RequestBodyContentTypes = null!) }, + new object[] { "RequestHeadersDataClasses", (TOpt) (x => x.RequestHeadersDataClasses = null!) }, + new object[] { "ResponseBodyContentTypes", (TOpt) (x => x.ResponseBodyContentTypes = null!) }, + new object[] { "ResponseHeadersDataClasses", (TOpt) (x => x.ResponseHeadersDataClasses = null!) }, + new object[] { "RouteParameterDataClasses", (TOpt) (x => x.RouteParameterDataClasses = null!) }, + new object[] { "ExcludePathStartsWith", (TOpt) ( x=> x.ExcludePathStartsWith = null!) } + }; +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/MediaTypeSetExtensionsTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/MediaTypeSetExtensionsTest.cs new file mode 100644 index 0000000000..24b89c6157 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/MediaTypeSetExtensionsTest.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Mvc.Formatters; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test; + +public class MediaTypeSetExtensionsTest +{ + [Fact] + public void Covers_WhenCovers_ReturnsTrue() + { + var collection = new[] + { + new MediaType("application/xml"), + new MediaType("text/*") + }; + + Assert.True(collection.Covers("application/xml")); + Assert.True(collection.Covers("text/whatever")); + Assert.True(collection.Covers("text/whatever-else")); + } + + [Fact] + public void Covers_WhenNotCovers_ReturnsFalse() + { + var collection = new[] + { + new MediaType("application/xml"), + new MediaType("text/*") + }; + + Assert.False(collection.Covers(null)); + Assert.False(collection.Covers(string.Empty)); + Assert.False(collection.Covers("image")); + Assert.False(collection.Covers("image/png")); + Assert.False(collection.Covers("audio/ogg")); + Assert.False(collection.Covers("application")); + Assert.False(collection.Covers("application/octet-stream")); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/PipeReaderExtensionsTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/PipeReaderExtensionsTest.cs new file mode 100644 index 0000000000..8ae82a84d1 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/PipeReaderExtensionsTest.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.IO; +using System.IO.Pipelines; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test; + +public class PipeReaderExtensionsTest +{ + [Theory] + [InlineData("T", 32)] + [InlineData("Hr", 2)] + [InlineData("AG8", 2)] + [InlineData("IpR7E", 5)] + [InlineData("DSbwoedf", 16)] + [InlineData("dthT18LsIaZNy", 1)] + [InlineData("ShOXqmhQyLFxW78V4DgwE", 11)] + [InlineData("FB3GefYUQxQeDiKnqtXglzd2szS2o7X6ei", 64)] + [InlineData("qREdmuDaoNmWdC2gbhD3rsRVke6FloRlw7fbM0of7d6RTEXGGc5D3HF", 64)] + public async Task ReadAsync_VariousSizesDefaultPipeReader(string content, int numBytes) + { + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + var pipe = PipeReader.Create(stream); + + var result = await pipe.ReadAsync(numBytes, CancellationToken.None); + + Assert.Equal( + content.Substring(0, Math.Min(content.Length, numBytes)), + Encoding.UTF8.GetString(result.ToArray())); + } + + [Theory] + [InlineData("T", 32)] + [InlineData("Hr", 2)] + [InlineData("AG8", 2)] + [InlineData("IpR7E", 5)] + [InlineData("DSbwoedf", 16)] + [InlineData("dthT18LsIaZNy", 1)] + [InlineData("ShOXqmhQyLFxW78V4DgwE", 11)] + [InlineData("FB3GefYUQxQeDiKnqtXglzd2szS2o7X6ei", 64)] + [InlineData("qREdmuDaoNmWdC2gbhD3rsRVke6FloRlw7fbM0of7d6RTEXGGc5D3HF", 64)] + public async Task ReadAsync_VariousSizesSmallBuffersPipeReader(string content, int numBytes) + { + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + var options = new StreamPipeReaderOptions(bufferSize: 16, minimumReadSize: 16); + var pipe = PipeReader.Create(stream, options); + + var result = await pipe.ReadAsync(numBytes, CancellationToken.None); + + Assert.Equal( + content.Substring(0, Math.Min(content.Length, numBytes)), + Encoding.UTF8.GetString(result.ToArray())); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/ResponseInterceptingStreamTest.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/ResponseInterceptingStreamTest.cs new file mode 100644 index 0000000000..77ca742c73 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Logging/ResponseInterceptingStreamTest.cs @@ -0,0 +1,204 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Shared.Pools; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Http.Logging.Test; + +public class ResponseInterceptingStreamTest +{ + [Fact] + public async Task ResponseInterceptingStream_Calls_Intercepted_Stream_Methods() + { + var streamMock = new Mock(); + const int Position = 1; + const int WriteTimeout = 1; + + using var stream = new MemoryStream(); + using var responseInterceptingStream = new ResponseInterceptingStream( + interceptedStream: streamMock.Object, + responseBodyFeature: new StreamResponseBodyFeature(new MemoryStream()), + bufferWriter: new BufferWriter(), + interceptedValueWriteLimit: 1000); + + var writeResult = responseInterceptingStream.BeginWrite(Array.Empty(), 0, 0, _ => { }, new object()); + responseInterceptingStream.EndWrite(writeResult); + var readResult = responseInterceptingStream.BeginRead(Array.Empty(), 0, 0, _ => { }, new object()); + responseInterceptingStream.EndRead(readResult); + _ = responseInterceptingStream.CanRead; + _ = responseInterceptingStream.CanSeek; + _ = responseInterceptingStream.CanWrite; + await responseInterceptingStream.CompleteAsync(); + responseInterceptingStream.CopyTo(stream, 10); + await responseInterceptingStream.CopyToAsync(stream, 10, default); + responseInterceptingStream.Flush(); + await responseInterceptingStream.FlushAsync(default); + _ = responseInterceptingStream.Length; + responseInterceptingStream.Position = Position; + _ = responseInterceptingStream.Position; + await responseInterceptingStream.ReadAsync(Array.Empty()); + _ = responseInterceptingStream.Seek(0, SeekOrigin.Current); + responseInterceptingStream.SetLength(0); + await responseInterceptingStream.WriteAsync(Array.Empty()); + _ = responseInterceptingStream.WriteTimeout; + responseInterceptingStream.WriteTimeout = WriteTimeout; + await responseInterceptingStream.ReadAsync(Array.Empty(), 0, 0, default); + await responseInterceptingStream.WriteAsync(Array.Empty(), 0, 0, default); + responseInterceptingStream.Read(Array.Empty(), 0, 0); + await responseInterceptingStream.DisposeAsync(); + + streamMock.Verify( + expression: mock => mock.BeginWrite(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + times: Times.AtLeastOnce); + + streamMock.Verify( + expression: mock => mock.EndWrite(It.IsAny()), + times: Times.AtLeastOnce); + + streamMock.Verify( + expression: mock => mock.BeginRead(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + times: Times.AtLeastOnce); + + streamMock.Verify( + expression: mock => mock.EndRead(It.IsAny()), + times: Times.AtLeastOnce); + + streamMock.Verify( + expression: mock => mock.CanRead, + times: Times.AtLeastOnce); + + streamMock.Verify( + expression: mock => mock.CanSeek, + times: Times.AtLeastOnce); + + streamMock.Verify( + expression: mock => mock.CanWrite, + times: Times.AtLeastOnce); + + streamMock.Verify( + expression: mock => mock.CopyTo(It.IsAny(), It.IsAny()), + times: Times.AtLeastOnce); + + streamMock.Verify( + expression: mock => mock.CopyToAsync(It.IsAny(), It.IsAny(), It.IsAny()), + times: Times.AtLeastOnce); + + streamMock.Verify( + expression: mock => mock.DisposeAsync(), + times: Times.AtLeastOnce); + + streamMock.Verify( + expression: mock => mock.Flush(), + times: Times.AtLeastOnce); + + streamMock.Verify( + expression: mock => mock.FlushAsync(It.IsAny()), + times: Times.AtLeastOnce); + + streamMock.Verify( + expression: mock => mock.Length, + times: Times.AtLeastOnce); + + streamMock.Verify( + expression: mock => mock.Position, + times: Times.AtLeastOnce); + + streamMock.Verify( + expression: mock => mock.ReadAsync(It.IsAny>(), It.IsAny()), + times: Times.AtLeastOnce); + + streamMock.Verify( + expression: mock => mock.Seek(It.IsAny(), It.IsAny()), + times: Times.AtLeastOnce); + + streamMock.Verify( + expression: mock => mock.SetLength(It.IsAny()), + times: Times.AtLeastOnce); + + streamMock.Verify( + expression: mock => mock.WriteAsync(It.IsAny>(), It.IsAny()), + times: Times.AtLeast(2)); + + streamMock.Verify( + expression: mock => mock.WriteTimeout, + times: Times.AtLeastOnce); + + streamMock.Verify( + expression: mock => mock.Read(It.IsAny(), It.IsAny(), It.IsAny()), + times: Times.AtLeastOnce); + + streamMock.Verify( + expression: mock => mock.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + times: Times.AtLeastOnce); + + streamMock.VerifySet(x => x.Position = Position, Times.AtLeastOnce); + streamMock.VerifySet(x => x.WriteTimeout = WriteTimeout, Times.AtLeastOnce); + + Assert.Equal(responseInterceptingStream, responseInterceptingStream.Stream); + } + + [Fact] + public async Task ResponseInterceptingStream_Calls_Underlying_HttpResponseBodyFeature_Methods() + { + var featureMock = new Mock(); + + using var responseInterceptingStream = new ResponseInterceptingStream( + interceptedStream: new MemoryStream(), + responseBodyFeature: featureMock.Object, + bufferWriter: new BufferWriter(), + interceptedValueWriteLimit: 1000); + + responseInterceptingStream.DisableBuffering(); + await responseInterceptingStream.StartAsync(default); + await responseInterceptingStream.SendFileAsync(string.Empty, 0, 0, default); + await responseInterceptingStream.CompleteAsync(); + + featureMock.Verify( + expression: mock => mock.DisableBuffering(), + times: Times.AtLeastOnce); + + featureMock.Verify( + expression: mock => mock.StartAsync(It.IsAny()), + times: Times.AtLeastOnce); + + featureMock.Verify( + expression: mock => mock.SendFileAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + times: Times.AtLeastOnce); + + featureMock.Verify( + expression: mock => mock.CompleteAsync(), + times: Times.AtLeastOnce); + } + + [Fact] + public void Consumer_Can_Write_Or_Read_From_InterceptingStream_Using_Span_Overloads() + { + using var responseInterceptingStream = new ResponseInterceptingStream( + interceptedStream: new MemoryStream(), + responseBodyFeature: new StreamResponseBodyFeature(new MemoryStream()), + bufferWriter: new BufferWriter(), + interceptedValueWriteLimit: 1000); + + var data = Encoding.UTF8.GetBytes("Kebab"); + + responseInterceptingStream.Write(data, 0, data.Length); + + Assert.Equal(responseInterceptingStream.Position, data.Length); + + responseInterceptingStream.Seek(0, SeekOrigin.Begin); + + var buffer = new byte[data.Length]; + responseInterceptingStream.Read(buffer); + + Assert.Equal(Encoding.UTF8.GetString(data), Encoding.UTF8.GetString(buffer)); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/HttpMeteringTests.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/HttpMeteringTests.cs new file mode 100644 index 0000000000..e9ae89ceda --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/HttpMeteringTests.cs @@ -0,0 +1,596 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.AspNetCore.Telemetry.Internal; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metering; +using Microsoft.Extensions.Time.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Test; + +#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits +public class HttpMeteringTests +{ + private const long DefaultClockAdvanceMs = 200; + + private static readonly RequestDelegate _stubRequestDelegate = + static _ => Task.CompletedTask; + + private readonly FakeTimeProvider _fakeTimeProvider = new(); + + private readonly RequestDelegate _advanceTimeRequestDelegate; + + public HttpMeteringTests() + { + _advanceTimeRequestDelegate = _ => + { + _fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(DefaultClockAdvanceMs)); + return Task.CompletedTask; + }; + } + + [Fact] + public async Task CanLogIncomingRequestMetric() + { + using var metricCollector = new MetricCollector(new List { typeof(HttpMeteringMiddleware).FullName! }); + using var host = await FakeHost.CreateBuilder() + .ConfigureWebHost(webBuilder => webBuilder + .UseTestServer() + .ConfigureServices(services => services + .AddRouting() + .AddHttpMetering()) + .Configure(app => app + .UseRouting() + .UseHttpMetering() + .UseEndpoints(endpoints => + { + endpoints.MapGet("/some/route/{routeId}", async context => + { + context.Response.StatusCode = 401; + await context.Response.WriteAsync("TestCompleted"); + }); + }))) + .StartAsync(); + + using var client = host.GetTestClient(); + using var response = client.GetAsync("/some/route/123").Result; + + var latest = metricCollector.GetHistogramValues(Metric.IncomingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal("localhost", latest.GetDimension(Metric.ReqHost)); + Assert.Equal("GET /some/route/{routeId}", latest.GetDimension(Metric.ReqName)); + Assert.Equal("401", latest.GetDimension(Metric.RspResultCode)); + Assert.Equal("no_exception", latest.GetDimension(Metric.ExceptionType)); + + await host.StopAsync(); + } + + [Fact] + public async Task CanLogIncomingRequestMetricWithEnricher() + { + IIncomingRequestMetricEnricher testEnricher = new TestEnricher(2, "2"); + + using var metricCollector = new MetricCollector(new List { typeof(HttpMeteringMiddleware).FullName! }); + using var host = await FakeHost.CreateBuilder() + .ConfigureWebHost(webBuilder => + webBuilder + .UseTestServer() + .ConfigureServices(services => + { + services + .AddRouting() + .AddHttpMetering(builder => + { + builder.AddMetricEnricher(); + builder.AddMetricEnricher(testEnricher); + }); + }) + .Configure(app => + app.UseRouting() + .UseHttpMetering() + .UseEndpoints(endpoints => + { + endpoints.MapGet("/some/route/{routeId}", async context => + { + context.Response.StatusCode = 401; + await context.Response.WriteAsync("TestCompleted"); + }); + }))) + .StartAsync(); + + using var client = host.GetTestClient(); + using var response = await client.GetAsync("/some/route/123").ConfigureAwait(false); + + var latest = metricCollector.GetHistogramValues(Metric.IncomingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal("localhost", latest.GetDimension(Metric.ReqHost)); + Assert.Equal("GET /some/route/{routeId}", latest.GetDimension(Metric.ReqName)); + Assert.Equal("401", latest.GetDimension(Metric.RspResultCode)); + Assert.Equal("no_exception", latest.GetDimension(Metric.ExceptionType)); + Assert.Equal("test_value_1", latest.GetDimension("test_property_1")); + Assert.Equal("test_value_21", latest.GetDimension("test_property_21")); + Assert.Equal("test_value_22", latest.GetDimension("test_property_22")); + + await host.StopAsync(); + } + + [Fact] + public async Task CanLogIncomingRequestMetric_UseRoutingAfterMiddleware() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(new List { typeof(HttpMeteringMiddleware).FullName! }); + using var host = await FakeHost.CreateBuilder() + .ConfigureWebHost(webBuilder => + webBuilder + .UseTestServer() + .ConfigureServices(services => + { + services + .AddHttpMetering(); + services.AddRouting(); + }) + .Configure(app => + app.UseHttpMetering() + .UseRouting() + .UseEndpoints(endpoints => + { + endpoints.MapGet("/some/route/{routeId}", async context => + { + context.Response.StatusCode = 503; + await context.Response.WriteAsync("TestCompleted"); + }); + }))) + .StartAsync(); + + using var client = host.GetTestClient(); + using var response = await client.GetAsync("/some/route/456").ConfigureAwait(false); + + var latest = metricCollector.GetHistogramValues(Metric.IncomingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal("localhost", latest.GetDimension(Metric.ReqHost)); + Assert.Equal("GET /some/route/{routeId}", latest.GetDimension(Metric.ReqName)); + Assert.Equal("503", latest.GetDimension(Metric.RspResultCode)); + Assert.Equal("no_exception", latest.GetDimension(Metric.ExceptionType)); + + await host.StopAsync(); + } + + [Fact] + public async Task CanLogIncomingRequestMetric_TimeoutException() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(new List { typeof(HttpMeteringMiddleware).FullName! }); + using var host = await FakeHost.CreateBuilder() + .ConfigureWebHost(webBuilder => webBuilder + .UseTestServer() + .ConfigureServices(services => services + .AddHttpMetering() + .AddRouting()) + .Configure(app => app + .UseHttpMetering() + .UseRouting() + .UseEndpoints(endpoints => + { + endpoints.MapGet("/some/route/{routeId}", async context => + { + context.Response.StatusCode = 301; + await context.Response.WriteAsync("TestCompleted"); + throw new TimeoutException(); + }); + + endpoints.MapGet("/some/route2/{routeId}", async context => + { + context.Response.StatusCode = 400; + await context.Response.WriteAsync("TestCompleted"); + throw new TimeoutException(); + }); + + endpoints.MapGet("/some/route3/{routeId}", async context => + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync("TestCompleted"); + throw new TimeoutException(); + }); + }))) + .StartAsync(); + + string exceptionTypeName = typeof(TimeoutException).FullName!; + + using var client = host.GetTestClient(); + + await Assert.ThrowsAsync(async () => await client.GetAsync("/some/route/456")); + + var latest = metricCollector.GetHistogramValues(Metric.IncomingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal("localhost", latest.GetDimension(Metric.ReqHost)); + Assert.Equal("GET /some/route/{routeId}", latest.GetDimension(Metric.ReqName)); + Assert.Equal("500", latest.GetDimension(Metric.RspResultCode)); + Assert.Equal(exceptionTypeName, latest.GetDimension(Metric.ExceptionType)); + metricCollector.Clear(); + + await Assert.ThrowsAsync(async () => await client.GetAsync("/some/route2/456")); + latest = metricCollector.GetHistogramValues(Metric.IncomingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal("localhost", latest.GetDimension(Metric.ReqHost)); + Assert.Equal("GET /some/route2/{routeId}", latest.GetDimension(Metric.ReqName)); + Assert.Equal("400", latest.GetDimension(Metric.RspResultCode)); + Assert.Equal(exceptionTypeName, latest.GetDimension(Metric.ExceptionType)); + metricCollector.Clear(); + + await Assert.ThrowsAsync(async () => await client.GetAsync("/some/route3/456")); + latest = metricCollector.GetHistogramValues(Metric.IncomingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal("localhost", latest.GetDimension(Metric.ReqHost)); + Assert.Equal("GET /some/route3/{routeId}", latest.GetDimension(Metric.ReqName)); + Assert.Equal("500", latest.GetDimension(Metric.RspResultCode)); + Assert.Equal(exceptionTypeName, latest.GetDimension(Metric.ExceptionType)); + metricCollector.Clear(); + + await host.StopAsync(); + } + + [Fact] + public void InvokeAsync_HttpMeteringMiddleware_Success() + { + string httpMethod = HttpMethods.Get; + string hostString = "teams.microsoft.com"; + string route = "/tenant/{tenantid}/users/{userId}"; + + using var meter = new Meter(); + using var metricCollector = new MetricCollector(new List { typeof(HttpMeteringMiddleware).FullName! }); + var middleware = SetupMockMiddleware(meter: meter, new List()); + + var context = new DefaultHttpContext(); + context.Request.Method = httpMethod; + context.Request.Host = new HostString(hostString); + var routePattern = RoutePatternFactory.Parse(route); + context.SetEndpoint(new RouteEndpoint( + _stubRequestDelegate, routePattern, 0, EndpointMetadataCollection.Empty, string.Empty)); + + middleware.InvokeAsync(context, _advanceTimeRequestDelegate).Wait(); + + var latest = metricCollector.GetHistogramValues(Metric.IncomingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal(hostString, latest.GetDimension(Metric.ReqHost)); + Assert.Equal("GET " + route, latest.GetDimension(Metric.ReqName)); + Assert.Equal("200", latest.GetDimension(Metric.RspResultCode)); + Assert.Equal("no_exception", latest.GetDimension(Metric.ExceptionType)); + } + + [Fact] + public void PerfStopwatch_ReturnsTotalMilliseconds_InsteadOfFraction() + { + const long TimeAdvanceMs = 1500L; // We need to use any value greater than 1000 (1 second) + + string httpMethod = HttpMethods.Get; + string hostString = "teams.microsoft.com"; + string route = "/tenant/{tenantid}/users/{userId}"; + + using var meter = new Meter(); + using var metricCollector = new MetricCollector(new List { typeof(HttpMeteringMiddleware).FullName! }); + var middleware = SetupMockMiddleware(meter: meter, new List()); + + var context = new DefaultHttpContext(); + context.Request.Method = httpMethod; + context.Request.Host = new HostString(hostString); + var routePattern = RoutePatternFactory.Parse(route); + context.SetEndpoint(new RouteEndpoint( + _stubRequestDelegate, routePattern, 0, EndpointMetadataCollection.Empty, string.Empty)); + + RequestDelegate next = _ => + { + _fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(TimeAdvanceMs)); + return Task.CompletedTask; + }; + + middleware.InvokeAsync(context, next).Wait(); + + var latest = metricCollector.GetHistogramValues(Metric.IncomingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal(hostString, latest.GetDimension(Metric.ReqHost)); + Assert.Equal("GET " + route, latest.GetDimension(Metric.ReqName)); + Assert.Equal("200", latest.GetDimension(Metric.RspResultCode)); + Assert.Equal("no_exception", latest.GetDimension(Metric.ExceptionType)); + } + + [Fact] + public void InvokeAsync_HttpMeteringMiddleware_NullRoute() + { + string httpMethod = HttpMethods.Post; + string hostString = "teams.microsoft.com"; + + using var meter = new Meter(); + using var metricCollector = new MetricCollector(new List { typeof(HttpMeteringMiddleware).FullName! }); + var middleware = SetupMockMiddleware(meter: meter, new List()); + + var context = new DefaultHttpContext(); + context.Request.Method = httpMethod; + context.Request.Host = new HostString(hostString); + context.Response.StatusCode = 409; + + middleware.InvokeAsync(context, _advanceTimeRequestDelegate).Wait(); + + var latest = metricCollector.GetHistogramValues(Metric.IncomingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal(hostString, latest.GetDimension(Metric.ReqHost)); + Assert.Equal("POST unsupported_route", latest.GetDimension(Metric.ReqName)); + Assert.Equal("409", latest.GetDimension(Metric.RspResultCode)); + Assert.Equal("no_exception", latest.GetDimension(Metric.ExceptionType)); + } + + [Fact] + public void InvokeAsync_HttpMeteringMiddleware_NullHostString() + { + string httpMethod = HttpMethods.Post; + string expectedHostString = "unknown_host_name"; + + using var meter = new Meter(); + using var metricCollector = new MetricCollector(new List { typeof(HttpMeteringMiddleware).FullName! }); + var middleware = SetupMockMiddleware(meter: meter, new List()); + + var context = new DefaultHttpContext(); + context.Request.Method = httpMethod; + context.Response.StatusCode = 409; + + middleware.InvokeAsync(context, _advanceTimeRequestDelegate).Wait(); + + var latest = metricCollector.GetHistogramValues(Metric.IncomingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal(expectedHostString, latest.GetDimension(Metric.ReqHost)); + Assert.Equal("POST unsupported_route", latest.GetDimension(Metric.ReqName)); + Assert.Equal("409", latest.GetDimension(Metric.RspResultCode)); + Assert.Equal("no_exception", latest.GetDimension(Metric.ExceptionType)); + } + + [Fact] + public void InvokeAsync_HttpMeteringMiddleware_InternalServerError() + { + string httpMethod = HttpMethods.Post; + string hostString = "teams.microsoft.com"; + + using var meter = new Meter(); + using var metricCollector = new MetricCollector(new List { typeof(HttpMeteringMiddleware).FullName! }); + var middleware = SetupMockMiddleware(meter: meter, new List()); + + var context = new DefaultHttpContext(); + context.Request.Method = httpMethod; + context.Request.Host = new HostString(hostString); + + static Task next(HttpContext context) => throw new InvalidOperationException(); + + Assert.Throws(() => middleware.InvokeAsync(context, next).RunSynchronously()); + + var latest = metricCollector.GetHistogramValues(Metric.IncomingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal(hostString, latest.GetDimension(Metric.ReqHost)); + Assert.Equal("POST unsupported_route", latest.GetDimension(Metric.ReqName)); + Assert.Equal("500", latest.GetDimension(Metric.RspResultCode)); + Assert.Equal(typeof(InvalidOperationException).FullName!, latest.GetDimension(Metric.ExceptionType)); + } + + [Fact] + public void SendAsync_MultiEnrich() + { + string hostString = "teams.microsoft.com"; + + for (int i = 1; i <= 15; i++) + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(new List { typeof(HttpMeteringMiddleware).FullName! }); + var middleware = SetupMockMiddleware(meter: meter, new List + { + new TestEnricher(i) + }); + + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Get; + context.Request.Host = new HostString(hostString); + context.Response.StatusCode = 409; + + middleware.InvokeAsync(context, Mock.Of()).Wait(); + + var latest = metricCollector.GetHistogramValues(Metric.IncomingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal(hostString, latest.GetDimension(Metric.ReqHost)); + Assert.Equal("GET unsupported_route", latest.GetDimension(Metric.ReqName)); + Assert.Equal("409", latest.GetDimension(Metric.RspResultCode)); + Assert.Equal("no_exception", latest.GetDimension(Metric.ExceptionType)); + + for (int j = 0; j < i; j++) + { + Assert.Equal($"test_value_{j + 1}", latest.GetDimension($"test_property_{j + 1}")); + } + } + } + + [Fact] + public void InvokeAsync_MultipleEnrichers() + { + string hostString = "teams.microsoft.com"; + + using var meter = new Meter(); + using var metricCollector = new MetricCollector(new List { typeof(HttpMeteringMiddleware).FullName! }); + var middleware = SetupMockMiddleware(meter: meter, new List + { + new TestEnricher(2), + new TestEnricher(2, "2"), + }); + + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Get; + context.Request.Host = new HostString(hostString); + context.Response.StatusCode = 409; + + middleware.InvokeAsync(context, Mock.Of()).Wait(); + + var latest = metricCollector.GetHistogramValues(Metric.IncomingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal(hostString, latest.GetDimension(Metric.ReqHost)); + Assert.Equal("GET unsupported_route", latest.GetDimension(Metric.ReqName)); + Assert.Equal("409", latest.GetDimension(Metric.RspResultCode)); + Assert.Equal("no_exception", latest.GetDimension(Metric.ExceptionType)); + + Assert.Equal("test_value_1", latest.GetDimension("test_property_1")); + Assert.Equal("test_value_2", latest.GetDimension("test_property_2")); + Assert.Equal("test_value_21", latest.GetDimension("test_property_21")); + Assert.Equal("test_value_22", latest.GetDimension("test_property_22")); + } + + [Fact] + public async Task InvokeAsync_HttpMeteringMiddleware_PropertyBagEdgeCase() + { + string httpMethod = HttpMethods.Post; + string hostString = "teams.microsoft.com"; + + using var meter = new Meter(); + using var metricCollector = new MetricCollector(new List { typeof(HttpMeteringMiddleware).FullName! }); + var middleware = SetupMockMiddleware(meter: meter, new List + { + new PropertyBagEdgeCaseEnricher() + }); + + var context = new DefaultHttpContext(); + context.Request.Method = httpMethod; + context.Request.Host = new HostString(hostString); + context.Response.StatusCode = 409; + + await middleware.InvokeAsync(context, Mock.Of()); + + var latest = metricCollector.GetHistogramValues(Metric.IncomingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal(hostString, latest.GetDimension(Metric.ReqHost)); + Assert.Equal("POST unsupported_route", latest.GetDimension(Metric.ReqName)); + Assert.Equal("409", latest.GetDimension(Metric.RspResultCode)); + Assert.Equal("no_exception", latest.GetDimension(Metric.ExceptionType)); + + Assert.Equal("test_val", latest.GetDimension("non_null_object_property")); + } + + [Fact] + public void HttpMeteringMiddleware_Fail_16DEnrich() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(new List { typeof(HttpMeteringMiddleware).FullName! }); + Assert.Throws(() => SetupMockMiddleware( + meter: meter, + new List + { + new TestEnricher(16) + })); + } + + [Fact] + public void HttpMeteringMiddleware_Fail_RepeatCustomDimensions() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(new List { typeof(HttpMeteringMiddleware).FullName! }); + Assert.Throws(() => SetupMockMiddleware( + meter: meter, + new List + { + new TestEnricher(), + new TestEnricher() + })); + } + + [Fact] + public void HttpMeteringMiddleware_Fail_RepeatDefaultDimensions() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(new List { typeof(HttpMeteringMiddleware).FullName! }); + Assert.Throws(() => SetupMockMiddleware( + meter: meter, + new List + { + new SameDefaultDimEnricher() + })); + } + + [Fact] + public void ServiceCollection_GivenNullArguments_Throws() + { + Assert.Throws(() => + ((HttpMeteringBuilder)null!).AddMetricEnricher()); + + Assert.Throws(() => + ((HttpMeteringBuilder)null!).AddMetricEnricher(Mock.Of())); + + Assert.Throws(() => + new HttpMeteringBuilder(null!) + .AddMetricEnricher(null!)); + } + + [Fact] + public void ServiceCollection_AddMultipleRequestEnrichersSuccessfully() + { + IIncomingRequestMetricEnricher testEnricher = new TestEnricher(); + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddSingleton(testEnricher); + + using var provider = services.BuildServiceProvider(); + var enrichersCollection = provider.GetServices(); + + var enricherCount = 0; + foreach (var enricher in enrichersCollection) + { + enricherCount++; + } + + Assert.Equal(2, enricherCount); + } + + private HttpMeteringMiddleware SetupMockMiddleware(Meter meter, IEnumerable requestMetricEnrichers) + { + var propertyBagPoolMock = new Mock>(); + propertyBagPoolMock + .Setup(o => o.Get()) + .Returns(new MetricEnrichmentPropertyBag()); + + IServiceCollection services = new ServiceCollection(); + services.AddSingleton(meter); + services.AddHttpMetering(); + foreach (IIncomingRequestMetricEnricher requestMetricEnricher in requestMetricEnrichers) + { + services.AddSingleton(requestMetricEnricher); + } + + using var serviceProvider = services.BuildServiceProvider(); + + var middleware = serviceProvider.GetRequiredService(); + middleware.TimeProvider = _fakeTimeProvider; + return middleware; + } +} +#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/Internals/NullRequestEnricher.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/Internals/NullRequestEnricher.cs new file mode 100644 index 0000000000..0ff4622e77 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/Internals/NullRequestEnricher.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Shared.Collections; + +namespace Microsoft.AspNetCore.Telemetry; + +internal class NullRequestEnricher : IIncomingRequestMetricEnricher +{ + public IReadOnlyList DimensionNames => Empty.ReadOnlyList(); + + public void Enrich(IEnrichmentPropertyBag enrichmentBag) + { + enrichmentBag.Add(null!, null!); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/Internals/PropertyBagEdgeCaseEnricher.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/Internals/PropertyBagEdgeCaseEnricher.cs new file mode 100644 index 0000000000..8f6ac45008 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/Internals/PropertyBagEdgeCaseEnricher.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.AspNetCore.Telemetry; + +public class PropertyBagEdgeCaseEnricher : IIncomingRequestMetricEnricher +{ + public IReadOnlyList DimensionNames => new[] { "non_null_object_property" }; + private readonly object _stringObj = "test_val"; + + public void Enrich(IEnrichmentPropertyBag bag) + { + bag.Add("non_null_object_property", _stringObj); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/Internals/SameDefaultDimEnricher.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/Internals/SameDefaultDimEnricher.cs new file mode 100644 index 0000000000..8936cb9422 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/Internals/SameDefaultDimEnricher.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.AspNetCore.Telemetry; + +public class SameDefaultDimEnricher : IIncomingRequestMetricEnricher +{ + public IReadOnlyList DimensionNames => new[] { "req_host" }; + + public void Enrich(IEnrichmentPropertyBag bag) + { + bag.Add("req_host", "req_host_value"); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/Internals/TestEnricher.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/Internals/TestEnricher.cs new file mode 100644 index 0000000000..8ba4b59084 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Metering/Internals/TestEnricher.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.AspNetCore.Telemetry; + +public class TestEnricher : IIncomingRequestMetricEnricher +{ + private readonly List _dimensionNames = new(); + private readonly int _numDimensions; + private readonly string _prefix; + + public TestEnricher() + { + _numDimensions = 1; + _prefix = string.Empty; + + for (int i = 1; i <= _numDimensions; i++) + { + _dimensionNames.Add($"test_property_{_prefix}{i}"); + } + } + + public TestEnricher(int numDimensions, string prefix = "") + { + _numDimensions = numDimensions; + _prefix = prefix; + + for (int i = 1; i <= _numDimensions; i++) + { + _dimensionNames.Add($"test_property_{_prefix}{i}"); + } + } + + public IReadOnlyList DimensionNames => _dimensionNames; + + public void Enrich(IEnrichmentPropertyBag bag) + { + for (int i = 1; i <= _numDimensions; i++) + { + bag.Add($"test_property_{_prefix}{i}", $"test_value_{_prefix}{i}"); + } + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Microsoft.AspNetCore.Telemetry.Middleware.Tests.csproj b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Microsoft.AspNetCore.Telemetry.Middleware.Tests.csproj new file mode 100644 index 0000000000..4996f2f518 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/Microsoft.AspNetCore.Telemetry.Middleware.Tests.csproj @@ -0,0 +1,30 @@ + + + Microsoft.AspNetCore.Telemetry + Unit tests for Microsoft.AspNetCore.Telemetry.Middleware. + + + + $(NetCoreTargetFrameworks) + true + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/appsettings.json b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/appsettings.json new file mode 100644 index 0000000000..c1503ee98d --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Middleware.Tests/appsettings.json @@ -0,0 +1,21 @@ +{ + "HttpLogging": { + "LogRequestStart": true, + "RequestPathLoggingMode": "Structured", + "RequestPathParameterRedactionMode": "None", + "RequestBodyReadTimeout": "00:00:05", + "BodySizeLimit": 65536, + "RequestHeadersDataClasses": { + "Accept": "PublicNonPersonalData" + }, + "ResponseHeadersDataClasses": { + "Content-Type": "CustomerContent" + }, + "RequestBodyContentTypes": [ "text/plain" ], + "ResponseBodyContentTypes": [ "application/json" ], + "RouteParameterDataClasses": { + "userId": "EUII", + "userContent": "CustomerContent" + } + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Enrichment.RequestHeaders.Tests/Internals/TestExtensions.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Enrichment.RequestHeaders.Tests/Internals/TestExtensions.cs new file mode 100644 index 0000000000..1550a4dd72 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Enrichment.RequestHeaders.Tests/Internals/TestExtensions.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Moq; + +namespace Microsoft.AspNetCore.Telemetry.RequestHeaders.Test.Internals; + +internal static class TestExtensions +{ + public static IOptions ToOptions(this RequestHeadersLogEnricherOptions options) + { + var mock = new Mock>(); + mock.Setup(o => o.Value).Returns(options); + return mock.Object; + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Enrichment.RequestHeaders.Tests/RequestHeadersEnricherExtensionsTests.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Enrichment.RequestHeaders.Tests/RequestHeadersEnricherExtensionsTests.cs new file mode 100644 index 0000000000..8d77f9c38d --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Enrichment.RequestHeaders.Tests/RequestHeadersEnricherExtensionsTests.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Enrichment; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.RequestHeaders.Test; + +public class RequestHeadersEnricherExtensionsTests +{ + [Fact] + public void RequestHeadersLogEnricher_GivenAnyNullArgument_Throws() + { + Assert.Throws(() => + ((IServiceCollection)null!).AddRequestHeadersLogEnricher()); + + Assert.Throws(() => + ((IServiceCollection)null!).AddRequestHeadersLogEnricher(_ => { })); + + Assert.Throws(() => + ((IServiceCollection)null!).AddRequestHeadersLogEnricher(Mock.Of())); + + Assert.Throws(() => + new ServiceCollection().AddRequestHeadersLogEnricher((IConfigurationSection)null!)); + + Assert.Throws(() => + new ServiceCollection().AddRequestHeadersLogEnricher((Action)null!)); + } + + [Fact] + public void RequestHeadersLogEnricher_GivenOptions_HeaderKeysWithDataClass_NoRedaction_Throws() + { + using var sp = new ServiceCollection() + .AddRequestHeadersLogEnricher(e => e.HeadersDataClasses.Add("TestKey", DataClassification.None)) + .BuildServiceProvider(); + + Assert.Throws(() => sp.GetRequiredService()); + } + + [Fact] + public void RequestHeadersLogEnricher_GivenInvalidOptions_HeaderKeysWithDataClass_Throws() + { + using var sp = new ServiceCollection() + .AddRequestHeadersLogEnricher(e => e.HeadersDataClasses = null!) + .BuildServiceProvider(); + + Assert.Throws(() => sp.GetRequiredService>().Value); + } + + [Fact] + public void RequestHeadersLogEnricher_GivenNoArguments_WithRedaction_RegistersInDI() + { + // Act + using var serviceProvider = new ServiceCollection() + .AddRequestHeadersLogEnricher() + .AddFakeRedaction() + .BuildServiceProvider(); + + // Assert + Assert.NotNull(serviceProvider.GetRequiredService()); + } + + [Fact] + public void RequestHeadersLogEnricher_GivenHeaderKeysWithDataClassAndRedaction_RegistersInDI() + { + // Act + using var serviceProvider = new ServiceCollection() + .AddRequestHeadersLogEnricher(e => + { + e.HeadersDataClasses.Add("TestKey", DataClassification.None); + }) + .AddFakeRedaction() + .BuildServiceProvider(); + + // Assert + Assert.NotNull(serviceProvider.GetRequiredService()); + var options = serviceProvider.GetRequiredService>().Value; + Assert.NotNull(options); + Assert.NotNull(options.HeadersDataClasses); + Assert.Single(options.HeadersDataClasses); + } + +#if FIXME + [Fact] + public void RequestHeadersLogEnricher_GivenConfigurationSectionAndRedaction_RegistersInDI() + { + var dc = new DataClassification("TAX", 1); + + // Arrange + var configRoot = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { ["Section:HeadersDataClasses:TestKey"] = "SimpleClassifications.PrivateData" }) + .Build(); + + // Act + using var serviceProvider = new ServiceCollection() + .AddRequestHeadersLogEnricher(configRoot.GetSection("Section")) + .AddFakeRedaction() + .BuildServiceProvider(); + + // Assert + var enricher = serviceProvider.GetRequiredService(); + Assert.NotNull(enricher); + Assert.IsType(enricher); + + var options = serviceProvider.GetRequiredService>().Value; + Assert.NotNull(options); + Assert.NotNull(options.HeadersDataClasses); + Assert.Single(options.HeadersDataClasses); + Assert.Equal(SimpleClassifications.PrivateData, options.HeadersDataClasses["TestKey"]); + } +#endif +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Enrichment.RequestHeaders.Tests/RequestHeadersEnricherTests.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Enrichment.RequestHeaders.Tests/RequestHeadersEnricherTests.cs new file mode 100644 index 0000000000..b98b449e9e --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Enrichment.RequestHeaders.Tests/RequestHeadersEnricherTests.cs @@ -0,0 +1,248 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Telemetry.RequestHeaders.Test.Internals; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Telemetry.Enrichment.Test; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.RequestHeaders.Test; + +public class RequestHeadersEnricherTests +{ + private const string HeaderKey1 = "X-RequestID"; + private const string HeaderKey2 = "Host"; + private const string HeaderKey3 = "NullHeader"; + private const string HeaderKey4 = "X-Platform"; + private const string RequestId = "RequestIdTestValue"; + private const string TestValue = "TestValue"; + + private readonly Mock _accessorMock; + private readonly Mock _redactorProviderMock; + + public RequestHeadersEnricherTests() + { + var headers = new HeaderDictionary + { + { HeaderKey1, RequestId }, + { HeaderKey2, string.Empty }, + { HeaderKey3, (string)null! }, + { HeaderKey4, TestValue }, + }; + + var featureCollection = new FeatureCollection(); + + var httpContextMock = new Mock(MockBehavior.Strict); + httpContextMock.SetupGet(c => c.Request.Headers).Returns(headers); + httpContextMock.SetupGet(c => c.Request.HttpContext).Returns(httpContextMock.Object); + httpContextMock.SetupGet(c => c.Features).Returns(featureCollection); + + _accessorMock = new Mock(MockBehavior.Strict); + _accessorMock.SetupGet(r => r.HttpContext).Returns(httpContextMock.Object); + + var redactor = FakeRedactor.Create(); + _redactorProviderMock = new Mock(MockBehavior.Default); + _redactorProviderMock.SetReturnsDefault(redactor); + } + + [Fact] + public void RequestHeadersEnricher_GivenDisabledEnricherOptions_HeaderKeysDataClasses_DoesNotEnrich() + { + // Arrange + var options = new RequestHeadersLogEnricherOptions + { + HeadersDataClasses = new Dictionary() + }; + + var enricher = new RequestHeadersLogEnricher(_accessorMock.Object, options.ToOptions(), _redactorProviderMock.Object); + var enrichedProperties = new TestLogEnrichmentPropertyBag(); + + // Act + enricher.Enrich(enrichedProperties); + var enrichedState = enrichedProperties.Properties; + + // Assert + Assert.Empty(enrichedState); + } + + [Fact] + public void RequestHeadersEnricher_GivenEnricherOptions_HeaderKeysDataClasses_Enriches() + { + // Arrange + var options = new RequestHeadersLogEnricherOptions + { + HeadersDataClasses = new Dictionary + { + { HeaderKey1, SimpleClassifications.PrivateData }, + { HeaderKey4, SimpleClassifications.PublicData } + } + }; + + Mock redactorProviderMock = new Mock(); + redactorProviderMock.Setup(x => x.GetRedactor(SimpleClassifications.PublicData)) + .Returns(new FakeRedactor()); + redactorProviderMock.Setup(x => x.GetRedactor(SimpleClassifications.PrivateData)) + .Returns(FakeRedactor.Create(new FakeRedactorOptions { RedactionFormat = "redacted:{0}" })); + + var enricher = new RequestHeadersLogEnricher(_accessorMock.Object, options.ToOptions(), redactorProviderMock.Object); + + var enrichedProperties = new TestLogEnrichmentPropertyBag(); + + // Act + enricher.Enrich(enrichedProperties); + var enrichedState = enrichedProperties.Properties; + + // Assert + Assert.True(enrichedState.Count == 2); + Assert.Equal($"redacted:{RequestId}", enrichedState[HeaderKey1].ToString()); + Assert.Equal(TestValue, enrichedState[HeaderKey4].ToString()); + } + + [Fact] + public void RequestHeadersEnricher_GivenEnricherOptions_OneHeaderValueIsEmpty_HeaderKeysDataClasses_PartiallyEnriches() + { + // Arrange + var options = new RequestHeadersLogEnricherOptions + { + HeadersDataClasses = new Dictionary + { + { HeaderKey1, SimpleClassifications.PrivateData }, + { HeaderKey2, SimpleClassifications.PublicData } + } + }; + + Mock redactorProviderMock = new Mock(); + redactorProviderMock.Setup(x => x.GetRedactor(SimpleClassifications.PrivateData)) + .Returns(FakeRedactor.Create(new FakeRedactorOptions { RedactionFormat = "REDACTED:{0}" })); + var enricher = new RequestHeadersLogEnricher(_accessorMock.Object, options.ToOptions(), redactorProviderMock.Object); + + var enrichedProperties = new TestLogEnrichmentPropertyBag(); + + // Act + enricher.Enrich(enrichedProperties); + var enrichedState = enrichedProperties.Properties; + + // Assert + Assert.Single(enrichedState); + Assert.Equal($"REDACTED:{RequestId}", enrichedState[HeaderKey1].ToString()); + Assert.False(enrichedState.ContainsKey(HeaderKey2)); + } + + [Fact] + public void RequestHeadersEnricher_GivenEnricherOptions_OneHeaderValueIsNull_HeaderKeysDataClasses_PartiallyEnriches() + { + // Arrange + var options = new RequestHeadersLogEnricherOptions + { + HeadersDataClasses = new Dictionary + { + { HeaderKey1, SimpleClassifications.PublicData }, + { HeaderKey3, SimpleClassifications.PublicData } + } + }; + var enricher = new RequestHeadersLogEnricher(_accessorMock.Object, options.ToOptions(), _redactorProviderMock.Object); + + var enrichedProperties = new TestLogEnrichmentPropertyBag(); + + // Act + enricher.Enrich(enrichedProperties); + var enrichedState = enrichedProperties.Properties; + + // Assert + Assert.Single(enrichedState); + Assert.Equal(RequestId, enrichedState[HeaderKey1].ToString()); + Assert.False(enrichedState.ContainsKey(HeaderKey3)); + } + + [Fact] + public void RequestHeadersEnricher_GivenEnricherOptions_OneHeaderValueIsMissing_HeaderKeysDataClasses_PartiallyEnriches() + { + // Arrange + var headerKey2 = "header_does_not_exist"; + var options = new RequestHeadersLogEnricherOptions + { + HeadersDataClasses = new Dictionary + { + { HeaderKey1, SimpleClassifications.PublicData }, + { headerKey2, SimpleClassifications.PublicData } + } + }; + var enricher = new RequestHeadersLogEnricher(_accessorMock.Object, options.ToOptions(), _redactorProviderMock.Object); + + var enrichedProperties = new TestLogEnrichmentPropertyBag(); + + // Act + enricher.Enrich(enrichedProperties); + var enrichedState = enrichedProperties.Properties; + + // Assert + Assert.Equal(RequestId, enrichedState[HeaderKey1].ToString()); + Assert.False(enrichedState.ContainsKey(headerKey2)); + } + + [Fact] + public void RequestHeadersEnricher_GivenNullHttpContext_HeaderKeysDataClasses_DoesNotEnrich() + { + // Arrange + var options = new RequestHeadersLogEnricherOptions + { + HeadersDataClasses = new Dictionary + { + { HeaderKey1, SimpleClassifications.PublicData } + } + }; + + var accessorMock = new Mock(MockBehavior.Strict); + accessorMock.SetupGet(r => r.HttpContext).Returns((HttpContext)null!); + + var enricher = new RequestHeadersLogEnricher(accessorMock.Object, options.ToOptions(), _redactorProviderMock.Object); + + var enrichedProperties = new TestLogEnrichmentPropertyBag(); + + // Act + enricher.Enrich(enrichedProperties); + var enrichedState = enrichedProperties.Properties; + + // Assert + Assert.Empty(enrichedState); + } + + [Fact] + public void RequestHeadersEnricher_GivenNullRequest_HeaderKeysDataClasses_DoesNotEnrich() + { + // Arrange + var options = new RequestHeadersLogEnricherOptions + { + HeadersDataClasses = new Dictionary + { + { HeaderKey1, SimpleClassifications.PublicData } + } + }; + + var featureCollection = new FeatureCollection(); + + var httpContextMock = new Mock(MockBehavior.Strict); + httpContextMock.SetupGet(c => c.Request).Returns((HttpRequest)null!); + httpContextMock.SetupGet(c => c.Features).Returns(featureCollection); + + var accessorMock = new Mock(MockBehavior.Strict); + accessorMock.SetupGet(r => r.HttpContext).Returns(httpContextMock.Object); + + var enricher = new RequestHeadersLogEnricher(accessorMock.Object, options.ToOptions(), _redactorProviderMock.Object); + + var enrichedProperties = new TestLogEnrichmentPropertyBag(); + + // Act + enricher.Enrich(enrichedProperties); + var enrichedState = enrichedProperties.Properties; + + // Assert + Assert.Empty(enrichedState); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Microsoft.AspNetCore.Telemetry.Tests.csproj b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Microsoft.AspNetCore.Telemetry.Tests.csproj new file mode 100644 index 0000000000..6929e6b073 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Microsoft.AspNetCore.Telemetry.Tests.csproj @@ -0,0 +1,37 @@ + + + Microsoft.AspNetCore.Telemetry.Test + Unit tests for Microsoft.AspNetCore.Telemetry. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Telemetry.Internal.Http/HttpUtilityExtensionsTests.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Telemetry.Internal.Http/HttpUtilityExtensionsTests.cs new file mode 100644 index 0000000000..cdd537e949 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Telemetry.Internal.Http/HttpUtilityExtensionsTests.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETCOREAPP3_1_OR_GREATER +using System; +#endif +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +#if NETCOREAPP3_1_OR_GREATER +using Microsoft.AspNetCore.Routing.Patterns; +#endif +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; +#if !NETCOREAPP3_1_OR_GREATER +using Opt = Microsoft.Extensions.Options.Options; +#endif + +namespace Microsoft.AspNetCore.Telemetry.Internal.Test; + +public class HttpUtilityExtensionsTests +{ + [Fact] + public void AddHttpRouteUtilities_AddsIncomingHttpRouteUtility() + { + var sp = new ServiceCollection().AddHttpRouteUtilities().BuildServiceProvider(); + + var routeUtilities = sp.GetService(); + Assert.NotNull(routeUtilities); + } + +#if NETCOREAPP3_1_OR_GREATER + [Fact] + public void GetRoute_ReturnsRouteWhenExists() + { + var metadata = new List(1); + + var httpRoute = "v1/profile/users/{userId}/teams/{teamId}"; + var endpoint = new RouteEndpoint( + c => throw new InvalidOperationException("Test"), + RoutePatternFactory.Parse(httpRoute), + 0, + new EndpointMetadataCollection(metadata), + "Endpoint display name"); + + Mock mockHttpRequest = new Mock(); + HttpContext context = new DefaultHttpContext(); + mockHttpRequest.Setup(m => m.HttpContext).Returns(context); + context.SetEndpoint(endpoint); + + Assert.Equal(httpRoute, mockHttpRequest.Object.GetRoute()); + } + + [Fact] + public void GetRoute_NullEndpoint_ReturnsEmpty() + { + Mock mockHttpRequest = new Mock(); + HttpContext context = new DefaultHttpContext(); + mockHttpRequest.Setup(m => m.HttpContext).Returns(context); + + Assert.Equal(string.Empty, mockHttpRequest.Object.GetRoute()); + } + + [Fact] + public void GetRoute_NullRawText_ReturnsEmpty() + { + var metadata = new List(1); + var endpoint = new RouteEndpoint( + c => throw new InvalidOperationException("Test"), + RoutePatternFactory.Pattern(), + 0, + new EndpointMetadataCollection(metadata), + "Endpoint display name"); + + Mock mockHttpRequest = new Mock(); + HttpContext context = new DefaultHttpContext(); + mockHttpRequest.Setup(m => m.HttpContext).Returns(context); + context.SetEndpoint(endpoint); + + Assert.Equal(string.Empty, mockHttpRequest.Object.GetRoute()); + } +#else + [Fact] + public void GetRoute_NullRouteData_ReturnsEmpty() + { + Mock mockHttpRequest = new Mock(); + HttpContext context = new DefaultHttpContext(); + mockHttpRequest.SetupGet(x => x.HttpContext).Returns(context); + Assert.Equal(string.Empty, mockHttpRequest.Object.GetRoute()); + } + + [Fact] + public void GetRoute_WhenRouteExists_ShouldReturnCorrectRoute() + { + Mock mockHttpRequest = new Mock(); + + var httpRoute = "v1/profile/users/{userId}/teams/{teamId}"; + var routeValues = new RouteValueDictionary + { + { "route", httpRoute } + }; + + var routeData = new RouteData(routeValues); + Mock mockRouter = new Mock(); + + var routerColl = new RouteCollection(); +#pragma warning disable CS0618 // Type or member is obsolete + routerColl.Add(new Route( + mockRouter.Object, + httpRoute, + new RouteValueDictionary(), + new Dictionary(), + new RouteValueDictionary(), + new DefaultInlineConstraintResolver(Opt.Create(new RouteOptions())))); +#pragma warning restore CS0618 // Type or member is obsolete + + routeData.Routers.Add(routerColl); + + var context = new DefaultHttpContext(); + + IRoutingFeature routingFeature = new RoutingFeature + { + RouteData = routeData + }; + context.Features.Set(routingFeature); + mockHttpRequest.SetupGet(x => x.HttpContext).Returns(context); + + Assert.Equal(httpRoute, mockHttpRequest.Object.GetRoute()); + } +#endif +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Telemetry.Internal.Http/IncomingHttpRouteUtilityTests.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Telemetry.Internal.Http/IncomingHttpRouteUtilityTests.cs new file mode 100644 index 0000000000..fe50ef5721 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Telemetry.Internal.Http/IncomingHttpRouteUtilityTests.cs @@ -0,0 +1,491 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETCOREAPP3_1_OR_GREATER +using System; +#endif +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +#if NETCOREAPP3_1_OR_GREATER +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; +#endif +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Internal.Test; + +public class IncomingHttpRouteUtilityTests +{ +#if NETCOREAPP3_1_OR_GREATER + [Fact] + public void GetSensitiveParameter_OneParameterWithDataClassAttrib_ReturnsSensitiveParameter() + { + ControllerActionDescriptor controllerActionDescriptor = new ControllerActionDescriptor(); + var parametersInfo = typeof(TestController).GetMethod(nameof(TestController.GetTest1Async))!.GetParameters(); + + controllerActionDescriptor.Parameters = new List(parametersInfo.Length); + foreach (var parameterInfo in parametersInfo) + { + var parameter = new ControllerParameterDescriptor + { + ParameterInfo = parameterInfo, + }; + controllerActionDescriptor.Parameters.Add(parameter); + } + + var metadata = new List { controllerActionDescriptor }; + + var httpRoute = "/v1/profile/users/userId123"; + var endpoint = new RouteEndpoint( + c => throw new InvalidOperationException("Test"), + RoutePatternFactory.Parse(httpRoute), + 0, + new EndpointMetadataCollection(metadata), + "Endpoint display name"); + + Mock mockHttpRequest = new Mock(); + HttpContext context = new DefaultHttpContext(); + mockHttpRequest.Setup(m => m.HttpContext).Returns(context); + context.SetEndpoint(endpoint); + + var routeUtility = new IncomingHttpRouteUtility(); + var sensitiveParameters = routeUtility.GetSensitiveParameters(httpRoute, mockHttpRequest.Object, new Dictionary(StringComparer.Ordinal)); + Assert.Single(sensitiveParameters); + Assert.True(sensitiveParameters.ContainsKey("userId")); + Assert.Equal(SimpleClassifications.PrivateData, sensitiveParameters.GetValueOrDefault("userId")); + } + + [Fact] + public void GetSensitiveParameter_MoreThanOneParameterWithDataClassAttrib_ReturnsAllSensitiveParameters() + { + ControllerActionDescriptor controllerActionDescriptor = new ControllerActionDescriptor(); + var parametersInfo = typeof(TestController).GetMethod(nameof(TestController.GetTest2Async))!.GetParameters(); + + controllerActionDescriptor.Parameters = new List(parametersInfo.Length); + foreach (var parameterInfo in parametersInfo) + { + var parameter = new ControllerParameterDescriptor + { + ParameterInfo = parameterInfo, + }; + controllerActionDescriptor.Parameters.Add(parameter); + } + + var metadata = new List { controllerActionDescriptor }; + + var httpRoute = "v1/profile/users/{userId}/teams/{teamId}"; + var endpoint = new RouteEndpoint( + c => throw new InvalidOperationException("Test"), + RoutePatternFactory.Parse(httpRoute), + 0, + new EndpointMetadataCollection(metadata), + "Endpoint display name"); + + Mock mockHttpRequest = new Mock(); + HttpContext context = new DefaultHttpContext(); + mockHttpRequest.Setup(m => m.HttpContext).Returns(context); + context.SetEndpoint(endpoint); + + var routeUtility = new IncomingHttpRouteUtility(); + var sensitiveParameters = routeUtility.GetSensitiveParameters(httpRoute, mockHttpRequest.Object, new Dictionary(StringComparer.Ordinal)); + Assert.Equal(2, sensitiveParameters.Count); + Assert.True(sensitiveParameters.ContainsKey("userId")); + Assert.Equal(SimpleClassifications.PrivateData, sensitiveParameters.GetValueOrDefault("userId")); + Assert.True(sensitiveParameters.ContainsKey("teamId")); + Assert.Equal(SimpleClassifications.PrivateData, sensitiveParameters.GetValueOrDefault("teamId")); + } + + [Fact] + public void GetSensitiveParameter_MixParamsWithAndWithoutDataClass_ReturnsOnlySensitiveParameters() + { + ControllerActionDescriptor controllerActionDescriptor = new ControllerActionDescriptor(); + var parametersInfo = typeof(TestController).GetMethod(nameof(TestController.GetTest3Async))!.GetParameters(); + + controllerActionDescriptor.Parameters = new List(parametersInfo.Length); + foreach (var parameterInfo in parametersInfo) + { + var parameter = new ControllerParameterDescriptor + { + ParameterInfo = parameterInfo, + }; + controllerActionDescriptor.Parameters.Add(parameter); + } + + var metadata = new List { controllerActionDescriptor }; + + var httpRoute = "v1/profile/users/{userId}/teams/{teamId}/chats/{chatId}"; + var endpoint = new RouteEndpoint( + c => throw new InvalidOperationException("Test"), + RoutePatternFactory.Parse(httpRoute), + 0, + new EndpointMetadataCollection(metadata), + "Endpoint display name"); + + Mock mockHttpRequest = new Mock(); + HttpContext context = new DefaultHttpContext(); + mockHttpRequest.Setup(m => m.HttpContext).Returns(context); + context.SetEndpoint(endpoint); + + var routeUtility = new IncomingHttpRouteUtility(); + var sensitiveParameters = routeUtility.GetSensitiveParameters(httpRoute, mockHttpRequest.Object, new Dictionary(StringComparer.Ordinal)); + Assert.Equal(2, sensitiveParameters.Count); + Assert.True(sensitiveParameters.ContainsKey("userId")); + Assert.Equal(SimpleClassifications.PrivateData, sensitiveParameters.GetValueOrDefault("userId")); + Assert.True(sensitiveParameters.ContainsKey("teamId")); + Assert.Equal(SimpleClassifications.PrivateData, sensitiveParameters.GetValueOrDefault("teamId")); + + Assert.False(sensitiveParameters.ContainsKey("chatId")); + } + + [Fact] + public void GetSensitiveParameter_NoParameters_ReturnsDefault() + { + ControllerActionDescriptor controllerActionDescriptor = new ControllerActionDescriptor(); + var parametersInfo = typeof(TestController).GetMethod(nameof(TestController.GetTest4Async))!.GetParameters(); + + controllerActionDescriptor.Parameters = new List(parametersInfo.Length); + foreach (var parameterInfo in parametersInfo) + { + var parameter = new ControllerParameterDescriptor + { + ParameterInfo = parameterInfo, + }; + controllerActionDescriptor.Parameters.Add(parameter); + } + + var metadata = new List { controllerActionDescriptor }; + + var httpRoute = "v1/profile/users"; + var endpoint = new RouteEndpoint( + c => throw new InvalidOperationException("Test"), + RoutePatternFactory.Parse(httpRoute), + 0, + new EndpointMetadataCollection(metadata), + "Endpoint display name"); + + Mock mockHttpRequest = new Mock(); + HttpContext context = new DefaultHttpContext(); + mockHttpRequest.Setup(m => m.HttpContext).Returns(context); + context.SetEndpoint(endpoint); + + var routeUtility = new IncomingHttpRouteUtility(); + var d = new Dictionary + { + { "testKey", SimpleClassifications.PrivateData } + }; + var sensitiveParameters = routeUtility.GetSensitiveParameters(httpRoute, mockHttpRequest.Object, d); + Assert.Single(sensitiveParameters); + Assert.True(sensitiveParameters.ContainsKey("testKey")); + Assert.Equal(SimpleClassifications.PrivateData, sensitiveParameters.GetValueOrDefault("testKey")); + } + + [Fact] + public void GetSensitiveParameter_AllParametersWithoutDataClass_ReturnsDefault() + { + ControllerActionDescriptor controllerActionDescriptor = new ControllerActionDescriptor(); + var parametersInfo = typeof(TestController).GetMethod(nameof(TestController.GetTest1Async))!.GetParameters(); + + controllerActionDescriptor.Parameters = new List(parametersInfo.Length); + foreach (var parameterInfo in parametersInfo) + { + var parameter = new ControllerParameterDescriptor + { + ParameterInfo = parameterInfo, + }; + controllerActionDescriptor.Parameters.Add(parameter); + } + + var metadata = new List { controllerActionDescriptor }; + + var httpRoute = string.Empty; + var endpoint = new RouteEndpoint( + c => throw new InvalidOperationException("Test"), + RoutePatternFactory.Parse(httpRoute), + 0, + new EndpointMetadataCollection(metadata), + "Endpoint display name"); + + Mock mockHttpRequest = new Mock(); + HttpContext context = new DefaultHttpContext(); + mockHttpRequest.Setup(m => m.HttpContext).Returns(context); + context.SetEndpoint(endpoint); + + var routeUtility = new IncomingHttpRouteUtility(); + + var d = new Dictionary + { + { "testKey", SimpleClassifications.PrivateData } + }; + var sensitiveParameters = routeUtility.GetSensitiveParameters(httpRoute, mockHttpRequest.Object, d); + Assert.Single(sensitiveParameters); + Assert.True(sensitiveParameters.ContainsKey("testKey")); + Assert.Equal(SimpleClassifications.PrivateData, sensitiveParameters.GetValueOrDefault("testKey")); + + Assert.False(sensitiveParameters.ContainsKey("userId")); + Assert.False(sensitiveParameters.ContainsKey("teamId")); + Assert.False(sensitiveParameters.ContainsKey("chatId")); + } + + [Fact] + public void GetSensitiveParameter_EmptyRoute_ReturnsDefault() + { + ControllerActionDescriptor controllerActionDescriptor = new ControllerActionDescriptor(); + var parametersInfo = typeof(TestController).GetMethod(nameof(TestController.GetTest5Async))!.GetParameters(); + + controllerActionDescriptor.Parameters = new List(parametersInfo.Length); + foreach (var parameterInfo in parametersInfo) + { + var parameter = new ControllerParameterDescriptor + { + ParameterInfo = parameterInfo, + }; + controllerActionDescriptor.Parameters.Add(parameter); + } + + var metadata = new List { controllerActionDescriptor }; + + var httpRoute = "v1/profile/users/{userId}/teams/{teamId}/chats/{chatId}"; + var endpoint = new RouteEndpoint( + c => throw new InvalidOperationException("Test"), + RoutePatternFactory.Parse(httpRoute), + 0, + new EndpointMetadataCollection(metadata), + "Endpoint display name"); + + Mock mockHttpRequest = new Mock(); + HttpContext context = new DefaultHttpContext(); + mockHttpRequest.Setup(m => m.HttpContext).Returns(context); + context.SetEndpoint(endpoint); + + var routeUtility = new IncomingHttpRouteUtility(); + + var sensitiveParameters = routeUtility.GetSensitiveParameters(httpRoute, mockHttpRequest.Object, new Dictionary(StringComparer.Ordinal)); + Assert.Empty(sensitiveParameters); + + Assert.False(sensitiveParameters.ContainsKey("userId")); + Assert.False(sensitiveParameters.ContainsKey("teamId")); + Assert.False(sensitiveParameters.ContainsKey("chatId")); + } + + [Fact] + public void GetSensitiveParameter_ParameterWithDataClass_NonEmptyDefault_ReturnsCombinedWithNonDuplicateEntries() + { + ControllerActionDescriptor controllerActionDescriptor = new ControllerActionDescriptor(); + var parametersInfo = typeof(TestController).GetMethod(nameof(TestController.GetTest2Async))!.GetParameters(); + + controllerActionDescriptor.Parameters = new List(parametersInfo.Length); + foreach (var parameterInfo in parametersInfo) + { + var parameter = new ControllerParameterDescriptor + { + ParameterInfo = parameterInfo, + }; + controllerActionDescriptor.Parameters.Add(parameter); + } + + var metadata = new List { controllerActionDescriptor }; + + var httpRoute = "v1/profile/users/{userId}/teams/{teamId}"; + var endpoint = new RouteEndpoint( + c => throw new InvalidOperationException("Test"), + RoutePatternFactory.Parse(httpRoute), + 0, + new EndpointMetadataCollection(metadata), + "Endpoint display name"); + + Mock mockHttpRequest = new Mock(); + HttpContext context = new DefaultHttpContext(); + mockHttpRequest.Setup(m => m.HttpContext).Returns(context); + context.SetEndpoint(endpoint); + + var d = new Dictionary + { + { "testKey", SimpleClassifications.PrivateData }, + { "teamId", SimpleClassifications.PrivateData } + }; + + var routeUtility = new IncomingHttpRouteUtility(); + var sensitiveParameters = routeUtility.GetSensitiveParameters(httpRoute, mockHttpRequest.Object, d); + Assert.Equal(3, sensitiveParameters.Count); + Assert.True(sensitiveParameters.ContainsKey("userId")); + Assert.Equal(SimpleClassifications.PrivateData, sensitiveParameters.GetValueOrDefault("userId")); + Assert.True(sensitiveParameters.ContainsKey("teamId")); + Assert.Equal(SimpleClassifications.PrivateData, sensitiveParameters.GetValueOrDefault("teamId")); + Assert.True(sensitiveParameters.ContainsKey("testKey")); + Assert.Equal(SimpleClassifications.PrivateData, sensitiveParameters.GetValueOrDefault("testKey")); + } + + [Fact] + public void GetSensitiveParameter_ParameterWithDataClass_TakesPrecedenceOverDefaultList() + { + ControllerActionDescriptor controllerActionDescriptor = new ControllerActionDescriptor(); + var parametersInfo = typeof(TestController).GetMethod(nameof(TestController.GetTest2Async))!.GetParameters(); + + controllerActionDescriptor.Parameters = new List(parametersInfo.Length); + foreach (var parameterInfo in parametersInfo) + { + var parameter = new ControllerParameterDescriptor + { + ParameterInfo = parameterInfo, + }; + controllerActionDescriptor.Parameters.Add(parameter); + } + + var metadata = new List { controllerActionDescriptor }; + + var httpRoute = "v1/profile/users/{userId}/teams/{teamId}"; + var endpoint = new RouteEndpoint( + c => throw new InvalidOperationException("Test"), + RoutePatternFactory.Parse(httpRoute), + 0, + new EndpointMetadataCollection(metadata), + "Endpoint display name"); + + Mock mockHttpRequest = new Mock(); + HttpContext context = new DefaultHttpContext(); + mockHttpRequest.Setup(m => m.HttpContext).Returns(context); + context.SetEndpoint(endpoint); + + var d = new Dictionary + { + { "userId", SimpleClassifications.PublicData } + }; + + var routeUtility = new IncomingHttpRouteUtility(); + var sensitiveParameters = routeUtility.GetSensitiveParameters(httpRoute, mockHttpRequest.Object, d); + Assert.Equal(2, sensitiveParameters.Count); + Assert.True(sensitiveParameters.ContainsKey("userId")); + Assert.Equal(SimpleClassifications.PrivateData, sensitiveParameters.GetValueOrDefault("userId")); + Assert.True(sensitiveParameters.ContainsKey("teamId")); + Assert.Equal(SimpleClassifications.PrivateData, sensitiveParameters.GetValueOrDefault("teamId")); + } + + [Fact] + public void GetSensitiveParameter_2ndCallOnwardForSameRouteReturnsCachedResult() + { + ControllerActionDescriptor controllerActionDescriptor = new ControllerActionDescriptor(); + var parametersInfo = typeof(TestController).GetMethod(nameof(TestController.GetTest2Async))!.GetParameters(); + + controllerActionDescriptor.Parameters = new List(parametersInfo.Length); + foreach (var parameterInfo in parametersInfo) + { + var parameter = new ControllerParameterDescriptor + { + ParameterInfo = parameterInfo, + }; + controllerActionDescriptor.Parameters.Add(parameter); + } + + var metadata = new List { controllerActionDescriptor }; + + var httpRoute = "v1/profile/users/{userId}/teams/{teamId}"; + var endpoint = new RouteEndpoint( + c => throw new InvalidOperationException("Test"), + RoutePatternFactory.Parse(httpRoute), + 0, + new EndpointMetadataCollection(metadata), + "Endpoint display name"); + + Mock mockHttpRequest = new Mock(); + HttpContext context = new DefaultHttpContext(); + mockHttpRequest.Setup(m => m.HttpContext).Returns(context); + context.SetEndpoint(endpoint); + + var d = new Dictionary(); + + var routeUtility = new IncomingHttpRouteUtility(); + var sensitiveParameters = routeUtility.GetSensitiveParameters(httpRoute, mockHttpRequest.Object, d); + Assert.Equal(2, sensitiveParameters.Count); + Assert.True(sensitiveParameters.ContainsKey("userId")); + Assert.Equal(SimpleClassifications.PrivateData, sensitiveParameters.GetValueOrDefault("userId")); + Assert.True(sensitiveParameters.ContainsKey("teamId")); + Assert.Equal(SimpleClassifications.PrivateData, sensitiveParameters.GetValueOrDefault("teamId")); + + d.Add("testKey", SimpleClassifications.PrivateData); + d.Add("userId", SimpleClassifications.PublicData); + sensitiveParameters = routeUtility.GetSensitiveParameters(httpRoute, mockHttpRequest.Object, d); + Assert.Equal(2, sensitiveParameters.Count); + Assert.True(sensitiveParameters.ContainsKey("userId")); + Assert.Equal(SimpleClassifications.PrivateData, sensitiveParameters.GetValueOrDefault("userId")); + Assert.True(sensitiveParameters.ContainsKey("teamId")); + Assert.Equal(SimpleClassifications.PrivateData, sensitiveParameters.GetValueOrDefault("teamId")); + } + + [Fact] + public void GetSensitiveParameter_ControllerActionDescriptorMissing_ReturnsDefault() + { + var metadata = new List { }; + + var httpRoute = "v1/profile/users/{userId}/teams/{teamId}"; + var endpoint = new RouteEndpoint( + c => throw new InvalidOperationException("Test"), + RoutePatternFactory.Parse(httpRoute), + 0, + new EndpointMetadataCollection(metadata), + "Endpoint display name"); + + Mock mockHttpRequest = new Mock(); + HttpContext context = new DefaultHttpContext(); + mockHttpRequest.Setup(m => m.HttpContext).Returns(context); + context.SetEndpoint(endpoint); + + var routeUtility = new IncomingHttpRouteUtility(); + var d = new Dictionary + { + { "testKey", SimpleClassifications.PrivateData } + }; + var sensitiveParameters = routeUtility.GetSensitiveParameters(httpRoute, mockHttpRequest.Object, d); + Assert.Single(sensitiveParameters); + Assert.True(sensitiveParameters.ContainsKey("testKey")); + Assert.Equal(SimpleClassifications.PrivateData, sensitiveParameters.GetValueOrDefault("testKey")); + } + + [Fact] + public void GetSensitiveParameter_EndpointMissing_ReturnsDefault() + { + var metadata = new List { }; + + var httpRoute = "v1/profile/users/{userId}/teams/{teamId}"; + + Mock mockHttpRequest = new Mock(); + HttpContext context = new DefaultHttpContext(); + mockHttpRequest.Setup(m => m.HttpContext).Returns(context); + + var routeUtility = new IncomingHttpRouteUtility(); + var d = new Dictionary + { + { "testKey", SimpleClassifications.PrivateData } + }; + var sensitiveParameters = routeUtility.GetSensitiveParameters(httpRoute, mockHttpRequest.Object, d); + Assert.Single(sensitiveParameters); + Assert.True(sensitiveParameters.ContainsKey("testKey")); + Assert.Equal(SimpleClassifications.PrivateData, sensitiveParameters.GetValueOrDefault("testKey")); + } +#else + [Fact] + public void GetSensitiveParameter_AlwaysReturnsDefault() + { + var httpRoute = "v1/profile/users/{userId}/teams/{teamId}"; + + Mock mockHttpRequest = new Mock(); + HttpContext context = new DefaultHttpContext(); + mockHttpRequest.Setup(m => m.HttpContext).Returns(context); + + var routeUtility = new IncomingHttpRouteUtility(); + var d = new Dictionary + { + { "testKey", SimpleClassifications.PrivateData } + }; + var sensitiveParameters = routeUtility.GetSensitiveParameters(httpRoute, mockHttpRequest.Object, d); + Assert.Single(sensitiveParameters); + Assert.True(sensitiveParameters.ContainsKey("testKey")); + Assert.True(sensitiveParameters.TryGetValue("testKey", out DataClassification classification)); + Assert.Equal(SimpleClassifications.PrivateData, classification); + } +#endif +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Telemetry.Internal.Http/TestController.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Telemetry.Internal.Http/TestController.cs new file mode 100644 index 0000000000..c2368435e8 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Telemetry.Internal.Http/TestController.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETCOREAPP3_1_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Compliance.Testing; + +namespace Microsoft.AspNetCore.Telemetry.Internal.Test; + +[ApiController] +[Route("[controller]")] +public class TestController : ControllerBase +{ + [HttpGet] + [Route("v1/profile/users/{userId}")] + [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "For testing")] + public async Task GetTest1Async([PrivateData] string userId) + { + await Task.Yield(); + return Ok(); + } + + [HttpGet] + [Route("v1/profile/users/{userId}/teams/{teamId}")] + [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "For testing")] + public async Task GetTest2Async([PrivateData] string userId, [PrivateData] string teamId) + { + await Task.Yield(); + return Ok(); + } + + [HttpGet] + [Route("v1/profile/users/{userId}/teams/{teamId}/chats/{chatId}")] + [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "For testing")] + public async Task GetTest3Async([PrivateData] string userId, [PrivateData] string teamId, string chatId) + { + await Task.Yield(); + return Ok(); + } + + [HttpGet] + [Route("v1/profile/users")] + public async Task GetTest4Async() + { + await Task.Yield(); + return Ok(); + } + + [HttpGet] + [Route("v1/profile/users/{userId}/teams/{teamId}/chats/{chatId}")] + [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "For testing")] + public async Task GetTest5Async(string userId, string teamId, string chatId) + { + await Task.Yield(); + return Ok(); + } +} +#endif diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/HttpTracingExtensionsTests.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/HttpTracingExtensionsTests.cs new file mode 100644 index 0000000000..6c91818390 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/HttpTracingExtensionsTests.cs @@ -0,0 +1,171 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +#if !NETCOREAPP3_1_OR_GREATER +using System.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Telemetry.Internal; +using Microsoft.Extensions.Telemetry.Internal; +using Moq; +#endif +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using OpenTelemetry.Trace; +using Xunit; + +using MSOptions = Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Telemetry.Test; + +public class HttpTracingExtensionsTests +{ + [Fact] + public void AddHttpTracing_GivenNullArgument_Throws() + { + var configRoot = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + var configSection = configRoot.GetSection("HttpTracingOptions"); + + Assert.Throws(() => + ((TracerProviderBuilder)null!).AddHttpTracing()); + + Assert.Throws(() => + ((TracerProviderBuilder)null!).AddHttpTracing(options => { })); + + Assert.Throws(() => + ((TracerProviderBuilder)null!).AddHttpTracing(configSection)); + + var services = new ServiceCollection(); + Assert.Throws(() => + services.AddOpenTelemetry().WithTracing(builder => + builder.AddHttpTracing((Action)null!))); + + Assert.Throws(() => + services.AddOpenTelemetry().WithTracing(builder => + builder.AddHttpTracing((IConfigurationSection)null!))); + } + + [Fact] + public void AddHttpTraceEnricher_GivenNullArgument_Throws() + { + var testEnricher = new TestHttpTraceEnricher(MSOptions.Options.Create(new HttpTracingOptions())); + + Assert.Throws(() => + ((TracerProviderBuilder)null!).AddHttpTraceEnricher()); + + Assert.Throws(() => + ((TracerProviderBuilder)null!).AddHttpTraceEnricher(testEnricher)); + + Assert.Throws(() => + ((IServiceCollection)null!).AddHttpTraceEnricher()); + + Assert.Throws(() => + ((IServiceCollection)null!).AddHttpTraceEnricher(testEnricher)); + } + + [Theory] + [CombinatorialData] + public void AddHttpTracing_RegistersHttpUrlProcessor(bool isLoggerPresent) + { + using var host = FakeHost.CreateBuilder(options => options.FakeLogging = false) + .Configure(hostBuilder => + { + if (isLoggerPresent) + { + hostBuilder.ConfigureLogging(builder => builder.AddFakeLogging()); + } + }) + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder.AddHttpTracing())) + .Build(); + + Assert.NotNull(host.Services.GetService>()); + } + + [Fact] + public void AddHttpTracing_GivenRedactorProviderAndTagsToRedact_RegistersHttpUrlProcessorWithRedactorProvider() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddHttpTracing(options => options.RouteParameterDataClasses.Add("TestTag", SimpleClassifications.PrivateData)))) + .Build(); + + Assert.NotNull(host.Services.GetRequiredService()); + } + + [Fact] + public void AddHttpTracing_GivenNoRedactorProviderAndHasTagsToRedact_Throws() + { + using var host = FakeHost.CreateBuilder(new FakeHostOptions { FakeRedaction = false, ValidateOnBuild = false }) + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddHttpTracing(options => options.RouteParameterDataClasses.Add("TestTag", SimpleClassifications.PrivateData)))) + .Build(); + + Assert.Throws( + () => host.Services.GetService()); + } + +#if !NETCOREAPP3_1_OR_GREATER + [Fact] + public void HttpTraceEnrichmentProcessor_OnEndWithNullRequest_ShouldNotCallEnrich() + { + var httpEnricher = new TestHttpTraceEnricher(MSOptions.Options.Create(new HttpTracingOptions())); + var enrichers = new List + { + httpEnricher + }; + + using var httpEnrichmentProcessor = new HttpTraceEnrichmentProcessor(GetRedactionProcessor(), enrichers); + using Activity activity = new Activity("Test"); + httpEnrichmentProcessor.OnEnd(activity); + Assert.False(httpEnricher.IsEnrichCalled); + } + + [Fact] + public void HttpTraceEnrichmentProcessor_OnEndWithRequest_ShouldCallEnrich() + { + var httpContextMock = new Mock(MockBehavior.Default); + httpContextMock.Setup(h => h.Features.Get()).Returns((IEndpointFeature)null!); + var requestMock = new Mock(); + requestMock.SetupGet(r => r.HttpContext).Returns(httpContextMock.Object); + var httpEnricher = new TestHttpTraceEnricher(MSOptions.Options.Create(new HttpTracingOptions())); + var enrichers = new List + { + httpEnricher + }; + + using var httpEnrichmentProcessor = new HttpTraceEnrichmentProcessor(GetRedactionProcessor(), enrichers); + using Activity activity = new Activity("Test"); + + activity.SetCustomProperty(Constants.CustomPropertyHttpRequest, requestMock.Object); + httpEnrichmentProcessor.OnEnd(activity); + Assert.True(httpEnricher.IsEnrichCalled); + } + + private static HttpUrlRedactionProcessor GetRedactionProcessor() + { + var options = MSOptions.Options.Create(new HttpTracingOptions()); + var builder = new ServiceCollection() + .AddFakeRedaction(options => options.RedactionFormat = "Redacted:{0}") + .AddHttpRouteProcessor() + .AddHttpRouteUtilities() + .BuildServiceProvider(); + + var formatter = builder.GetService()!; + var parser = builder.GetService()!; + var utility = builder.GetService()!; + var logger = new Mock>().Object; + + return new HttpUrlRedactionProcessor(options, formatter, parser, utility, logger); + } +#endif +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/HttpTracingOptionsValidationTests.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/HttpTracingOptionsValidationTests.cs new file mode 100644 index 0000000000..e21c1379e5 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/HttpTracingOptionsValidationTests.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Test; + +public class HttpTracingOptionsValidationTests +{ + [Theory] + [MemberData(nameof(ConfigureHttpTracingDelegates))] + public async Task HttpTracingOptions_RequiredProperties_ShouldNotBeNull( + Action configureHttpTracing) + { + using var host = FakeHost + .CreateBuilder() + .ConfigureServices(configureHttpTracing) + .Build(); + + try + { + var exception = await Assert.ThrowsAsync(() => host.StartAsync()); + Assert.Contains(nameof(HttpTracingOptions.RouteParameterDataClasses), exception.Message); + Assert.Contains(nameof(HttpTracingOptions.ExcludePathStartsWith), exception.Message); + } + finally + { + await host.StopAsync(); + } + } + + public static IEnumerable ConfigureHttpTracingDelegates + { + get + { + yield return new object[] + { + (IServiceCollection services) => + { + services.AddOpenTelemetry().WithTracing(builder => builder.AddHttpTracing()); + services.Configure(options => + { + options.RouteParameterDataClasses = null!; + options.ExcludePathStartsWith = null!; + }); + } + }; + + yield return new object[] + { + (IServiceCollection services) => + { + services.AddOpenTelemetry().WithTracing(builder => + builder.AddHttpTracing(options => + { + options.RouteParameterDataClasses = null!; + options.ExcludePathStartsWith = null!; + })); + } + }; + } + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/HttpUrlRedactionProcessorTests.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/HttpUrlRedactionProcessorTests.cs new file mode 100644 index 0000000000..4a5c9d54bb --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/HttpUrlRedactionProcessorTests.cs @@ -0,0 +1,551 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Telemetry.Internal; +using Microsoft.AspNetCore.Telemetry.Test.Internal; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Moq; +using Xunit; +using MSOptions = Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Telemetry.Test; + +public class HttpUrlRedactionProcessorTests +{ + private readonly ILogger _logger = new Mock>().Object; + + [Fact] + public void HttpUrlRedactionProcessor_NullOptionsThrows() + { + var routeParser = GetHttpRouteParser(); + var routeFormatter = GetHttpRouteFormatter(); + var routeUtility = GetHttpRouteUtility(); + + Assert.Throws(() => + new HttpUrlRedactionProcessor(MSOptions.Options.Create(null!), routeFormatter, routeParser, routeUtility, _logger)); + } + + [Fact] + public void HttpUrlRedactionProcessor_OnEnd_WithNullEndpoint_SetsTargetToNull() + { + var httpTracingOptions = new HttpTracingOptions + { + IncludePath = true, + }; + var options = MSOptions.Options.Create(httpTracingOptions); + var routeParser = GetHttpRouteParser(); + var routeFormatter = GetHttpRouteFormatter(); + var routeUtility = GetHttpRouteUtility(); + var processor = new HttpUrlRedactionProcessor(options, routeFormatter, routeParser, routeUtility, _logger); + + using Activity activity = new Activity("test"); + activity.AddTag(Constants.AttributeHttpPath, "/users/testUserId/chats/testChatId"); + activity.AddTag(Constants.AttributeHttpUrl, "http://localhost/users/testUserId/chats/testChatId"); + activity.AddTag(Constants.AttributeHttpTarget, "/users/testUserId/chats/testChatId"); + activity.AddTag("http.status_code", 200); + + processor.Process(activity, GetMockedHttpRequest()); + + Assert.Null(activity.GetTagItem(Constants.AttributeHttpTarget)); + Assert.Null(activity.GetTagItem(Constants.AttributeHttpPath)); + Assert.Null(activity.GetTagItem(Constants.AttributeHttpUrl)); + } + + [Fact] + public void HttpUrlRedactionProcessor_OnEnd_WithNullRouteData_SetsTargetToNull() + { + var httpTracingOptions = new HttpTracingOptions(); + var options = MSOptions.Options.Create(httpTracingOptions); + var routeParser = GetHttpRouteParser(); + var routeFormatter = GetHttpRouteFormatter(); + var routeUtility = GetHttpRouteUtility(); + var logger = new FakeLogger(); + var processor = new HttpUrlRedactionProcessor(options, routeFormatter, routeParser, routeUtility, logger); + + using Activity activity = new Activity("test"); + activity.AddTag(Constants.AttributeHttpPath, "/users/testUserId/chats/testChatId"); + activity.AddTag(Constants.AttributeHttpUrl, "http://localhost/users/testUserId/chats/testChatId"); + activity.AddTag(Constants.AttributeHttpTarget, "/users/testUserId/chats/testChatId"); + activity.AddTag("http.status_code", 200); + activity.SetCustomProperty(Constants.CustomPropertyHttpRequest, GetMockedHttpRequest()); + + processor.Process(activity, GetMockedHttpRequest()); + + Assert.True(string.IsNullOrEmpty((string?)activity.GetTagItem(Constants.AttributeHttpRoute))); + Assert.Null(activity.GetTagItem(Constants.AttributeHttpTarget)); + Assert.Null(activity.GetTagItem(Constants.AttributeHttpPath)); + Assert.Null(activity.GetTagItem(Constants.AttributeHttpUrl)); + + ValidateLoggerRecord(logger, "test", 2); + } + + [Theory] + [InlineData(HttpRouteParameterRedactionMode.Strict, + $"api/users/xxxuser123xxx/unread/chats/{TelemetryConstants.Redacted}/messages", + $"/api/users/{{userId}}/unread/chats/{{chatId}}/messages")] + [InlineData(HttpRouteParameterRedactionMode.Loose, + $"api/users/xxxuser123xxx/unread/chats/chat123/messages", + $"/api/users/{{userId}}/unread/chats/{{chatId}}/messages")] + [InlineData(HttpRouteParameterRedactionMode.None, + $"/api/users/user123/unread/chats/chat123/messages", + $"/api/users/user123/unread/chats/chat123/messages")] + public async Task RedactionModeIsRespected(HttpRouteParameterRedactionMode mode, string expectedPath, string expectedName) + { + using var testTraceProcessor = new TestTraceProcessor(); + using var provider = new TestHttpClientProvider( + "/api/users/{userId}/unread/chats/{chatId}/messages", + builder => + { + builder + .AddHttpTracing( + options => + { + options.RouteParameterDataClasses.Add("userId", SimpleClassifications.PrivateData); + options.IncludePath = true; + options.RequestPathParameterRedactionMode = mode; + }) + .AddTestTraceProcessor(testTraceProcessor); + }, + redaction => redaction.SetFakeRedactor(x => x.RedactionFormat = "xxx{0}xxx", SimpleClassifications.PrivateData)); + using var client = await provider.GetHttpClientAsync(); + using var response = await client.GetAsync("/api/users/user123/unread/chats/chat123/messages").ConfigureAwait(false); + + WaitForProcessorInvocations(testTraceProcessor); + + Assert.Equal(expectedPath, testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpPath)); + Assert.Equal(200, testTraceProcessor.FirstActivity!.GetTagItem("http.status_code")); + Assert.Equal(expectedName, testTraceProcessor.FirstActivity!.DisplayName); + } + + [Fact] + public async Task IncludeFormattedUrlDisabled_ExportsRouteAndParameters() + { + using var testTraceProcessor = new TestTraceProcessor(); + using var provider = new TestHttpClientProvider( + "/some/route/{routeId}", + builder => + { + builder + .AddHttpTracing() + .AddTestTraceProcessor(testTraceProcessor); + }, + redaction => redaction.SetFakeRedactor(x => x.RedactionFormat = "xxx{0}xxx", Array.Empty())); + using var client = await provider.GetHttpClientAsync(); + using var response = await client.GetAsync("/some/route/123").ConfigureAwait(false); + + WaitForProcessorInvocations(testTraceProcessor); + Assert.Equal("/some/route/{routeId}", testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpPath)); + Assert.Equal(TelemetryConstants.Redacted, testTraceProcessor.FirstActivity!.GetTagItem("routeId")); + Assert.Equal(200, testTraceProcessor.FirstActivity!.GetTagItem("http.status_code")); + Assert.Equal("/some/route/{routeId}", testTraceProcessor.FirstActivity!.DisplayName); + Assert.Equal(6, testTraceProcessor.FirstActivity!.TagObjects.Count()); + } + + [Fact] + public async Task IncludeFormattedUrlDisabled_WhenRedactedLengthIsTooLong_ReturnsContstant() + { + using var testTraceProcessor = new TestTraceProcessor(); + using var provider = new TestHttpClientProvider( + "/api/users/{userId}/unread/chats/{chatId}/messages", + builder => + { + builder + .AddHttpTracing(options => options.RouteParameterDataClasses.Add("userId", SimpleClassifications.PrivateData)) + .AddTestTraceProcessor(testTraceProcessor); + }, + redaction => redaction.SetFakeRedactor(x => x.RedactionFormat = "Redacted: {0}", SimpleClassifications.PrivateData)); + using var client = await provider.GetHttpClientAsync(); + using var response = await client.GetAsync("/api/users/testUserId/unread/chats/testChatId/messages").ConfigureAwait(false); + + WaitForProcessorInvocations(testTraceProcessor); + + Assert.Null(testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpUrl)); + Assert.Null(testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpTarget)); + Assert.Equal("/api/users/{userId}/unread/chats/{chatId}/messages", testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpRoute)); + Assert.Equal("/api/users/{userId}/unread/chats/{chatId}/messages", testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpPath)); + Assert.StartsWith("Redacted:", (string)testTraceProcessor.FirstActivity!.GetTagItem("userId")!); + Assert.Equal(TelemetryConstants.Redacted, testTraceProcessor.FirstActivity!.GetTagItem("chatId")); + Assert.Equal(200, testTraceProcessor.FirstActivity!.GetTagItem("http.status_code")); + Assert.Equal("/api/users/{userId}/unread/chats/{chatId}/messages", testTraceProcessor.FirstActivity!.DisplayName); + } + + [Fact] + public async Task IncludeFormattedUrlDisabled_WithTagsToRedactAndHttpRoute_MatchingParametersGetsRedacted() + { + using var testTraceProcessor = new TestTraceProcessor(); + using var provider = new TestHttpClientProvider( + "/users/{userId}/chats/{chatId}", + builder => + { + builder + .AddHttpTracing(options => options.RouteParameterDataClasses.Add("userId", SimpleClassifications.PrivateData)) + .AddTestTraceProcessor(testTraceProcessor) + .AddHttpTraceEnricher(); + }, + _ => { }); + using var client = await provider.GetHttpClientAsync(); + + using var response = await client.GetAsync("/users/testUserId/chats/testChatId").ConfigureAwait(false); + + WaitForProcessorInvocations(testTraceProcessor); + Assert.Null(testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpUrl)); + Assert.Null(testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpTarget)); + Assert.Equal("/users/{userId}/chats/{chatId}", testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpRoute)); + Assert.Equal("/users/{userId}/chats/{chatId}", testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpPath)); + Assert.Equal(string.Empty, testTraceProcessor.FirstActivity!.GetTagItem("userId")); + Assert.Equal(TelemetryConstants.Redacted, testTraceProcessor.FirstActivity!.GetTagItem("chatId")); + Assert.Equal(200, testTraceProcessor.FirstActivity!.GetTagItem("http.status_code")); + Assert.Equal("/users/{userId}/chats/{chatId}", testTraceProcessor.FirstActivity!.DisplayName); + } + + [Fact] + public async Task RedactParameter_WithNonPreciseRedactor_MatchingParameterGetsRedacted() + { + using var testTraceProcessor = new TestTraceProcessor(); + using var provider = new TestHttpClientProvider( + "/get_user/{userId}", + builder => + { + builder + .AddHttpTracing(options => options.RouteParameterDataClasses.Add("userId", SimpleClassifications.PrivateData)) + .AddTestTraceProcessor(testTraceProcessor); + }, + redaction => redaction.SetFakeRedactor(x => x.RedactionFormat = "Redacted: {0}", SimpleClassifications.PrivateData)); + using var client = await provider.GetHttpClientAsync(); + using var response = await client.GetAsync("/get_user/testUserId").ConfigureAwait(false); + + WaitForProcessorInvocations(testTraceProcessor); + + Assert.Null(testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpUrl)); + Assert.Null(testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpTarget)); + Assert.Equal("/get_user/{userId}", testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpRoute)); + Assert.Equal("/get_user/{userId}", testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpPath)); + Assert.StartsWith("Redacted: ", testTraceProcessor.FirstActivity!.GetTagItem("userId")?.ToString()); + Assert.Equal(200, testTraceProcessor.FirstActivity!.GetTagItem("http.status_code")); + Assert.Equal("/get_user/{userId}", testTraceProcessor.FirstActivity!.DisplayName); + } + + [Theory] + [InlineData(HttpRouteParameterRedactionMode.Strict, $"api/users/xxxtestUserIdxxx/unread/chats/{TelemetryConstants.Redacted}/messages")] + [InlineData(HttpRouteParameterRedactionMode.Loose, "api/users/xxxtestUserIdxxx/unread/chats/testChatId/messages")] + public async Task IncludeFormattedUrlEnabled_WithTagsToRedact_MatchingParametersGetsRedacted(HttpRouteParameterRedactionMode mode, string expectedPath) + { + using var testTraceProcessor = new TestTraceProcessor(); + using var provider = new TestHttpClientProvider( + "/api/users/{userId}/unread/chats/{chatId}/messages", + builder => + { + builder + .AddHttpTracing(options => + { + options.IncludePath = true; + options.RequestPathParameterRedactionMode = mode; + options.RouteParameterDataClasses.Add("userId", SimpleClassifications.PrivateData); + }) + .AddTestTraceProcessor(testTraceProcessor); + }, + redaction => redaction.SetFakeRedactor(x => x.RedactionFormat = "xxx{0}xxx", SimpleClassifications.PrivateData)); + using var client = await provider.GetHttpClientAsync(); + + using var response = await client.GetAsync("/api/users/testUserId/unread/chats/testChatId/messages").ConfigureAwait(false); + + WaitForProcessorInvocations(testTraceProcessor); + + Assert.Null(testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpUrl)); + Assert.Null(testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpTarget)); + Assert.Equal("/api/users/{userId}/unread/chats/{chatId}/messages", testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpRoute)); + Assert.Equal(expectedPath, testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpPath)); + Assert.NotEqual("testUserId", testTraceProcessor.FirstActivity!.GetTagItem("userId")); + Assert.NotEqual(TelemetryConstants.Redacted, testTraceProcessor.FirstActivity!.GetTagItem("userId")); + Assert.Null(testTraceProcessor.FirstActivity!.GetTagItem("chatId")); + Assert.Equal(200, testTraceProcessor.FirstActivity!.GetTagItem("http.status_code")); + Assert.Equal("/api/users/{userId}/unread/chats/{chatId}/messages", testTraceProcessor.FirstActivity!.DisplayName); + } + + [Fact] + public async Task IncludeFormattedUrlEnabled_WithTagsToRedactAndHttpRoute_MatchingParametersGetsRedacted() + { + using var testTraceProcessor = new TestTraceProcessor(); + using var provider = new TestHttpClientProvider( + "/api/users/{userId}/unread/chats/{chatId}/messages", + builder => + { + builder + .AddHttpTracing(options => + { + options.IncludePath = true; + options.RouteParameterDataClasses.Add("userId", SimpleClassifications.PrivateData); + }) + .AddTestTraceProcessor(testTraceProcessor) + .AddHttpTraceEnricher(); + }, + redaction => redaction.SetFakeRedactor(x => x.RedactionFormat = "xxx{0}xxx", SimpleClassifications.PrivateData)); + using var client = await provider.GetHttpClientAsync(); + + using var response = await client.GetAsync("/api/users/testUserId/unread/chats/testChatId/messages").ConfigureAwait(false); + + WaitForProcessorInvocations(testTraceProcessor); + + Assert.Null(testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpUrl)); + Assert.Null(testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpTarget)); + Assert.Equal("/api/users/{userId}/unread/chats/{chatId}/messages", testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpRoute)); + Assert.Equal($"api/users/xxxtestUserIdxxx/unread/chats/{TelemetryConstants.Redacted}/messages", testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpPath)); + Assert.NotEqual("testUserId", testTraceProcessor.FirstActivity!.GetTagItem("userId")); + Assert.NotEqual(TelemetryConstants.Redacted, testTraceProcessor.FirstActivity!.GetTagItem("userId")); + Assert.Null(testTraceProcessor.FirstActivity!.GetTagItem("chatId")); + Assert.Equal(200, testTraceProcessor.FirstActivity!.GetTagItem("http.status_code")); + Assert.Equal("/api/users/{userId}/unread/chats/{chatId}/messages", testTraceProcessor.FirstActivity!.DisplayName); + } + + [Theory] + [InlineData(HttpRouteParameterRedactionMode.Strict, $"some/route/{TelemetryConstants.Redacted}")] + [InlineData(HttpRouteParameterRedactionMode.Loose, $"some/route/123")] + public async Task AddHttpTracing_WithIncludeFormattedUrlEnabled_ExportsFormattedUrl(HttpRouteParameterRedactionMode mode, string expectedPath) + { + using var testTraceProcessor = new TestTraceProcessor(); + using var provider = new TestHttpClientProvider( + "/some/route/{routeId}", + builder => + { + builder + .AddHttpTracing(options => + { + options.IncludePath = true; + options.RequestPathParameterRedactionMode = mode; + }) + .AddTestTraceProcessor(testTraceProcessor) + .AddHttpTraceEnricher(); + }, + redaction => redaction.SetFakeRedactor(x => x.RedactionFormat = "xxx{0}xxx", Array.Empty())); + using var client = await provider.GetHttpClientAsync(); + + using var response = await client.GetAsync("/some/route/123").ConfigureAwait(false); + + WaitForProcessorInvocations(testTraceProcessor); + + Assert.Null(testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpUrl)); + Assert.Null(testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpTarget)); + Assert.Equal("/some/route/{routeId}", testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpRoute)); + Assert.Equal(expectedPath, testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpPath)); + Assert.Equal(200, testTraceProcessor.FirstActivity!.GetTagItem("http.status_code")); + Assert.Equal("/some/route/{routeId}", testTraceProcessor.FirstActivity!.DisplayName); + } + + [Fact] + public async Task AddHttpTracing_WithConfigSection_ExportsFormattedUrl() + { + var configRoot = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + var configSection = configRoot.GetSection("HttpTracingOptions"); + + using var testTraceProcessor = new TestTraceProcessor(); + using var provider = new TestHttpClientProvider( + "/some/route/{routeId}", + builder => + { + builder + .AddHttpTracing(configSection) + .AddTestTraceProcessor(testTraceProcessor) + .AddHttpTraceEnricher(new TestEnricher()); + }, + redaction => redaction.SetFakeRedactor(x => x.RedactionFormat = "xxx{0}xxx", Array.Empty())); + + using var client = await provider.GetHttpClientAsync(); + using var response = await client.GetAsync("/some/route/123").ConfigureAwait(false); + + WaitForProcessorInvocations(testTraceProcessor); + + var enrichmentProcessor = provider.Services!.GetRequiredService(); + Assert.NotNull(enrichmentProcessor); + Assert.Null(testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpUrl)); + Assert.Null(testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpTarget)); + Assert.Equal("/some/route/{routeId}", testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpRoute)); + Assert.Equal($"some/route/{TelemetryConstants.Redacted}", testTraceProcessor.FirstActivity!.GetTagItem(Constants.AttributeHttpPath)); + Assert.Equal(200, testTraceProcessor.FirstActivity!.GetTagItem("http.status_code")); + Assert.Equal("/some/route/{routeId}", testTraceProcessor.FirstActivity!.DisplayName); + } + + [Fact] + public async Task AddHttpTracing_WithIncludeFormattedUrlEnabled_ExcludedRouteIsNotExported() + { + using var testExporter = new TestExporter(); + using var exportProcessor = new WrappedActivityExportProcessor(testExporter); + + using var provider = new TestHttpClientProvider( + "/some/route/{routeId}", + builder => + { + builder + .AddHttpTracing(o => + { + o.IncludePath = true; + o.ExcludePathStartsWith.Add("/some/route/{routeId}"); + }) + .AddHttpTraceEnricher() + .AddTestTraceProcessor(exportProcessor); + }, + redaction => redaction.SetFakeRedactor(x => x.RedactionFormat = "xxx{0}xxx", Array.Empty())); + using var client = await provider.GetHttpClientAsync(); + + using var response = await client.GetAsync("/some/route/123").ConfigureAwait(false); + + WaitForExportProcessorInvocations(exportProcessor); + + Assert.True(exportProcessor.IsInvoked); + Assert.False(testExporter.IsInvoked); + Assert.Equal(0, (int)(exportProcessor.FirstActivity!.ActivityTraceFlags & ActivityTraceFlags.Recorded)); + } + + [Fact] + public async Task AddHttpTracing_WithIncludeFormattedUrlDisabled_ExcludedRouteIsNotExported() + { + var configRoot = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + var configSection = configRoot.GetSection("HttpTracingOptionsWithExcludedRoute"); + + using var testExporter = new TestExporter(); + using var exportProcessor = new WrappedActivityExportProcessor(testExporter); + + using var provider = new TestHttpClientProvider( + "/some/route/{routeId}", + builder => + { + builder + .AddHttpTracing(configSection) + .AddHttpTraceEnricher() + .AddTestTraceProcessor(exportProcessor); + }, + redaction => redaction.SetFakeRedactor(x => x.RedactionFormat = "xxx{0}xxx", Array.Empty())); + + using var client = await provider.GetHttpClientAsync(); + using var response = await client.GetAsync("/some/route/123").ConfigureAwait(false); + + WaitForExportProcessorInvocations(exportProcessor); + + Assert.True(exportProcessor.IsInvoked); + Assert.False(testExporter.IsInvoked); + Assert.Equal(0, (int)(exportProcessor.FirstActivity!.ActivityTraceFlags & ActivityTraceFlags.Recorded)); + } + + [Fact] + public async Task AddHttpTracing_WithExcludePathStartsWith_NotMatchedRouteExported() + { + using var testExporter = new TestExporter(); + using var exportProcessor = new WrappedActivityExportProcessor(testExporter); + + using var provider = new TestHttpClientProvider( + "/some/route/{routeId}", + builder => + { + builder + .AddHttpTracing(o => + { + o.IncludePath = true; + o.ExcludePathStartsWith.Add("/route/{routeId}"); + }) + .AddTestTraceProcessor(exportProcessor); + }, + redaction => redaction.SetFakeRedactor(x => x.RedactionFormat = "xxx{0}xxx", Array.Empty())); + + using var client = await provider.GetHttpClientAsync(); + using var response = await client.GetAsync("/some/route/123").ConfigureAwait(false); + + WaitForExportProcessorInvocations(exportProcessor); + + Assert.True(exportProcessor.IsInvoked); + Assert.True(testExporter.IsInvoked); + Assert.Equal(ActivityTraceFlags.Recorded, exportProcessor.FirstActivity!.ActivityTraceFlags & ActivityTraceFlags.Recorded); + } + + private static void WaitForProcessorInvocations(TestTraceProcessor testTraceProcessor) + { + // We need to let End callback execute as it is executed AFTER response was returned. + // In unit tests environment there may be a lot of parallel unit tests executed, so + // giving some breezing room for the End callback to complete + SpinWait.SpinUntil( + () => + { + Thread.Sleep(10); + return testTraceProcessor.IsProcessorInvoked; + }, + TimeSpan.FromSeconds(10)); + } + + private static void WaitForExportProcessorInvocations(WrappedActivityExportProcessor testTraceProcessor) + { + // We need to let End callback execute as it is executed AFTER response was returned. + // In unit tests environment there may be a lot of parallel unit tests executed, so + // giving some breezing room for the End callback to complete + SpinWait.SpinUntil( + () => + { + Thread.Sleep(10); + return testTraceProcessor.IsInvoked; + }, + TimeSpan.FromSeconds(10)); + } + + private static void ValidateLoggerRecord(FakeLogger logger, string activityName, int eventId) + { + FakeLogCollector collector = logger.Collector; + Assert.Equal(2, collector.Count); + + FakeLogRecord record = collector.LatestRecord; + Assert.Equal(eventId, record.Id.Id); + Assert.Equal(LogLevel.Debug, record.Level); + Assert.Contains(activityName, record.Message); + } + + private static IHttpRouteParser GetHttpRouteParser() + { + var builder = new ServiceCollection() + .AddFakeRedaction(options => options.RedactionFormat = "Redacted:{0}") + .AddHttpRouteProcessor() + .BuildServiceProvider(); + + return builder.GetService()!; + } + + private static IHttpRouteFormatter GetHttpRouteFormatter() + { + var builder = new ServiceCollection() + .AddFakeRedaction(options => options.RedactionFormat = "Redacted:{0}") + .AddHttpRouteProcessor() + .BuildServiceProvider(); + + return builder.GetService()!; + } + + private static IIncomingHttpRouteUtility GetHttpRouteUtility() + { + var builder = new ServiceCollection() + .AddFakeRedaction(options => options.RedactionFormat = "Redacted:{0}") + .AddHttpRouteUtilities() + .BuildServiceProvider(); + + return builder.GetService()!; + } + + private static HttpRequest GetMockedHttpRequest() + { + var httpContextMock = new Mock(MockBehavior.Default); + httpContextMock.Setup(h => h.Features.Get()).Returns((IEndpointFeature)null!); + + var requestMock = new Mock(); + requestMock.SetupGet(r => r.HttpContext).Returns(httpContextMock.Object); + + return requestMock.Object; + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/ConfigurationExtensions.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/ConfigurationExtensions.cs new file mode 100644 index 0000000000..bfef319c72 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/ConfigurationExtensions.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Telemetry.Test.Internal; + +internal static class ConfigurationExtensions +{ + public static IServiceCollection TryConfigureRedaction(this IServiceCollection services, Action? config) + { + if (config == null) + { + return services; + } + + return services.AddRedaction(config); + } + + public static IWebHostBuilder TryConfigureServices(this IWebHostBuilder builder, Action? config) + { + if (config == null) + { + return builder; + } + + return builder.ConfigureServices(config); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestEnricher.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestEnricher.cs new file mode 100644 index 0000000000..19f9b11465 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestEnricher.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Routing; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Test.Internal; + +public sealed class TestEnricher : IHttpTraceEnricher +{ + public void Enrich(Activity activity, HttpRequest request) + { + Assert.NotNull(request); + + var endpoint = request?.HttpContext.Features.Get()?.Endpoint; + if (endpoint is RouteEndpoint routeEndpoint) + { + activity.AddTag(Constants.AttributeHttpRoute, routeEndpoint.RoutePattern.RawText); + } + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestEnricher2.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestEnricher2.cs new file mode 100644 index 0000000000..ace02f9b28 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestEnricher2.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Telemetry.Test.Internal; + +public sealed class TestEnricher2 : IHttpTraceEnricher +{ + public void Enrich(Activity activity, HttpRequest request) + { + activity.AddTag(Constants.AttributeHttpRoute, "randomRoute"); + activity.AddTag(Constants.AttributeHttpPath, "randomPath"); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestEventListener.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestEventListener.cs new file mode 100644 index 0000000000..8180d9a720 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestEventListener.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Tracing; + +namespace Microsoft.AspNetCore.Telemetry.Test.Internal; + +internal sealed class TestEventListener : EventListener +{ + public TestEventListener(EventSource eventSource) + { + EnableEvents(eventSource, EventLevel.Error); + } + + public EventWrittenEventArgs? LastEvent { get; private set; } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + LastEvent = eventData; + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestExporter.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestExporter.cs new file mode 100644 index 0000000000..a4dbf1dd31 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestExporter.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using OpenTelemetry; + +namespace Microsoft.AspNetCore.Telemetry.Test.Internal; + +internal sealed class TestExporter : BaseExporter +{ + public bool IsInvoked { get; set; } + + public override ExportResult Export(in Batch batch) + { + IsInvoked = true; + return ExportResult.Success; + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestHttpClientProvider.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestHttpClientProvider.cs new file mode 100644 index 0000000000..57fad47696 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestHttpClientProvider.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +#if NETCOREAPP3_1_OR_GREATER +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +#else +using Microsoft.AspNetCore.Internal; +using Microsoft.AspNetCore.Routing; +#endif +using Microsoft.Extensions.Compliance.Redaction; +using OpenTelemetry.Trace; + +namespace Microsoft.AspNetCore.Telemetry.Test.Internal; + +internal class TestHttpClientProvider : IDisposable +{ + private readonly Action _configureBuilder; + private readonly Action? _configureServices; + private readonly Action? _configureRedaction; + private readonly string _endpointPattern; + private bool _disposedValue; + private System.Net.Http.HttpClient? _httpClient; +#if NETCOREAPP3_1_OR_GREATER + private IHost? _host; +#else + private TestServer? _server; +#endif + public TestHttpClientProvider( + string endpointPattern, + Action configureBuilder, + Action? configureRedaction = null, + Action? configureServices = null) + { + _endpointPattern = endpointPattern; + _configureBuilder = configureBuilder; + _configureRedaction = configureRedaction; + _configureServices = configureServices; + } + + public IServiceProvider? Services { get; private set; } + + public async Task GetHttpClientAsync() + { +#if NETCOREAPP3_1_OR_GREATER + _host = await FakeHost.CreateBuilder(options => options.FakeRedaction = false) + .ConfigureWebHost(webBuilder => webBuilder + .UseTestServer() + .TryConfigureServices(_configureServices) + .ConfigureServices(services => services + .AddRouting() + .AddOpenTelemetry().WithTracing(_configureBuilder).Services + .TryConfigureRedaction(_configureRedaction)) + .Configure(app => app + .UseRouting() + .UseEndpoints(endpoints => + { + endpoints.MapGet(_endpointPattern, async context => + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync("TestCompleted"); + }); + }))) + .StartAsync(); + Services = _host.Services; + _httpClient = _host.GetTestClient(); +#else + var webHostBuilder = new WebHostBuilder() + .TryConfigureServices(_configureServices) + .ConfigureServices(services => services + .AddMvc() + .SetCompatibilityVersion(AspNetCore.Mvc.CompatibilityVersion.Version_2_2) + .Services + .AddRouting() + .AddOpenTelemetry().WithTracing(_configureBuilder).Services + .TryConfigureRedaction(_configureRedaction)) + .Configure(app => app + .UseEndpointRouting() + .UseRouter(routes => + { + routes.MapMiddlewareGet(_endpointPattern, builder => builder.Run(async context => + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync("TestCompleted"); + })); + }) + .UseMvc()); + + _server = new TestServer(webHostBuilder); + await _server.Host.StartAsync(); + + Services = _server.Host.Services; + _httpClient = _server.CreateClient(); +#endif + + return _httpClient; + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _httpClient?.Dispose(); +#if NETCOREAPP3_1_OR_GREATER +#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits + _host?.StopAsync().GetAwaiter().GetResult(); +#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits + _host?.Dispose(); +#else + _server?.Dispose(); +#endif + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestHttpTraceEnricher.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestHttpTraceEnricher.cs new file mode 100644 index 0000000000..119d29e733 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestHttpTraceEnricher.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Telemetry.Test; + +public sealed class TestHttpTraceEnricher : IHttpTraceEnricher +{ + public bool IsEnrichCalled { get; set; } + + public TestHttpTraceEnricher(IOptions _) + { + } + + public void Enrich(Activity activity, HttpRequest request) + { + IsEnrichCalled = true; + Assert.NotNull(request); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestTraceProcessor.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestTraceProcessor.cs new file mode 100644 index 0000000000..b088008c2e --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TestTraceProcessor.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using OpenTelemetry; + +namespace Microsoft.AspNetCore.Telemetry.Test; + +internal sealed class TestTraceProcessor : BaseProcessor +{ + public bool IsProcessorInvoked { get; set; } + + public Activity? FirstActivity { get; set; } + + public override void OnEnd(Activity activity) + { + FirstActivity = activity; + IsProcessorInvoked = true; + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TracerProviderExtensions.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TracerProviderExtensions.cs new file mode 100644 index 0000000000..da90e3d744 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/TracerProviderExtensions.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using OpenTelemetry; +using OpenTelemetry.Trace; + +namespace Microsoft.AspNetCore.Telemetry.Test; + +internal static class TracerProviderExtensions +{ + public static TracerProviderBuilder AddTestTraceProcessor(this TracerProviderBuilder builder, BaseProcessor processor) + { + if (builder is IDeferredTracerProviderBuilder deferredTracerProvider) + { + deferredTracerProvider.Configure((_, builder) => + { + builder.AddProcessor(processor); + }); + } + + return builder; + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/WrappedActivityExportProcessor.cs b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/WrappedActivityExportProcessor.cs new file mode 100644 index 0000000000..a0a61cfaa2 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/Tracing.Http/Internal/WrappedActivityExportProcessor.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using OpenTelemetry; + +namespace Microsoft.AspNetCore.Telemetry.Test.Internal; + +internal sealed class WrappedActivityExportProcessor : SimpleActivityExportProcessor +{ + public bool IsInvoked { get; set; } + + public Activity? FirstActivity { get; private set; } + + public WrappedActivityExportProcessor(BaseExporter exporter) + : base(exporter) + { + } + + public override void OnEnd(Activity data) + { + base.OnEnd(data); + + FirstActivity ??= data; + IsInvoked = true; + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/appsettings.json b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/appsettings.json new file mode 100644 index 0000000000..3c57791980 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Telemetry.Tests/appsettings.json @@ -0,0 +1,10 @@ +{ + "HttpTracingOptions": { + "IncludePath": true + }, + "HttpTracingOptionsWithExcludedRoute": { + "ExcludePathStartsWith": [ + "/some/route/{routeId}" + ] + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Testing.Tests/FakesExtensionsTest.cs b/test/Libraries/Microsoft.AspNetCore.Testing.Tests/FakesExtensionsTest.cs new file mode 100644 index 0000000000..9d462c6968 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Testing.Tests/FakesExtensionsTest.cs @@ -0,0 +1,255 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Testing.Internal; +using Microsoft.AspNetCore.Testing.Test.TestResources; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Testing.Test; + +[SuppressMessage("Reliability", "CA2000", Justification = "HttpClient shouldn't be deliberately disposed.")] +public class FakesExtensionsTest +{ + private static readonly string[] _urlAddresses = { "https://first.com/", "https://second.com/", "https://third.com/" }; + + [Fact] + public async Task UseTestStartup_UsesFakeStartup() + { + using var host = FakeHost.CreateBuilder() + .ConfigureWebHost(webHost => webHost.UseTestStartup().ListenHttpOnAnyPort()) + .Build(); + + Assert.Null(await Record.ExceptionAsync(() => host.StartAsync())); + } + + [Fact] + public async Task ListenHttpOnAnyPort_AddsListener() + { + using var host = await FakeHost.CreateBuilder() + .ConfigureWebHost(webHost => webHost.ListenHttpOnAnyPort().UseTestStartup()) + .StartAsync(); + + Assert.Null(Record.Exception(() => host.GetListenUris())); + } + + [Fact] + public async Task ListenHttpsOnAnyPort_WithoutCertificate_CertificateProvided() + { + using var host = await FakeHost.CreateBuilder() + .ConfigureWebHost(webHost => webHost.UseStartup().ListenHttpsOnAnyPort()) + .StartAsync(); + + var certificate = host.Services.GetRequiredService>().Value.Certificate; + + Assert.NotNull(certificate); + + var client = new HttpClient(new FakeCertificateHttpClientHandler(certificate)) + { + BaseAddress = host.GetListenUris().First() + }; + + Assert.Equal(HttpStatusCode.OK, (await client.GetAsync("/")).StatusCode); + } + + [Fact] + public async Task ListenHttpsOnAnyPort_WithCertificate_UsesTheCertificate() + { + var certificate = FakeSslCertificateFactory.CreateSslCertificate(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureWebHost(webHost => webHost.UseStartup().ListenHttpsOnAnyPort(certificate)) + .StartAsync(); + + var client = host.CreateClient(new FakeCertificateHttpClientHandler(certificate)); + + Assert.Equal(HttpStatusCode.OK, (await client.GetAsync("/")).StatusCode); + } + + [Fact] + public async Task CreateClient_HandlerGiven_UsesTheHandler() + { + var hostMock = CreateHostMock(_urlAddresses); + + using var handler = new ReturningHttpClientHandler(); + using var client = hostMock.Object.CreateClient(handler); + using var response = await client.SendAsync(new HttpRequestMessage()); + + Assert.Equal(HttpStatusCode.Gone, response.StatusCode); + } + + [Fact] + public void CreateClient_NoAddressFilter_UseFirstAddress() + { + var hostMock = CreateHostMock(_urlAddresses); + + using var client = hostMock.Object.CreateClient(); + + Assert.Equal(_urlAddresses[0], client.BaseAddress?.AbsoluteUri); + } + + [Fact] + public void CreateClient_NoAddress_Throws() + { + var hostMock = CreateHostMock(); + + var exception = Record.Exception(() => hostMock.Object.CreateClient(null, _ => false)); + + Assert.IsType(exception); + Assert.Equal("No suitable address found to call the server.", exception.Message); + } + + [Fact] + public void CreateClient_NoSuitableAddress_Throws() + { + var hostMock = CreateHostMock(_urlAddresses); + + var exception = Record.Exception(() => hostMock.Object.CreateClient(null, _ => false)); + + Assert.IsType(exception); + Assert.Equal("No suitable address found to call the server.", exception.Message); + } + + [Fact] + public void CreateClient_NoHost_Throws() + { + var exception = Record.Exception(() => ((IHost)null!).CreateClient(new TestHandler(), _ => true)); + Assert.IsType(exception); + } + + [Fact] + public void CreateClient_NoServer_Throws() + { + var hostMock = CreateHostMock(_urlAddresses); + var services = Mock.Get(hostMock.Object.Services); + services.Setup(x => x.GetService(typeof(IServer))).Returns(null); + + var exception = Record.Exception(() => hostMock.Object.CreateClient(new TestHandler(), _ => true)); + + Assert.IsType(exception); + } + + [Fact] + public void CreateClient_AddressFilterGiven_UseFirstAddressPassingFilter() + { + var hostMock = CreateHostMock(_urlAddresses); + + using var client = hostMock.Object.CreateClient(null, x => x.AbsoluteUri == _urlAddresses[1]); + + Assert.Equal(_urlAddresses[1], client.BaseAddress?.AbsoluteUri); + } + + [Fact] + public async Task CreateClient_UsingHttpsWithoutCertificate_CreatesCertificateAndMakesItWorking() + { + using var host = await FakeHost.CreateBuilder() + .ConfigureWebHost(webHost => webHost.UseStartup().ListenHttpsOnAnyPort()) + .StartAsync(); + + using var client = host.CreateClient(); + + Assert.Equal(HttpStatusCode.OK, (await client.GetAsync("/")).StatusCode); + } + + [Fact] + public async Task CreateClient_UsingHttp_CreatesClient() + { + using var host = await FakeHost.CreateBuilder() + .ConfigureWebHost(webHost => webHost.UseStartup().ListenHttpOnAnyPort()) + .StartAsync(); + + using var client = host.CreateClient(); + + Assert.Equal(HttpStatusCode.OK, (await client.GetAsync("/")).StatusCode); + } + + [Fact] + public void GetListenUris_NoServer_Throws() + { + var hostMock = CreateHostMock(_urlAddresses); + + var services = Mock.Get(hostMock.Object.Services); + services.Setup(x => x.GetService(typeof(IServer))).Returns(null); + + var exception = Record.Exception(() => hostMock.Object.GetListenUris()); + + Assert.IsType(exception); + } + + [Fact] + public void GetListenUris_NoAddressesFeatureInServer_Throws() + { + var hostMock = CreateHostMock(_urlAddresses); + + hostMock.Object.Services.GetRequiredService().Features[typeof(IServerAddressesFeature)] = null; + + var services = Mock.Get(hostMock.Object.Services); + services.Setup(x => x.GetService(typeof(IServer))).Returns(null); + + var exception = Record.Exception(() => hostMock.Object.GetListenUris()); + + Assert.IsType(exception); + } + + [Fact] + public void GetUri_NoHost_Throws() + { + var exception = Record.Exception(() => ((IHost)null!).GetListenUris()); + Assert.IsType(exception); + } + + [Fact] + public void GetListenUris_PassesAddresses() + { + var hostMock = CreateHostMock(_urlAddresses); + + Assert.Collection(hostMock.Object.GetListenUris(), + address => Assert.Equal(address.AbsoluteUri, _urlAddresses[0]), + address => Assert.Equal(address.AbsoluteUri, _urlAddresses[1]), + address => Assert.Equal(address.AbsoluteUri, _urlAddresses[2])); + } + + [Fact] + public void GetListenUris_ReplacesEmptyAddressWithLocalhost() + { + var hostMock = CreateHostMock("https://[::]"); + Assert.StartsWith("https://localhost", hostMock.Object.GetListenUris().Single().AbsoluteUri); + } + + private static Mock CreateHostMock(params string[] addresses) + { + var addressesFeature = new ServerAddressesFeature(); + + foreach (var address in addresses) + { + addressesFeature.Addresses.Add(address); + } + + var features = new FeatureCollection { [typeof(IServerAddressesFeature)] = addressesFeature }; + + var mockServer = new Mock(); + mockServer.Setup(x => x.Features).Returns(features); + + var serviceProviderMock = new Mock(); + serviceProviderMock.Setup(x => x.GetService(typeof(IServer))).Returns(mockServer.Object); + + var hostMock = new Mock(); + hostMock.SetupGet(x => x.Services).Returns(serviceProviderMock.Object); + + return hostMock; + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Testing.Tests/Internal/FakeCertificateFactoryTest.cs b/test/Libraries/Microsoft.AspNetCore.Testing.Tests/Internal/FakeCertificateFactoryTest.cs new file mode 100644 index 0000000000..db29adac68 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Testing.Tests/Internal/FakeCertificateFactoryTest.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Testing.Internal; +using Microsoft.TestUtilities; +using Xunit; + +namespace Microsoft.AspNetCore.Testing.Test.Internal; + +public class FakeCertificateFactoryTest +{ + [Fact] + public void Create_CreatesCertificate() + { + using var certificate = FakeSslCertificateFactory.CreateSslCertificate(); + + Assert.Equal("CN=r9-self-signed-unit-test-certificate", certificate.SubjectName.Name); + Assert.Equal("localhost", certificate.GetNameInfo(X509NameType.DnsFromAlternativeName, false)); + Assert.True(DateTime.Now > certificate.NotBefore + TimeSpan.FromHours(1)); + Assert.True(DateTime.Now < certificate.NotAfter - TimeSpan.FromHours(1)); + Assert.False(certificate.Extensions.OfType().Single().Critical); + } + + [ConditionalTheory] + [OSSkipCondition(OperatingSystems.Linux)] + [InlineData(false)] + [InlineData(true)] + public void GenerateRsa_RunsOnWindows_GeneratesRsa(bool runsOnWindows) + { + Assert.NotNull(FakeSslCertificateFactory.GenerateRsa(runsOnWindows)); + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Windows)] + public void GenerateRsa_DoesNotRunOnWindows_GeneratesRsa() + { + Assert.NotNull(FakeSslCertificateFactory.GenerateRsa(runsOnWindows: false)); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Testing.Tests/Internal/FakeCertificateHttpClientHandlerTest.cs b/test/Libraries/Microsoft.AspNetCore.Testing.Tests/Internal/FakeCertificateHttpClientHandlerTest.cs new file mode 100644 index 0000000000..bd94df7ae9 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Testing.Tests/Internal/FakeCertificateHttpClientHandlerTest.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Testing.Internal; +using Xunit; + +namespace Microsoft.AspNetCore.Testing.Test.Internal; + +[SuppressMessage("Design", "CA1063", Justification = "not needed")] +public class FakeCertificateHttpClientHandlerTest : IDisposable +{ + private readonly X509Certificate2 _certificate = FakeSslCertificateFactory.CreateSslCertificate(); + private readonly X509Certificate2 _anotherCertificate = FakeSslCertificateFactory.CreateSslCertificate(); + private readonly HttpRequestMessage _request = new(); + + [Fact] + public void ServerCertificateCustomValidationCallback_OurCertProvided_ReturnsTrue() + { + using var sut = new FakeCertificateHttpClientHandler(_certificate); + + Assert.True(sut.ServerCertificateCustomValidationCallback!( + _request, + _certificate, + null, + SslPolicyErrors.RemoteCertificateChainErrors)); + } + + [Fact] + public void ServerCertificateCustomValidationCallback_DifferentCertAndNoErrors_ReturnsTrue() + { + using var sut = new FakeCertificateHttpClientHandler(_certificate); + + Assert.True(sut.ServerCertificateCustomValidationCallback!( + _request, + _anotherCertificate, + null, + SslPolicyErrors.None)); + } + + [Fact] + public void ServerCertificateCustomValidationCallback_DifferentCertAndErrors_ReturnsFalse() + { + using var sut = new FakeCertificateHttpClientHandler(_certificate); + + Assert.False(sut.ServerCertificateCustomValidationCallback!( + _request, + _anotherCertificate, + null, + SslPolicyErrors.RemoteCertificateChainErrors)); + } + + [SuppressMessage("Usage", "CA1816", Justification = "not needed")] + public void Dispose() + { + _certificate.Dispose(); + _anotherCertificate.Dispose(); + _request.Dispose(); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Testing.Tests/Internal/FakeStartupTest.cs b/test/Libraries/Microsoft.AspNetCore.Testing.Tests/Internal/FakeStartupTest.cs new file mode 100644 index 0000000000..9144271656 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Testing.Tests/Internal/FakeStartupTest.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Testing.Internal; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Testing.Test.Internal; + +public class FakeStartupTest +{ + [Fact] + public void MethodsDoNothing() + { + var sut = new FakeStartup(); + + var exception = Record.Exception(() => + { + sut.Configure(null!); + sut.Configure(new ApplicationBuilder(new ServiceCollection().BuildServiceProvider())); + sut.ConfigureServices(null!); + sut.ConfigureServices(new ServiceCollection()); + }); + + Assert.Null(exception); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Testing.Tests/Microsoft.AspNetCore.Testing.Tests.csproj b/test/Libraries/Microsoft.AspNetCore.Testing.Tests/Microsoft.AspNetCore.Testing.Tests.csproj new file mode 100644 index 0000000000..9cc1f6898f --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Testing.Tests/Microsoft.AspNetCore.Testing.Tests.csproj @@ -0,0 +1,15 @@ + + + Microsoft.AspNetCore.Testing + Unit tests for Microsoft.AspNetCore.Testing + + + + $(NetCoreTargetFrameworks) + + + + + + + diff --git a/test/Libraries/Microsoft.AspNetCore.Testing.Tests/TestResources/ReturningHttpClientHandler.cs b/test/Libraries/Microsoft.AspNetCore.Testing.Tests/TestResources/ReturningHttpClientHandler.cs new file mode 100644 index 0000000000..425c7a5c97 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Testing.Tests/TestResources/ReturningHttpClientHandler.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Testing.Test.TestResources; + +public class ReturningHttpClientHandler : HttpClientHandler +{ +#if !NETCOREAPP3_1 + protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) + { + return new(HttpStatusCode.Gone); + } +#endif + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Gone)); + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Testing.Tests/TestResources/Startup.cs b/test/Libraries/Microsoft.AspNetCore.Testing.Tests/TestResources/Startup.cs new file mode 100644 index 0000000000..e1e9cc0a3f --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Testing.Tests/TestResources/Startup.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Testing.Test.TestResources; + +[SuppressMessage("Performance", "CA1822", Justification = "convention")] +public class Startup +{ +#if !NET6_0_OR_GREATER + [SuppressMessage("Minor Code Smell", "S3257:Declarations and initializations should be as concise as possible", Justification = "Type parameters needed for proper resolution")] +#endif + public void Configure(IApplicationBuilder app) => app.Use((HttpContext _, Func _) => Task.CompletedTask); + + public void ConfigureServices(IServiceCollection _) + { + // only need the class for testing + } +} diff --git a/test/Libraries/Microsoft.AspNetCore.Testing.Tests/TestResources/TestHandler.cs b/test/Libraries/Microsoft.AspNetCore.Testing.Tests/TestResources/TestHandler.cs new file mode 100644 index 0000000000..72f10ff256 --- /dev/null +++ b/test/Libraries/Microsoft.AspNetCore.Testing.Tests/TestResources/TestHandler.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Testing.Test.TestResources; + +public class TestHandler : HttpClientHandler +{ + protected override Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/AcceptanceTests.cs b/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/AcceptanceTests.cs new file mode 100644 index 0000000000..d66842b018 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/AcceptanceTests.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using AutoFixture; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.AmbientMetadata.Test; + +public class AcceptanceTests +{ + private static readonly Fixture _fixture = new(); + private static readonly ApplicationMetadata _metadata = new() + { + BuildVersion = _fixture.Create(), + DeploymentRing = _fixture.Create(), + ApplicationName = _fixture.Create(), + }; + + [Theory] + [InlineData("ambientmetadata:application")] + [InlineData(null)] + public async Task UseApplicationMetadata_CreatesPopulatesAndRegistersOptions(string? sectionName) => + await RunAsync( + (options, hostEnvironment) => + { + options.BuildVersion.Should().Be(_metadata.BuildVersion); + options.DeploymentRing.Should().Be(_metadata.DeploymentRing); + options.ApplicationName.Should().Be(_metadata.ApplicationName); + options.EnvironmentName.Should().Be(hostEnvironment.EnvironmentName); + + return Task.CompletedTask; + }, + sectionName); + + private static async Task RunAsync(Func func, string? sectionName) + { + using var host = await FakeHost.CreateBuilder() + + // need to set applicationName manually, because + // netfx console test runner cannot get assebly name + // to be able to set it automatically + // see https://source.dot.net/#Microsoft.Extensions.Hosting/HostBuilder.cs,240 + .ConfigureHostConfiguration("applicationname", _metadata.ApplicationName) + .UseApplicationMetadata(sectionName ?? "ambientmetadata:application") + .ConfigureServices((_, services) => services.AddApplicationMetadata(metadata => + { + metadata.BuildVersion = _metadata.BuildVersion; + metadata.DeploymentRing = _metadata.DeploymentRing; + })) + .StartAsync(); + + await func(host.Services.GetRequiredService>().Value, + host.Services.GetRequiredService()); + await host.StopAsync(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataExtensionsTests.cs new file mode 100644 index 0000000000..5091bd87e2 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataExtensionsTests.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using AutoFixture; +using FluentAssertions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.AmbientMetadata.Test; + +public class ApplicationMetadataExtensionsTests +{ + private const string TestEnvironmentName = "fancy environment"; + private const string TestApplicationName = "fancy application"; + + private readonly Fixture _fixture = new(); + private readonly Mock _hostEnvironment = new(); + + public ApplicationMetadataExtensionsTests() + { + _hostEnvironment.Setup(h => h.EnvironmentName).Returns(TestEnvironmentName); + _hostEnvironment.Setup(h => h.ApplicationName).Returns(TestApplicationName); + } + + [Fact] + public void ApplicationMetadataExtensions_GivenAnyNullArgument_Throws() + { + var serviceCollection = new ServiceCollection(); + var config = new ConfigurationBuilder().Build(); + + Assert.Throws(() => ((IServiceCollection)null!).AddApplicationMetadata(config.GetSection(string.Empty))); + Assert.Throws(() => ((IServiceCollection)null!).AddApplicationMetadata(_ => { })); + Assert.Throws(() => serviceCollection.AddApplicationMetadata((Action)null!)); + Assert.Throws(() => serviceCollection.AddApplicationMetadata((IConfigurationSection)null!)); + Assert.Throws(() => ((IHostBuilder)null!).UseApplicationMetadata(_fixture.Create())); + Assert.Throws(() => new ConfigurationBuilder().AddApplicationMetadata(null!)); + Assert.Throws(() => ((IConfigurationBuilder)null!).AddApplicationMetadata(null!)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData(" ")] + public void AddApplicationMetadata_InvalidSectionName_Throws(string? sectionName) + { + var act = () => new ConfigurationBuilder().AddApplicationMetadata(_hostEnvironment.Object, sectionName!); + act.Should().Throw(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData(" ")] + public void UseApplicationMetadata_InvalidSectionName_Throws(string? sectionName) + { + var act = () => FakeHost.CreateBuilder().UseApplicationMetadata(sectionName!); + act.Should().Throw(); + } + + [Fact] + public void AddApplicationMetadata_BuildsConfig() + { + var expectedConfig = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [$"{nameof(ApplicationMetadata)}:{nameof(ApplicationMetadata.ApplicationName)}"] = TestApplicationName, + [$"{nameof(ApplicationMetadata)}:{nameof(ApplicationMetadata.EnvironmentName)}"] = TestEnvironmentName, + }) + .Build(); + var expectedConfigSection = expectedConfig.GetSection(nameof(ApplicationMetadata)); + + var actualConfig = new ConfigurationBuilder().AddApplicationMetadata(_hostEnvironment.Object, nameof(ApplicationMetadata)).Build(); + var actualConfigSection = actualConfig.GetSection(nameof(ApplicationMetadata)); + + actualConfigSection.Should().BeEquivalentTo(expectedConfigSection); + } + + [Fact] + public void AddApplicationMetadata_GivenConfigurationSection_RegistersMetadata() + { + var expectedMetadata = new ApplicationMetadata + { + ApplicationName = _fixture.Create(), + EnvironmentName = _fixture.Create(), + BuildVersion = _fixture.Create(), + DeploymentRing = _fixture.Create(), + }; + + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [$"{nameof(ApplicationMetadata)}:{nameof(ApplicationMetadata.ApplicationName)}"] = expectedMetadata.ApplicationName, + [$"{nameof(ApplicationMetadata)}:{nameof(ApplicationMetadata.EnvironmentName)}"] = expectedMetadata.EnvironmentName, + [$"{nameof(ApplicationMetadata)}:{nameof(ApplicationMetadata.BuildVersion)}"] = expectedMetadata.BuildVersion, + [$"{nameof(ApplicationMetadata)}:{nameof(ApplicationMetadata.DeploymentRing)}"] = expectedMetadata.DeploymentRing, + }) + .Build(); + + var configurationSection = config.GetSection(nameof(ApplicationMetadata)); + + using var provider = new ServiceCollection() + .AddApplicationMetadata(configurationSection) + .BuildServiceProvider(); + + var actualMetadata = provider.GetRequiredService>().Value; + + actualMetadata.Should().BeEquivalentTo(expectedMetadata); + } + + [Fact] + public void AddApplicationMetadata_GivenConfigurationDelegate_RegistersMetadata() + { + var expectedMetadata = new ApplicationMetadata + { + ApplicationName = _fixture.Create(), + EnvironmentName = _fixture.Create(), + BuildVersion = _fixture.Create(), + DeploymentRing = _fixture.Create(), + }; + + using var provider = new ServiceCollection() + .AddApplicationMetadata(m => + { + m.ApplicationName = expectedMetadata.ApplicationName; + m.EnvironmentName = expectedMetadata.EnvironmentName; + m.BuildVersion = expectedMetadata.BuildVersion; + m.DeploymentRing = expectedMetadata.DeploymentRing; + }) + .BuildServiceProvider(); + + var actualMetadata = provider.GetRequiredService>().Value; + + actualMetadata.Should().BeEquivalentTo(expectedMetadata); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataSourceTests.cs b/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataSourceTests.cs new file mode 100644 index 0000000000..4e94987448 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataSourceTests.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using AutoFixture; +using FluentAssertions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.AmbientMetadata.Test; + +public class ApplicationMetadataSourceTests +{ + private const string TestEnvironmentName = "fancy environment"; + private const string TestApplicationName = "fancy application"; + + private readonly Mock _hostEnvironment = new(); + private readonly Fixture _fixture = new(); + + public ApplicationMetadataSourceTests() + { + _hostEnvironment.Setup(h => h.EnvironmentName).Returns(TestEnvironmentName); + _hostEnvironment.Setup(h => h.ApplicationName).Returns(TestApplicationName); + } + + [Fact] + public void ApplicationMetadataSource_CanConstruct() => new ApplicationMetadataSource(_hostEnvironment.Object, _fixture.Create()).Should().NotBeNull(); + + [Fact] + public void ApplicationMetadataSource_NullHostEnvironment_Throws() + { + var act = () => new ApplicationMetadataSource(null!, _fixture.Create()); + + act.Should().Throw(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData(" ")] + public void ApplicationMetadataSource_InvalidSectionName_Throws(string? sectionName) + { + var act = () => new ApplicationMetadataSource(_hostEnvironment.Object, sectionName!); + act.Should().Throw(); + } + + [Fact] + public void ApplicationMetadataSource_Build_BuildsProviderCorrectly() + { + var testSectionName = _fixture.Create(); + var sut = new ApplicationMetadataSource(_hostEnvironment.Object, testSectionName); + var configurationBuilder = new ConfigurationBuilder(); + + var provider = sut.Build(configurationBuilder); + + var result = provider.TryGet($"{testSectionName}:{nameof(ApplicationMetadata.EnvironmentName)}", out var actualEnvironmentName); + result.Should().BeTrue(); + actualEnvironmentName.Should().Be(TestEnvironmentName); + + result = provider.TryGet($"{testSectionName}:{nameof(ApplicationMetadata.ApplicationName)}", out var actualApplicationName); + result.Should().BeTrue(); + actualApplicationName.Should().Be(TestApplicationName); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataTests.cs b/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataTests.cs new file mode 100644 index 0000000000..6306f31993 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataTests.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using AutoFixture; +using FluentAssertions; +using Xunit; + +namespace Microsoft.Extensions.AmbientMetadata.Test; + +public class ApplicationMetadataTests +{ + private readonly ApplicationMetadata _sut; + private readonly Fixture _fixture; + + public ApplicationMetadataTests() + { + _sut = new ApplicationMetadata(); + _fixture = new Fixture(); + } + + [Fact] + public void CanConstruct() => new ApplicationMetadata().Should().NotBeNull(); + + [Fact] + public void DefaultChecks() + { + var applicationMetadata = new ApplicationMetadata(); + + applicationMetadata.ApplicationName.Should().BeEmpty(); + applicationMetadata.EnvironmentName.Should().BeEmpty(); + applicationMetadata.BuildVersion.Should().BeNull(); + applicationMetadata.DeploymentRing.Should().BeNull(); + } + + [Fact] + public void ApplicationMetadata_ApplicationName_CanSetAndGet() + { + var testValue = _fixture.Create(); + + _sut.ApplicationName = testValue; + + _sut.ApplicationName.Should().Be(testValue); + } + + [Fact] + public void ApplicationMetadata_EnvironmentName_CanSetAndGet() + { + var testValue = _fixture.Create(); + + _sut.EnvironmentName = testValue; + + _sut.EnvironmentName.Should().Be(testValue); + } + + [Fact] + public void ApplicationMetadata_BuildVersion_CanSetAndGet() + { + var testValue = _fixture.Create(); + + _sut.BuildVersion = testValue; + + _sut.BuildVersion.Should().Be(testValue); + } + + [Fact] + public void ApplicationMetadata_DeploymentRing_CanSetAndGet() + { + var testValue = _fixture.Create(); + + _sut.DeploymentRing = testValue; + + _sut.DeploymentRing.Should().Be(testValue); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataValidatorTests.cs b/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataValidatorTests.cs new file mode 100644 index 0000000000..73bbb1f4bd --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/ApplicationMetadataValidatorTests.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Xunit; + +namespace Microsoft.Extensions.AmbientMetadata.Test; + +public class ApplicationMetadataValidatorTests +{ + [Fact] + public void Ctor_CreatesAnInstance() + { + var act = () => _ = new ApplicationMetadataValidator(); + + act.Should().NotThrow(); + } + + [Fact] + public void Validate_ObjectHasNoIssues_Success() + { + var validator = new ApplicationMetadataValidator(); + var result = validator.Validate( + "model", + new ApplicationMetadata { ApplicationName = "test", EnvironmentName = "test2" }); + + result.Succeeded.Should().BeTrue(); + } + + [Fact] + public void Validate_NullApplicationName_Fails() + { + var validator = new ApplicationMetadataValidator(); + var applicationMetadata = new ApplicationMetadata { ApplicationName = null! }; + + validator.Validate("model", applicationMetadata).Failed.Should().BeTrue(); + } + + [Fact] + public void Validate_NullEnvironmentName_Fails() + { + var validator = new ApplicationMetadataValidator(); + var applicationMetadata = new ApplicationMetadata { EnvironmentName = null! }; + + validator.Validate("model", applicationMetadata).Failed.Should().BeTrue(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/Microsoft.Extensions.AmbientMetadata.Application.Tests.csproj b/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/Microsoft.Extensions.AmbientMetadata.Application.Tests.csproj new file mode 100644 index 0000000000..f5e82e665a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AmbientMetadata.Application.Tests/Microsoft.Extensions.AmbientMetadata.Application.Tests.csproj @@ -0,0 +1,15 @@ + + + Microsoft.Extensions.AmbientMetadata.Test + Unit tests for Microsoft.Extensions.AmbientMetadata.Application. + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncContextServiceCollectionExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncContextServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000000..651dc2b813 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncContextServiceCollectionExtensionsTests.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Xunit; + +namespace Microsoft.Extensions.AsyncState.Test; + +public class AsyncContextServiceCollectionExtensionsTests +{ + [Fact] + public void AddAsyncStateCore_Throws_WhenNullService() + { + Assert.Throws(() => AsyncStateExtensions.AddAsyncStateCore(null!)); + } + + [Fact] + public void AddAsyncStateCore_AddsWithCorrectLifetime() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddAsyncStateCore(); + + // Assert + var serviceDescriptor = services.First(x => x.ServiceType == typeof(IAsyncContext<>)); + Assert.Equal(ServiceLifetime.Singleton, serviceDescriptor.Lifetime); + + serviceDescriptor = services.First(x => x.ServiceType == typeof(IAsyncState)); + Assert.Equal(ServiceLifetime.Singleton, serviceDescriptor.Lifetime); + + serviceDescriptor = services.First(x => x.ServiceType == typeof(IAsyncLocalContext<>)); + Assert.Equal(ServiceLifetime.Singleton, serviceDescriptor.Lifetime); + } + + [Fact] + public void TryRemoveAsyncStateCore_Throws_WhenNullService() + { + Assert.Throws(() => AsyncStateExtensions.TryRemoveAsyncStateCore(null!)); + } + + [Fact] + public void TryRemoveAsyncStateCore_RemovesAsyncContext() + { + var services = new ServiceCollection(); + + services.AddAsyncStateCore(); + + Assert.NotNull(services.FirstOrDefault(x => + (x.ServiceType == typeof(IAsyncContext<>)) && (x.ImplementationType == typeof(AsyncContext<>)))); + + services.TryRemoveAsyncStateCore(); + + Assert.Null(services.FirstOrDefault(x => + (x.ServiceType == typeof(IAsyncContext<>)) && (x.ImplementationType == typeof(AsyncContext<>)))); + } + + [Fact] + public void TryRemoveSingleton_DoesNothingToEmptyServices() + { + var services = new ServiceCollection(); + + services.TryRemoveSingleton(typeof(IThing), typeof(Thing)); + + Assert.Empty(services); + } + + [Fact] + public void TryRemoveSingleton_RemovesWhenPresent() + { + var services = new ServiceCollection(); + + services.TryAddSingleton(); + + Assert.Single(services); + var descriptor = services[0]; + Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime); + Assert.Equal(typeof(IThing), descriptor.ServiceType); + Assert.Equal(typeof(Thing), descriptor.ImplementationType); + + services.TryRemoveSingleton(typeof(IThing), typeof(Thing)); + + Assert.Empty(services); + } + + [Fact] + public void TryRemoveSingleton_DoesNotRemoveOtherThanSpecified() + { + var services = new ServiceCollection(); + + services.TryAddSingleton(); + + Assert.Single(services); + var descriptor = services[0]; + Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime); + Assert.Equal(typeof(IThing), descriptor.ServiceType); + Assert.Equal(typeof(Thing), descriptor.ImplementationType); + + services.TryRemoveSingleton(typeof(IThing), typeof(AnotherThing)); + + Assert.Single(services); + var descriptor2 = services[0]; + Assert.Equal(ServiceLifetime.Singleton, descriptor2.Lifetime); + Assert.Equal(typeof(IThing), descriptor2.ServiceType); + Assert.Equal(typeof(Thing), descriptor2.ImplementationType); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncContextTests.cs b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncContextTests.cs new file mode 100644 index 0000000000..c7cca81bdf --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncContextTests.cs @@ -0,0 +1,256 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.AsyncState.Test; +public class AsyncContextTests +{ + [Fact] + public async Task CreateAsyncContext_BeforeInitialize() + { + var state = new AsyncState(); + var context1 = new AsyncContext(state); + var context2 = new AsyncContext(state); + var obj1 = new Thing(); + var obj2 = new Thing(); + + Assert.Equal(2, state.ContextCount); + state.Initialize(); + + await Task.Run(() => context1.Set(obj1)); + + await Task.Run(() => + { + Assert.Same(obj1, context1.Get()); + + Assert.Null(context2.Get()); + }); + } + + [Fact] + public async Task CreateAsyncContext_AfterInitialize() + { + var state = new AsyncState(); + state.Initialize(); + + var context1 = new AsyncContext(state); + var context2 = new AsyncContext(state); + + Assert.Equal(2, state.ContextCount); + + var obj1 = new Thing(); + + await Task.Run(() => context1.Set(obj1)); + + await Task.Run(() => + { + Assert.Same(obj1, context1.Get()); + + Assert.Null(context2.Get()); + }); + } + + [Fact] + public async Task CreateAsyncContext_BeforeAndAfterInitialize() + { + var state = new AsyncState(); + + var context1 = new AsyncContext(state); + state.Initialize(); + var context2 = new AsyncContext(state); + var obj1 = new Thing(); + var obj2 = new Thing(); + + Assert.Equal(2, state.ContextCount); + + await Task.Run(() => + { + context1.Set(obj1); + context2.Set(obj2); + }); + + await Task.Run(() => + { + Assert.Same(obj1, context1.Get()); + + Assert.Same(obj2, context2.Get()); + }); + } + + [Fact] + public async Task Tryget_BeforeAndAfterInitialize() + { + var state = new AsyncState(); + var context1 = new AsyncContext(state); + Assert.False(context1.TryGet(out _)); + state.Initialize(); + var obj1 = new Thing(); + + Assert.Equal(1, state.ContextCount); + + await Task.Run(() => + { + Assert.True(context1.TryGet(out var ctx1)); + Assert.Null(ctx1); + context1.Set(obj1); + }); + + await Task.Run(() => + { + Assert.True(context1.TryGet(out var ctx1)); + Assert.Same(obj1, ctx1); + }); + } + + [Fact] + public async Task CreateAsyncContextInAsync_AfterInitialize() + { + var state = new AsyncState(); + + var context1 = new AsyncContext(state); + var obj1 = new Thing(); + + state.Initialize(); + Assert.Equal(1, state.ContextCount); + + await Task.Run(async () => + { + context1.Set(obj1); + + var context2 = new AsyncContext(state); + var obj2 = new Thing(); + context2.Set(obj2); + + await Task.Run(() => + { + Assert.Same(obj1, context1.Get()); + + Assert.Same(obj2, context2.Get()); + }); + }); + } + + [Fact] + public void Get_WhenNotInitialized() + { + var state = new AsyncState(); + var context1 = new AsyncContext(state); + + Assert.Throws(() => _ = context1.Get()); + } + + [Fact] + public void TryGet_WhenNotInitialized() + { + var state = new AsyncState(); + var context1 = new AsyncContext(state); + + Assert.False(context1.TryGet(out var context)); + Assert.Null(context); + } + + [Fact] + public void Set_WhenNotInitialized() + { + var state = new AsyncState(); + var context1 = new AsyncContext(state); + + Assert.Throws(() => context1.Set(new Thing())); + } + + [Fact] + public void Reset_DoesNotDisposeObjects() + { + var state = new AsyncState(); + var context = new AsyncContext(state); + var obj = new Mock(); + obj.Setup(m => m.Dispose()); + + state.Initialize(); + context.Set(obj.Object); + state.Reset(); + + obj.Verify(m => m.Dispose(), Times.Never); + } + + [Fact] + public async Task TwoAsyncFlows_WithDiffrentAsyncStates() + { + var state = new AsyncState(); + + var task1 = Task.Run(async () => + { + var context = new AsyncContext(state); + state.Initialize(); + var obj = new Thing(); + + await Task.Run(async () => + { + context.Set(obj); + + await Task.Run(() => Assert.Same(obj, context.Get())); + }); + + state.Reset(); + }); + + var task2 = Task.Run(async () => + { + var context = new AsyncContext(state); + state.Initialize(); + var obj = string.Empty; + + await Task.Run(async () => + { + context.Set(obj); + + await Task.Run(() => Assert.Same(obj, context.Get())); + }); + + state.Reset(); + }); + + await Task.WhenAll(task1, task2); + Assert.Equal(2, state.ContextCount); + } + + [Fact] + public async Task IndependentAsyncFlows_WithSameAsyncState() + { + var state = new AsyncState(); + var context = new AsyncContext(state); + + Func setAsyncState = async () => + { + state.Initialize(); + + await Task.Run(async () => + { + var obj2 = new Thing(); + context.Set(obj2); + + await Task.Run(() => + { + Assert.Same(obj2, context.Get()); + }); + }); + + state.Reset(); + }; + + var tasks = new Task[10]; + + for (int i = 0; i < tasks.Length; i++) + { + tasks[i] = Task.Run(setAsyncState); + } + + await Task.WhenAll(tasks); + + Assert.Equal(1, state.ContextCount); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTests.cs b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTests.cs new file mode 100644 index 0000000000..f7a7b050eb --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTests.cs @@ -0,0 +1,232 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.AsyncState.Test; + +[SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Testing")] +public class AsyncStateTests +{ + [Fact] + public async Task GettingAsyncContextReturnsAsyncContext() + { + var state = new AsyncState(); + var context = new Thing(); + + var token = state.RegisterAsyncContext(); + + static Task SetAsyncContext(AsyncState state, IThing context, AsyncStateToken token) + { + state.Initialize(); + state.Set(token, context); + + return Task.CompletedTask; + } + + await SetAsyncContext(state, context, token).ConfigureAwait(false); + + Assert.Same(context, state.Get(token)); + } + + [Fact] + public async Task GettingAsyncContextReturnsNullAsyncContextIfSetToNull() + { + var state = new AsyncState(); + var context = new Thing(); + var token = state.RegisterAsyncContext(); + state.Initialize(); + state.Set(token, context); + + var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var waitForNullTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var afterNullCheckTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var task = Task.Run(async () => + { + // The AsyncContext flows with the execution context + Assert.Same(context, state.Get(token)); + + checkAsyncFlowTcs.SetResult(null!); + + await waitForNullTcs.Task; + + try + { + Assert.Null(state.Get(token)); + + afterNullCheckTcs.SetResult(null!); + } + catch (Exception ex) + { + afterNullCheckTcs.SetException(ex); + } + }); + + await checkAsyncFlowTcs.Task; + + // Null out the context + state.Set(token, null); + + waitForNullTcs.SetResult(null!); + + Assert.Null(state.Get(token)); + + await afterNullCheckTcs.Task; + await task; + } + + [Fact] + public async Task GettingAsyncContextReturnsNullAsyncContextIfChanged() + { + var state = new AsyncState(); + var context = new Thing(); + var token = state.RegisterAsyncContext(); + + state.Initialize(); + state.Set(token, context); + + var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var waitForNullTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var afterNullCheckTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var task = Task.Run(async () => + { + // The AsyncContext flows with the execution context + Assert.Same(context, state.Get(token)); + + checkAsyncFlowTcs.SetResult(null!); + + await waitForNullTcs.Task; + + try + { + Assert.Throws(() => state.Get(token)); + + afterNullCheckTcs.SetResult(null!); + } + catch (Exception ex) + { + afterNullCheckTcs.SetException(ex); + } + }); + + await checkAsyncFlowTcs.Task; + + // Set a new Async context + state.Initialize(); + var context2 = new Thing(); + state.Set(token, context2); + + waitForNullTcs.SetResult(null!); + + Assert.Same(context2, state.Get(token)); + + await afterNullCheckTcs.Task; + await task; + } + + [Fact] + public async Task GettingAsyncContextDoesNotFlowIfAccessorSetToNull() + { + var state = new AsyncState(); + var context = new Thing(); + var token = state.RegisterAsyncContext(); + state.Initialize(); + state.Set(token, context); + + var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + state.Set(token, null); + + var task = Task.Run(() => + { + try + { + // The AsyncContext flows with the execution context + Assert.Null(state.Get(token)); + + checkAsyncFlowTcs.SetResult(null!); + } + catch (Exception ex) + { + checkAsyncFlowTcs.SetException(ex); + } + }); + + await checkAsyncFlowTcs.Task; + await task; + } + + [Fact] + public async Task GettingAsyncContextDoesNotFlowIfExecutionContextDoesNotFlow() + { + var state = new AsyncState(); + var context = new Thing(); + var token = state.RegisterAsyncContext(); + state.Initialize(); + state.Set(token, context); + + var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + ThreadPool.UnsafeQueueUserWorkItem(_ => + { + try + { + // The AsyncContext flows with the execution context + Assert.Throws(() => state.Get(token)); + checkAsyncFlowTcs.SetResult(null!); + } + catch (Exception ex) + { + checkAsyncFlowTcs.SetException(ex); + } + }, null); + + await checkAsyncFlowTcs.Task; + } + + [Fact] + public void RegisterContextCorrectly() + { + var asyncState = new AsyncState(); + + var c1 = asyncState.RegisterAsyncContext(); + Assert.Equal(0, c1.Index); + var c2 = asyncState.RegisterAsyncContext(); + Assert.Equal(1, c2.Index); + var c3 = asyncState.RegisterAsyncContext(); + Assert.Equal(2, c3.Index); + + Assert.Equal(3, asyncState.ContextCount); + } + + [Fact] + public void EnsureCount_IncreasesCountCorrectly() + { + var l = new List(); + AsyncState.EnsureCount(l, 5); + Assert.Equal(5, l.Count); + } + + [Fact] + public void EnsureCount_WhenCountLessThanExpected() + { + var l = new List(new object?[5]); + AsyncState.EnsureCount(l, 2); + Assert.Equal(5, l.Count); + } + + [Fact] + public void EnsureCount_WhenCountEqualWithExpected() + { + var l = new List(new object?[5]); + AsyncState.EnsureCount(l, 5); + Assert.Equal(5, l.Count); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTokenTests.cs b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTokenTests.cs new file mode 100644 index 0000000000..06a1c30e5b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/AsyncStateTokenTests.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.AsyncState.Test; +public class AsyncStateTokenTests +{ + [Fact] + public void AsyncStateToekn_Equal() + { + var t1 = new AsyncStateToken(1); + var t2 = new AsyncStateToken(1); + + Assert.True(t1.Equals(t2)); + Assert.True((object)t1 != (object)t2); + Assert.True(t1.Equals((object)t2)); + Assert.False(t1.Equals(string.Empty)); + Assert.True(t1 == t2); + Assert.False(t1 != t2); + } + + [Fact] + public void AsyncStateToekn_NotEqual() + { + var t1 = new AsyncStateToken(1); + var t2 = new AsyncStateToken(2); + + Assert.False(t1.Equals(t2)); + Assert.True((object)t1 != (object)t2); + Assert.False(t1.Equals((object)t2)); + Assert.False(t1 == t2); + Assert.True(t1 != t2); + } + + [Fact] + public void AsyncStateToekn_HashCode() + { + int ind = 1; + var t1 = new AsyncStateToken(ind); + + Assert.Equal(ind.GetHashCode(), t1.GetHashCode()); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs new file mode 100644 index 0000000000..0f28db6c90 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/FeaturesPooledPolicyTests.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Extensions.AsyncState.Test; +public class FeaturesPooledPolicyTests +{ + [Fact] + public void Return_ShouldBeTrue() + { + var policy = new FeaturesPooledPolicy(); + + Assert.True(policy.Return(new List())); + } + + [Fact] + public void Return_ShouldNullList() + { + var policy = new FeaturesPooledPolicy(); + + var list = policy.Create(); + list.Add(string.Empty); + list.Add(Array.Empty()); + list.Add(new object()); + + Assert.True(policy.Return(list)); + Assert.All(list, el => Assert.Null(el)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/Microsoft.Extensions.AsyncState.Tests.csproj b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/Microsoft.Extensions.AsyncState.Tests.csproj new file mode 100644 index 0000000000..f329b8cf7f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/Microsoft.Extensions.AsyncState.Tests.csproj @@ -0,0 +1,14 @@ + + + Microsoft.Extensions.AsyncState.Test + Unit tests for Microsoft.Extensions.AsyncState. + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/Mock/AnotherThing.cs b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/Mock/AnotherThing.cs new file mode 100644 index 0000000000..c90d008d7e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/Mock/AnotherThing.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.AsyncState.Test; + +public class AnotherThing : IThing +{ + public string Hello() + { + return "Hello World, in a different way!"; + } +} diff --git a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/Mock/IThing.cs b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/Mock/IThing.cs new file mode 100644 index 0000000000..0d5a0b64de --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/Mock/IThing.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.AsyncState.Test; + +public interface IThing +{ + string Hello(); +} diff --git a/test/Libraries/Microsoft.Extensions.AsyncState.Tests/Mock/Thing.cs b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/Mock/Thing.cs new file mode 100644 index 0000000000..925757ae6b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AsyncState.Tests/Mock/Thing.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.AsyncState.Test; + +public class Thing : IThing +{ + public string Hello() + { + return "Hello World!"; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Classification/DataClassificationAttributeTest.cs b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Classification/DataClassificationAttributeTest.cs new file mode 100644 index 0000000000..b1cac0a7ce --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Classification/DataClassificationAttributeTest.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Compliance.Classification.Tests; + +public static class DataClassificationAttributeTest +{ + private const string TaxonomyName = "Tax"; + private const ulong Mask = 123; + + private sealed class TestAttribute : DataClassificationAttribute + { + public TestAttribute() + : base(new DataClassification(TaxonomyName, Mask)) + { + } + } + + [Fact] + public static void Basic() + { + var attribute = new TestAttribute(); + Assert.Equal(0, attribute.Notes.Length); + Assert.True(attribute.Classification == new DataClassification(TaxonomyName, Mask)); + + attribute.Notes = "Hello"; + Assert.Equal("Hello", attribute.Notes); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Classification/DataClassificationTest.cs b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Classification/DataClassificationTest.cs new file mode 100644 index 0000000000..c3c043ef5e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Classification/DataClassificationTest.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.Extensions.Compliance.Classification.Tests; + +public static class DataClassificationTest +{ + [Fact] + public static void Basic() + { + const string TaxonomyName = "MyTaxonomy"; + const ulong Mask = 123; + + var dc = new DataClassification(TaxonomyName, Mask); + Assert.Equal(TaxonomyName, dc.TaxonomyName); + Assert.Equal(Mask, dc.Value); + + Assert.True(dc == new DataClassification(TaxonomyName, Mask)); + Assert.False(dc != new DataClassification(TaxonomyName, Mask)); + + Assert.True(dc != new DataClassification(TaxonomyName + "x", Mask)); + Assert.False(dc == new DataClassification(TaxonomyName + "x", Mask)); + + Assert.True(dc != new DataClassification(TaxonomyName, Mask + 1)); + Assert.False(dc == new DataClassification(TaxonomyName, Mask + 1)); + + Assert.True(dc.Equals((object)dc)); + Assert.False(dc.Equals(new object())); + + Assert.Equal(dc.GetHashCode(), dc.GetHashCode()); + Assert.NotEqual(dc.GetHashCode(), new DataClassification(TaxonomyName + "X", Mask).GetHashCode()); + Assert.NotEqual(dc.GetHashCode(), new DataClassification(TaxonomyName, Mask + 1).GetHashCode()); + } + + [Fact] + public static void CantCreateUnknownClassifications() + { + Assert.Throws(() => new DataClassification("Foo", DataClassification.None.Value)); + Assert.Throws(() => new DataClassification("Foo", DataClassification.Unknown.Value)); + } + + [Fact] + public static void Combine() + { + const string TaxonomyName = "MyTaxonomy"; + const ulong Mask1 = 0x0123; + const ulong Mask2 = 0x8000; + + var dc1 = new DataClassification(TaxonomyName, Mask1); + var dc2 = new DataClassification(TaxonomyName, Mask2); + + Assert.Equal(Mask1 | Mask2, (dc1 | dc2).Value); + Assert.Equal(Mask1 | Mask2, (dc2 | dc1).Value); + Assert.Throws(() => dc1 | new DataClassification(TaxonomyName + "X", Mask2)); + + Assert.Equal(DataClassification.UnknownTaxonomyValue, (dc1 | DataClassification.Unknown).Value); + Assert.Equal(string.Empty, (dc1 | DataClassification.Unknown).TaxonomyName); + + Assert.Equal(dc1.Value, (dc1 | DataClassification.None).Value); + Assert.Equal(dc1.TaxonomyName, (dc1 | DataClassification.None).TaxonomyName); + + Assert.Equal(DataClassification.UnknownTaxonomyValue, (DataClassification.Unknown | dc1).Value); + Assert.Equal(string.Empty, (dc1 | DataClassification.Unknown).TaxonomyName); + + Assert.Equal(dc1.Value, (DataClassification.None | dc1).Value); + Assert.Equal(dc1.TaxonomyName, (DataClassification.None | dc1).TaxonomyName); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Classification/NoDataClassificationAttributeTest.cs b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Classification/NoDataClassificationAttributeTest.cs new file mode 100644 index 0000000000..4c316c2679 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Classification/NoDataClassificationAttributeTest.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Compliance.Classification.Tests; + +public static class NoDataClassificationAttributeTest +{ + [Fact] + public static void Basic() + { + var attribute = new NoDataClassificationAttribute(); + Assert.Equal(DataClassification.None, attribute.Classification); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Classification/UnknownDataClassificationAttributeTest.cs b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Classification/UnknownDataClassificationAttributeTest.cs new file mode 100644 index 0000000000..2363629c9b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Classification/UnknownDataClassificationAttributeTest.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Compliance.Classification.Tests; + +public static class UnknownDataClassificationAttributeTest +{ + [Fact] + public static void Basic() + { + var attribute = new UnknownDataClassificationAttribute(); + Assert.Equal(DataClassification.Unknown, attribute.Classification); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Microsoft.Extensions.Compliance.Abstractions.Tests.csproj b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Microsoft.Extensions.Compliance.Abstractions.Tests.csproj new file mode 100644 index 0000000000..cf3a07e6ac --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Microsoft.Extensions.Compliance.Abstractions.Tests.csproj @@ -0,0 +1,10 @@ + + + Microsoft.Extensions.Compliance + Unit tests for Microsoft.Extensions.Compliance.Abstactions. + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/FakeFormattable.cs b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/FakeFormattable.cs new file mode 100644 index 0000000000..befcfb73b1 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/FakeFormattable.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Compliance.Redaction.Tests; + +internal sealed class FakeFormattable : IFormattable +{ + private readonly string _value; + + public FakeFormattable(string value) + { + _value = value; + } + + public string ToString(string? format, IFormatProvider? formatProvider) => _value; +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/FakeObject.cs b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/FakeObject.cs new file mode 100644 index 0000000000..db1de5c8c4 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/FakeObject.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Compliance.Redaction.Tests; + +internal readonly struct FakeObject +{ + private readonly string _value; + + public FakeObject(string value) + { + _value = value; + } + + public override string ToString() => _value; +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/FakeSpanFormattable.cs b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/FakeSpanFormattable.cs new file mode 100644 index 0000000000..331089f742 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/FakeSpanFormattable.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET6_0_OR_GREATER + +using System; + +namespace Microsoft.Extensions.Compliance.Redaction.Tests; + +internal sealed class FakeSpanFormattable : ISpanFormattable +{ + private readonly string _value; + + public FakeSpanFormattable(string value) + { + _value = value; + } + + public string ToString(string? format, System.IFormatProvider? formatProvider) + { + return _value; + } + + public bool TryFormat(System.Span destination, out int charsWritten, + System.ReadOnlySpan format, System.IFormatProvider? provider) + { + if (destination.Length < _value.Length) + { + charsWritten = 0; + return false; + } + + for (var i = 0; i < _value.Length; i++) + { + destination[i] = _value[i]; + } + + charsWritten = _value.Length; + + return true; + } +} + +#endif diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/NullRedactor.cs b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/NullRedactor.cs new file mode 100644 index 0000000000..1896c0afcb --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/NullRedactor.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.Compliance.Redaction; + +/// +/// No-op redactor implementation used for data classes that don't require any redaction. +/// +internal sealed class NullRedactor : Redactor +{ + private NullRedactor() + { + } + + /// + /// Gets the singleton instance of this class. + /// + public static NullRedactor Instance { get; } = new(); + + /// + public override int GetRedactedLength(ReadOnlySpan input) => input.Length; + + /// + public override int Redact(ReadOnlySpan source, Span destination) + { + Throw.IfBufferTooSmall(destination.Length, source.Length); + + // span.CopyTo method throws on 0 input, it is not an error in this case so we can just return. + if (source.IsEmpty) + { + return 0; + } + + source.CopyTo(destination); + + return source.Length; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/RedactorAbstractionsExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/RedactorAbstractionsExtensionsTest.cs new file mode 100644 index 0000000000..7b06e07384 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Abstractions.Tests/Redaction/RedactorAbstractionsExtensionsTest.cs @@ -0,0 +1,239 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Xunit; + +namespace Microsoft.Extensions.Compliance.Redaction.Tests; + +public class RedactorAbstractionsExtensionsTest +{ + [Fact] + public void Redaction_Extensions_Throws_ArgumentNullException_When_Redactor_Is_Null() + { + string s = null!; + + Assert.Throws(() => RedactionAbstractionsExtensions.AppendRedacted(null!, NullRedactor.Instance, s)); + Assert.Throws(() => RedactionAbstractionsExtensions.AppendRedacted(new StringBuilder(), null!, "")); + } + + [Fact] + [SuppressMessage("Style", "IDE0004:Remove Unnecessary Cast", Justification = "Cast is required to call extension method.")] + public void Redaction_Extensions_Return_Zero_On_Null_Input_Value() + { + Assert.Equal(string.Empty, NullRedactor.Instance.Redact((string?)null)); + Assert.Equal(0, NullRedactor.Instance.GetRedactedLength((string?)null)); + Assert.Equal(0, NullRedactor.Instance.Redact((string)null!, new char[0])); + } + + [Fact] + public void When_Passed_Null_Value_String_Builder_Extensions_Does_Not_Append_To_String_Builder() + { + var sb = new StringBuilder(); + var redactor = NullRedactor.Instance; + + sb.AppendRedacted(NullRedactor.Instance, +#if NETCOREAPP3_1_OR_GREATER + null); +#else + (string?)null); +#endif + + Assert.Equal(0, sb.Length); + } + + [Theory] + [InlineData(10)] + [InlineData(100)] + [InlineData(1000)] + [InlineData(10000)] + public void User_Can_Get_String_From_IRedactor_Using_Extension_Method_With_Different_Input_Length(int length) + { + var data = new string('*', length); + + Redactor r = NullRedactor.Instance; + + var redacted = r.Redact(data); + + Assert.Equal(data, redacted); + } + + [Fact] + public void Return_Quickly_When_User_Tries_To_Append_Empty_Span_Using_StringBuilder_Extensions() + { + var sb = new StringBuilder(); + + sb.AppendRedacted(NullRedactor.Instance, string.Empty); + + Assert.Empty(sb.ToString()); + } + + [Fact] + public void Get_Redacted_String_API_Returns_Equivalent_Output_As_Span_Overload() + { + var data = new string('3', 3); + var r = NullRedactor.Instance; + + var lengthFromExtension = r.GetRedactedLength(data); + var length = r.GetRedactedLength(data); + + Assert.Equal(lengthFromExtension, length); + } + + [Fact] + public void Redact_Extension_String_Span_Works_The_Same_Way_As_Native_Method() + { + var data = new string('3', 3); + + Span extBuffer = stackalloc char[3]; + Span buffer = stackalloc char[3]; + + var r = NullRedactor.Instance; + var extensionWritten = r.Redact(data, extBuffer); + var written = r.Redact(data, buffer); + + Assert.Equal(extensionWritten, written); + Assert.Equal(extBuffer.ToString(), buffer.ToString()); + } + + [Theory] + [InlineData(10)] + [InlineData(100)] + [InlineData(1000)] + [InlineData(10000)] + public void User_Can_Use_String_Builder_Extensions_To_Append_Redacted_Strings(int length) + { + var data = new string('*', length); + var data2 = new string('c', length); + + var sb = new StringBuilder(); + var r = NullRedactor.Instance; + + sb.AppendRedacted(r, data) + .AppendRedacted(r, data2); + + var redactedData = sb.ToString(); + + Assert.Contains(data, redactedData); + Assert.Contains(data2, redactedData); + } + +#if NET6_0_OR_GREATER + [Theory] + [InlineData(100)] + [InlineData(1000)] + public void SpanFormattable_Format_And_Redacts_Data(int inputSize) + { + var data = new string('&', inputSize); + + var spanFormattable = new FakeSpanFormattable(data); + + var r = NullRedactor.Instance; + + var redacted = r.Redact(spanFormattable, null, null); + var redactedDirectly = r.Redact(data); + + Assert.Equal(redactedDirectly, redacted); + } + + [Theory] + [InlineData(100)] + [InlineData(1000)] + public void SpanFormattable_Format_And_Redacts_Data_With_Destination_Buffer(int inputSize) + { + var data = new string('^', inputSize); + + var spanFormattable = new FakeSpanFormattable(data); + + var r = NullRedactor.Instance; + + var buffer = new char[data.Length]; + var bufferDirect = new char[data.Length]; + + var redacted = r.Redact(spanFormattable, buffer, null, null); + var redactedDirectly = r.Redact(data, bufferDirect); + + for (var i = 0; i < buffer.Length; i++) + { + Assert.Equal(buffer[i], bufferDirect[i]); + } + } +#endif + + [Fact] + public void Formattable_Format_And_Redacts_Data() + { + var data = Guid.NewGuid().ToString(); + + var formattable = new FakeFormattable(data); + + Redactor r = NullRedactor.Instance; + + var redacted = r.Redact(formattable, null, null); + var redactedDirectly = r.Redact(data); + + Assert.Equal(redactedDirectly, redacted); + } + + [Fact] + public void Formattable_Format_And_Redacts_Data_With_Destination_Buffer() + { + var data = Guid.NewGuid().ToString(); + + var spanFormattable = new FakeFormattable(data); + + var r = NullRedactor.Instance; + + var buffer = new char[data.Length]; + var bufferDirect = new char[data.Length]; + + var redacted = r.Redact(spanFormattable, buffer, null, null); + var redactedDirectly = r.Redact(data, bufferDirect); + + for (var i = 0; i < buffer.Length; i++) + { + Assert.Equal(buffer[i], bufferDirect[i]); + } + } + + [Fact] + public void Object_Format_And_Redacts_Data() + { + var data = Guid.NewGuid().ToString(); + + var obj = new FakeObject(data); + + var r = NullRedactor.Instance; + + var buffer = new char[data.Length]; + var bufferDirect = new char[data.Length]; + + var redacted = r.Redact(obj); + var redactedDirectly = r.Redact(data); + + Assert.Equal(redactedDirectly, redacted); + } + + [Fact] + public void Object_Format_And_Redacts_Data_With_Destination_Buffer() + { + var data = Guid.NewGuid().ToString(); + + var obj = new FakeObject(data); + + var r = NullRedactor.Instance; + + var buffer = new char[data.Length]; + var bufferDirect = new char[data.Length]; + + var redacted = r.Redact(obj, buffer); + var redactedDirectly = r.Redact(data, bufferDirect); + + for (var i = 0; i < buffer.Length; i++) + { + Assert.Equal(buffer[i], bufferDirect[i]); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/ErasingRedactorTest.cs b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/ErasingRedactorTest.cs new file mode 100644 index 0000000000..678691e3be --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/ErasingRedactorTest.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.Extensions.Compliance.Redaction.Tests; + +public class ErasingRedactorTest +{ + [Fact] + public void Null_Redactor_Always_Returns_Same_Stuff() + { + Redactor redactor = ErasingRedactor.Instance; + + var oneLength = redactor.GetRedactedLength(new char[1]); + var twoLength = redactor.GetRedactedLength(new char[10]); + var threeLength = redactor.GetRedactedLength(new char[100]); + + Assert.Equal(oneLength, twoLength); + Assert.Equal(oneLength, threeLength); + } + + [Fact] + public void Null_Redactor_Always_Returns_Same_Stuff_When_Used_As_IRedactor_Of_String() + { + Redactor redactor = ErasingRedactor.Instance; + + var oneLength = redactor.GetRedactedLength(new string('a', 1)); + var twoLength = redactor.GetRedactedLength(new string('a', 12)); + var threeLength = redactor.GetRedactedLength(new string('a', 82)); + + Assert.Equal(oneLength, twoLength); + Assert.Equal(oneLength, threeLength); + } + + [Fact] + public void Null_Redactor_Doesnt_Mutate_Passed_Buffer() + { + var input = new string('G', 20); + Redactor redactor = ErasingRedactor.Instance; + Span buffer = stackalloc char[20]; + + redactor.Redact(input, buffer); + + var bufferString = buffer.ToString(); + + Assert.NotEqual(input, bufferString); + Assert.DoesNotContain(input, bufferString); + } + + [Fact] + public void Null_Redactor_Returns_Empty_String_On_Redact_Span_String_Overload() + { + var e = ErasingRedactor.Instance.Redact("any"); + + Assert.NotNull(e); + Assert.Empty(e); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/FakePlaintextRedactor.cs b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/FakePlaintextRedactor.cs new file mode 100644 index 0000000000..96959b5c61 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/FakePlaintextRedactor.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Compliance.Redaction.Tests; + +public class FakePlaintextRedactor : Redactor +{ + public override int GetRedactedLength(ReadOnlySpan input) => input.Length; + + public override int Redact(ReadOnlySpan source, Span destination) + { + source.CopyTo(destination); + return source.Length; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/FakeStartup.cs b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/FakeStartup.cs new file mode 100644 index 0000000000..4790cb9c61 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/FakeStartup.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Compliance.Redaction.Tests; + +[SuppressMessage("Performance", "CA1812: Avoid uninstantiated internal classes", Justification = "Instantiated via reflection.")] +internal class FakeStartup +{ + [SuppressMessage("Performance", "CA1822:Mark members as static", + Justification = "Because then I get another warning that when type contains just static stuff it can be all made static. When I will make type static I cannot use it as generic parameter.")] + public void Configure() + { + // WebHostBuilder requirement + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/Microsoft.Extensions.Compliance.Redaction.Tests.csproj b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/Microsoft.Extensions.Compliance.Redaction.Tests.csproj new file mode 100644 index 0000000000..d30cbe8385 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/Microsoft.Extensions.Compliance.Redaction.Tests.csproj @@ -0,0 +1,16 @@ + + + Microsoft.Extensions.Compliance.Redaction.Tests + Unit tests for Microsoft.Extensions.Compliance.Redaction. + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/NullRedactorProvider.cs b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/NullRedactorProvider.cs new file mode 100644 index 0000000000..6ec881534d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/NullRedactorProvider.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Compliance.Classification; + +namespace Microsoft.Extensions.Compliance.Redaction.Tests; + +internal sealed class NullRedactorProvider : IRedactorProvider +{ + private NullRedactorProvider() + { + } + + public static NullRedactorProvider Instance { get; } = new(); + public Redactor GetRedactor(DataClassification classification) => NullRedactor.Instance; +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/NullRedactorTest.cs b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/NullRedactorTest.cs new file mode 100644 index 0000000000..dd320c789b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/NullRedactorTest.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Compliance.Classification; +using Xunit; + +namespace Microsoft.Extensions.Compliance.Redaction.Tests; + +public class NullRedactorTest +{ + [Fact] + public void NullRedactor_When_Given_Empty_String_Returns_Empty_String() + { + var r = NullRedactor.Instance; + + var emptyStringRedacted = r.Redact(string.Empty); + + Assert.Equal(string.Empty, emptyStringRedacted); + } + + [Fact] + public void NullRedactor_When_Given_Empty_Buffer_Returns_0_Chars_Written() + { + var r = NullRedactor.Instance; + + Span input = stackalloc char[0]; + + var c = new char[1]; + + var charsWritten = r.Redact(input, c); + + Assert.Equal(0, charsWritten); + Assert.Equal('\0', c[0]); + } + + [Fact] + public void NullRedactorProvider_Returns_Always_NullRedactor() + { + var dc1 = new DataClassification("TAX", 1); + var dc2 = new DataClassification("TAX", 2); + var dc3 = new DataClassification("TAX", 4); + + var rp = NullRedactorProvider.Instance; + var redactor1 = NullRedactor.Instance; + var redactor2 = rp.GetRedactor(dc1); + var redactor3 = rp.GetRedactor(dc2); + var redactor4 = rp.GetRedactor(dc3); + + Assert.Equal(redactor1, redactor2); + Assert.Equal(redactor1, redactor3); + Assert.Equal(redactor1, redactor4); + Assert.IsAssignableFrom(redactor1); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/RedactionAcceptanceTest.cs b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/RedactionAcceptanceTest.cs new file mode 100644 index 0000000000..363cb171a8 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/RedactionAcceptanceTest.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Xunit; + +namespace Microsoft.Extensions.Compliance.Redaction.Tests; + +public class RedactionAcceptanceTest +{ + [Fact] + public void RedactorProvider_Allows_To_Register_And_Use_Redactors_Using_DataClassification() + { + var dc1 = new DataClassification("TAX", 1); + var dc2 = new DataClassification("TAX", 2); + var data = "Mississippi"; + + using var services = new ServiceCollection() + .AddLogging() + .AddRedaction(redaction => redaction + .SetRedactor(dc1) + .SetRedactor(DataClassification.Unknown) + .SetFallbackRedactor()) + .BuildServiceProvider(); + + var redactorProvider = services.GetRequiredService(); + + var redactor = redactorProvider.GetRedactor(dc1); + var redacted = redactor.Redact(data); + Assert.Equal(redacted, data); + + redactor = redactorProvider.GetRedactor(dc2); + redacted = redactor.Redact(data); + Assert.Equal(redacted, data); + } + + [Fact] + public void Redaction_Can_Be_Registered_While_Creating_Host() + { + var dc = new DataClassification("TAX", 1); + using var host = FakeHost.CreateBuilder(options => options.FakeRedaction = false) + .ConfigureRedaction(x => x.SetRedactor(dc)) + .Build(); + + var redactorProvider = host.Services.GetService(); + + Assert.IsAssignableFrom(redactorProvider); + } + + [Fact] + public void Redaction_Can_Be_Registered_While_Creating_Host_Using_Context_Overload() + { + var dc = new DataClassification("TAX", 1); + using var host = FakeHost.CreateBuilder(options => options.FakeRedaction = false) + .ConfigureRedaction((_, x) => x.SetRedactor(dc)) + .Build(); + + var redactorProvider = host.Services.GetService(); + + Assert.IsAssignableFrom(redactorProvider); + } + + [Fact] + public void Redaction_Can_Be_Registered_In_Service_Collection() + { + var dc = new DataClassification("TAX", 1); + using var services = new ServiceCollection() + .AddLogging() + .AddRedaction(x => x.SetRedactor(dc)) + .BuildServiceProvider(); + + var redactorProvider = services.GetService(); + + Assert.IsAssignableFrom(redactorProvider); + } + + [Fact] + public void Redaction_Extensions_Throws_When_Gets_Null_Args() + { + Assert.Throws(() => ((IHostBuilder)null!).ConfigureRedaction()); + Assert.Throws(() => ((IHostBuilder)null!).ConfigureRedaction(_ => { })); + Assert.Throws(() => ((IHostBuilder)null!).ConfigureRedaction((_, _) => { })); + Assert.Throws( + () => FakeHost.CreateBuilder(options => options.FakeRedaction = false).ConfigureRedaction((Action)null!)); + Assert.Throws( + () => FakeHost.CreateBuilder(options => options.FakeRedaction = false).ConfigureRedaction((Action)null!)); + Assert.Throws(() => ((IServiceCollection)null!).AddRedaction(_ => { })); + Assert.Throws(() => new ServiceCollection().AddRedaction(null!)); + } + + [Fact] + public void Can_Register_Redactor_Provider_With_Defaults_Without_Specifying_Arguments() + { + using var serviceProvider = new ServiceCollection() + .AddLogging() + .AddRedaction() + .BuildServiceProvider(); + + var dc = new DataClassification("TAX", 1); + var redactorProvider = serviceProvider.GetRequiredService(); + var redactor = redactorProvider.GetRedactor(dc); + + Assert.IsAssignableFrom(redactor); + Assert.IsAssignableFrom(redactorProvider); + } + + [Fact] + public void Developer_Can_Use_Null_Redactor_Provider_From_IHost() + { + using var host = FakeHost.CreateBuilder(options => options.FakeRedaction = false) + .ConfigureRedaction() + .Build(); + + var redactor = host.Services.GetRequiredService(); + + Assert.IsAssignableFrom(redactor); + } + + [Fact] + public void Extensions_Throws_Exception_When_Used_With_Null_Arguments() + { + Assert.Throws(() => ((IHostBuilder)null!).ConfigureRedaction()); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/RedactorProviderTest.cs b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/RedactorProviderTest.cs new file mode 100644 index 0000000000..15932b6903 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/RedactorProviderTest.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Compliance.Classification; +using Xunit; + +namespace Microsoft.Extensions.Compliance.Redaction.Tests; + +public class RedactorProviderTest +{ + [Fact] + public void RedactorProvider_Returns_Redactor_For_Every_Data_Classification() + { + var dc = new DataClassification("Foo", 0x2); + + var redactorProvider = new RedactorProvider( + redactors: new Redactor[] { ErasingRedactor.Instance, NullRedactor.Instance }, + options: Microsoft.Extensions.Options.Options.Create(new RedactorProviderOptions())); + + var r = redactorProvider.GetRedactor(dc); + Assert.IsAssignableFrom(r); + } + + private static readonly DataClassification _dataClassification1 = new("TAX", 1); + private static readonly DataClassification _dataClassification2 = new("TAX", 2); + private static readonly DataClassification _dataClassification3 = new("TAX", 4); + + [Fact] + public void RedactorProvider_Returns_Redactor_For_Data_Classifications() + { + var opt = new RedactorProviderOptions(); + opt.Redactors.Add(_dataClassification1, typeof(ErasingRedactor)); + opt.Redactors.Add(_dataClassification2, typeof(NullRedactor)); + + var redactorProvider = new RedactorProvider( + redactors: new Redactor[] { ErasingRedactor.Instance, NullRedactor.Instance }, + options: Microsoft.Extensions.Options.Options.Create(opt)); + + var r1 = redactorProvider.GetRedactor(_dataClassification1); + var r2 = redactorProvider.GetRedactor(_dataClassification2); + var r3 = redactorProvider.GetRedactor(_dataClassification3); + + Assert.Equal(typeof(ErasingRedactor), r1.GetType()); + Assert.Equal(typeof(NullRedactor), r2.GetType()); + Assert.Equal(typeof(ErasingRedactor), r3.GetType()); + } + + [Fact] + public void RedactorProvider_Throws_On_Ctor_When_Options_Come_As_Null() + { + Assert.Throws(() => new RedactorProvider( + redactors: new Redactor[] { ErasingRedactor.Instance, new FakePlaintextRedactor() }, + options: Microsoft.Extensions.Options.Options.Create(null!))); + } + + [Fact] + public void RedactorProvider_Throws_When_Fallback_Redactor_Not_In_DI() + { + Assert.Throws(() => new RedactorProvider( + Array.Empty(), + Microsoft.Extensions.Options.Options.Create(new RedactorProviderOptions()))); + } + + [Fact] + public void RedactorProvider_Throws_When_ModernFallback_Redactor_Not_In_DI() + { + var opt = new RedactorProviderOptions + { + FallbackRedactor = typeof(FakePlaintextRedactor), + }; + + Assert.Throws(() => new RedactorProvider( + new Redactor[] + { + ErasingRedactor.Instance, + NullRedactor.Instance, + }, + Microsoft.Extensions.Options.Options.Create(opt))); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/XXHash3RedactorExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/XXHash3RedactorExtensionsTests.cs new file mode 100644 index 0000000000..5cfc706a31 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/XXHash3RedactorExtensionsTests.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Xunit; + +namespace Microsoft.Extensions.Compliance.Redaction.Tests; + +public class XXHash3RedactorExtensionsTests +{ + [Fact] + public void DelegateBased() + { + using var host = FakeHost.CreateBuilder(options => options.FakeRedaction = false) + .ConfigureRedaction((_, redaction) => redaction + .SetXXHash3Redactor(o => o.HashSeed = 101, SimpleClassifications.PrivateData)) + .Build(); + + var redactorProvider = host.Services.GetRequiredService(); + + CheckProvider(redactorProvider); + } + + [Fact] + public void HostBuilder_GivenXXHashRedactorWithConfigurationSectionConfig_RegistersItAsHashingRedactorAndRedacts() + { + using var host = FakeHost.CreateBuilder(options => options.FakeRedaction = false) + .ConfigureRedaction((_, redaction) => + { + var section = GetRedactorConfiguration(new ConfigurationBuilder(), 101); + redaction.SetXXHash3Redactor(section, SimpleClassifications.PrivateData); + }) + .Build(); + + var redactorProvider = host.Services.GetRequiredService(); + CheckProvider(redactorProvider); + } + + private static void CheckProvider(IRedactorProvider redactorProvider) + { + const string Example = "Redact Me"; + + var classifications = new[] + { + SimpleClassifications.PublicData, + SimpleClassifications.PrivateData + }; + + foreach (var dc in classifications) + { + var redactor = redactorProvider.GetRedactor(dc); + + var expectedLength = redactor.GetRedactedLength(Example); + var destination = new char[expectedLength]; + var actualLength = redactor.Redact(Example, destination); + + if (dc == SimpleClassifications.PrivateData) + { + Assert.Equal(XXHash3Redactor.RedactedSize, expectedLength); + Assert.Equal(XXHash3Redactor.RedactedSize, actualLength); + } + else + { + Assert.True(expectedLength == 0 || expectedLength == Example.Length); + Assert.True(actualLength == 0 || actualLength == Example.Length); + } + } + } + + private static IConfigurationSection GetRedactorConfiguration(IConfigurationBuilder builder, ulong hashSeed) + { + XXHash3RedactorOptions options; + + return builder + .AddInMemoryCollection(new Dictionary + { + { $"{nameof(XXHash3RedactorOptions)}:{nameof(options.HashSeed)}", hashSeed.ToString(CultureInfo.InvariantCulture) }, + }) + .Build() + .GetSection(nameof(XXHash3RedactorOptions)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/XXHash3RedactorTests.cs b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/XXHash3RedactorTests.cs new file mode 100644 index 0000000000..44d27256ec --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Redaction.Tests/XXHash3RedactorTests.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.Extensions.Compliance.Redaction.Tests; + +public class XXHash3RedactorTests +{ + [Fact] + public void Basic() + { + var redactor = new XXHash3Redactor(Microsoft.Extensions.Options.Options.Create(new XXHash3RedactorOptions + { + HashSeed = 101, + })); + + Assert.Equal(XXHash3Redactor.RedactedSize, redactor.GetRedactedLength(" ")); + Assert.Equal(XXHash3Redactor.RedactedSize, redactor.GetRedactedLength("--")); + Assert.Equal(XXHash3Redactor.RedactedSize, redactor.GetRedactedLength("XXXXXXXXXXXXXXXXXXXXXXX")); + + var s1 = new char[XXHash3Redactor.RedactedSize]; + var r1 = redactor.Redact("Hello", s1); + + var s2 = new char[XXHash3Redactor.RedactedSize]; + var r2 = redactor.Redact("Hello", s2); + + Assert.Equal(r1, r2); + Assert.Equal(s1, s2); + + redactor = new XXHash3Redactor(Microsoft.Extensions.Options.Options.Create(new XXHash3RedactorOptions + { + HashSeed = 10101, + })); + + var s3 = new char[XXHash3Redactor.RedactedSize]; + var r3 = redactor.Redact("Hello", s3); + + Assert.Equal(r1, r3); + Assert.NotEqual(s1, s3); + } + + [Fact] + public void NullChecks() + { + Assert.Throws(() => new XXHash3Redactor(Microsoft.Extensions.Options.Options.Create(null!))); + } + + [Fact] + public void XXHashRedactor_Does_Nothing_With_Empty_Input() + { + var redactor = new XXHash3Redactor(Microsoft.Extensions.Options.Options.Create(new XXHash3RedactorOptions + { + HashSeed = 101, + })); + + var buffer = new char[5]; + + var written = redactor.Redact(string.Empty, buffer); + + Assert.Equal(0, written); + + foreach (var item in buffer) + { + Assert.Equal('\0', item); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/AttributeTest.cs b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/AttributeTest.cs new file mode 100644 index 0000000000..53246bf02c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/AttributeTest.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Compliance.Classification; +using Xunit; + +namespace Microsoft.Extensions.Compliance.Testing.Tests; + +public static class AttributeTest +{ + [Fact] + public static void Basic() + { + DataClassificationAttribute a; + + a = new PrivateDataAttribute(); + Assert.Equal(SimpleClassifications.PrivateData, a.Classification); + + a = new PublicDataAttribute(); + Assert.Equal(SimpleClassifications.PublicData, a.Classification); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/FakeRedactorOptionsValidatorTest.cs b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/FakeRedactorOptionsValidatorTest.cs new file mode 100644 index 0000000000..91d472a0c0 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/FakeRedactorOptionsValidatorTest.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Compliance.Testing.Test; + +public class FakeRedactorOptionsValidatorTest +{ + [Fact] + public void Validator_Fails_When_Template_Is_Forcefully_Set_To_Null() + { + var validator = new FakeRedactorOptionsAutoValidator(); + var options = new FakeRedactorOptions + { + RedactionFormat = null! + }; + + var validationResult = validator.Validate(string.Empty, options); + + Assert.True(validationResult.Failed, "Validator passed when it should fail."); + Assert.False(validationResult.Succeeded, "Validator passed when it should fail."); + Assert.Contains(nameof(FakeRedactorOptions.RedactionFormat), validationResult.FailureMessage); + } + + [Theory] + [InlineData("__________{{{}}2}________")] + [InlineData("{0}{1}")] + [InlineData("{{01}{}{}}}{")] + [InlineData("_}}2{{{3}}}}")] + [InlineData("{0}{1}{2}{3}{4}")] + public void FakeRedactorValidator_Fails_Given_Invalid_Template(string format) + { + var validator = new FakeRedactorOptionsCustomValidator(); + var options = new FakeRedactorOptions + { + RedactionFormat = format + }; + + var validationResult = validator.Validate(string.Empty, options); + + Assert.True(validationResult.Failed, validationResult.FailureMessage); + Assert.Contains(nameof(options.RedactionFormat), validationResult.FailureMessage); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/FakeRedactorProviderTest.cs b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/FakeRedactorProviderTest.cs new file mode 100644 index 0000000000..f336eb0924 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/FakeRedactorProviderTest.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Compliance.Classification; +using Xunit; + +namespace Microsoft.Extensions.Compliance.Testing.Tests; + +public class FakeRedactorProviderTest +{ + [Fact] + public void Basic() + { + var provider = new FakeRedactorProvider(); + + var dc = new DataClassification("TAX", 1); + var redactor = provider.GetRedactor(dc); + Assert.Equal("Hello", redactor.Redact("Hello")); + } + + [Fact] + public void Can_Access_Event_Collector_From_Within_Redactor_Provider() + { + var rp = new FakeRedactorProvider(); + var dc = new DataClassification("TAX", 1); + rp.GetRedactor(dc); + Assert.Equal(dc, rp.Collector.LastRedactorRequested.DataClassification); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/FakeRedactorTest.cs b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/FakeRedactorTest.cs new file mode 100644 index 0000000000..9da96e8a3f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/FakeRedactorTest.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Compliance.Classification; +using Xunit; + +namespace Microsoft.Extensions.Compliance.Testing.Test; + +public class FakeRedactorTest +{ + [Fact] + public void Fake_Redactors_Shares_Behavior_Between_Instances_When_Different_Ctors_Used() + { + var r1 = new FakeRedactor(); + var r2 = FakeRedactor.Create(); + var r3 = FakeRedactor.Create(new FakeRedactorOptions()); + + var data = "Ananas"; + + var redacted1 = r1.Redact(data); + var redacted2 = r2.Redact(data); + var redacted3 = r3.Redact(data); + + Assert.Equal(redacted1, redacted2); + Assert.Equal(redacted1, redacted3); + } + + [Fact] + public void Fake_Redactor_Throws_When_Provided_Redaction_Format_Is_Not_Correct() + { + Assert.Throws(() => FakeRedactor.Create(new FakeRedactorOptions { RedactionFormat = "{{{{{23123{}}" })); + } + + [Fact] + public void Can_Use_Fake_Redactor_With_Constant_Value() + { + const string RedactedConstOutput = "TEST"; + + var redactor = FakeRedactor.Create(new FakeRedactorOptions { RedactionFormat = RedactedConstOutput }); + + var redacted = redactor.Redact(new string('*', 100)); + + Assert.Equal(RedactedConstOutput, redacted); + } + + [Fact] + public void When_Using_Factory_Method_To_Create_Without_Parameter_Fallback_Is_Used() + { + var data = "Bill Windmill"; + var redactor = FakeRedactor.Create(); + var redacted = redactor.Redact(data); + + Assert.Equal(redacted, data); + } + + /// + /// We are using singleton fake redactor using parallel for each which simulates parallel workload. + /// If the classes are thread safe we should never get the same sequence number assigned to two events. + /// We should also always have the same number of results - it will mean that we never overwrote any data. + /// + [Fact] + public void FakeRedaction_EventTracking_Is_Thread_Safe() + { + var fakeRedactorProvider = new FakeRedactorProvider(); + + var iterations = new int[30]; + for (var i = 0; i < iterations.Length; i++) + { + iterations[i] = i; + } + + var dc = new[] + { + new DataClassification("TAX", 1), + new DataClassification("TAX", 2), + new DataClassification("TAX", 3), + }; + + Parallel.ForEach(iterations, iteration => + { + var r = fakeRedactorProvider.GetRedactor(dc[iteration % dc.Length]); + r.Redact(iteration.ToString(CultureInfo.InvariantCulture)); + }); + + Assert.Equal(fakeRedactorProvider.Collector.AllRedactorRequests.Count, iterations.Length); + + var sequenceNumbersRedacted = fakeRedactorProvider.Collector.AllRedactedData.Select(x => x.SequenceNumber); + var sequenceNumbersRequested = fakeRedactorProvider.Collector.AllRedactorRequests.Select(x => x.SequenceNumber); + + var numbersSetRedacted = new HashSet(sequenceNumbersRedacted); + var numbersSetRequested = new HashSet(sequenceNumbersRequested); + + Assert.Equal(numbersSetRedacted.Count, iterations.Length); + Assert.Equal(numbersSetRequested.Count, iterations.Length); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/InstancesTest.cs b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/InstancesTest.cs new file mode 100644 index 0000000000..f1c54509b5 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/InstancesTest.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Compliance.Testing.Tests; + +public static class InstancesTest +{ + [Fact] + public static void Basic() + { + Assert.Equal(SimpleTaxonomy.PrivateData, (SimpleTaxonomy)SimpleClassifications.PrivateData.Value); + Assert.Equal(SimpleTaxonomy.PublicData, (SimpleTaxonomy)SimpleClassifications.PublicData.Value); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/Microsoft.Extensions.Compliance.Testing.Tests.csproj b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/Microsoft.Extensions.Compliance.Testing.Tests.csproj new file mode 100644 index 0000000000..5437f416cf --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/Microsoft.Extensions.Compliance.Testing.Tests.csproj @@ -0,0 +1,17 @@ + + + Microsoft.Extensions.Compliance.Testing.Tests + Unit tests for Microsoft.Extensions.Compliance.Testing. + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/RedactionFakesAcceptanceTest.cs b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/RedactionFakesAcceptanceTest.cs new file mode 100644 index 0000000000..b9cbea50f4 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/RedactionFakesAcceptanceTest.cs @@ -0,0 +1,257 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Compliance.Testing.Test; + +public class RedactionFakesAcceptanceTest +{ + [Fact] + public void Can_Register_And_Use_Fake_Redactor_With_Default_Options_With_DataClassification() + { + var dc = new DataClassification("Foo", 0x1); + + var data = "Lalaalal"; + using var services = new ServiceCollection() + .AddLogging() + .AddRedaction(x => x.SetFakeRedactor(dc)) + .BuildServiceProvider(); + + var provider = services.GetRequiredService(); + + var r = provider.GetRedactor(dc); + var redacted = r.Redact(data); + + Assert.IsAssignableFrom(r); + Assert.Contains(data, redacted); + } + + [Fact] + public void Can_Register_And_Use_Fake_Redactor_With_Default_Options() + { + var dc = new DataClassification("TAX", 1); + var data = "Lalaalal"; + using var services = new ServiceCollection() + .AddLogging() + .AddRedaction(x => x.SetFakeRedactor(dc)) + .BuildServiceProvider(); + + var provider = services.GetRequiredService(); + + var r = provider.GetRedactor(dc); + var redacted = r.Redact(data); + + Assert.IsAssignableFrom(r); + Assert.Contains(data, redacted); + } + + [Fact] + public void Can_Register_And_Use_Fake_Redactor_With_Configuration_Section_Options_With_Data_Classification() + { + var dc = new DataClassification("Foo", 0x1); + + var data = "Lalaalal"; + using var services = new ServiceCollection() + .AddLogging() + .AddRedaction(x => x.SetFakeRedactor(Setup.GetFakesConfiguration(), dc)) + .BuildServiceProvider(); + + var provider = services.GetRequiredService(); + + var r = provider.GetRedactor(dc); + var redacted = r.Redact(data); + + Assert.IsAssignableFrom(r); + Assert.Contains(data, redacted); + } + + [Fact] + public void Can_Register_And_Use_Fake_Redactor_With_Configuration_Section_Options() + { + var dc = new DataClassification("TAX", 1); + var data = "Lalaalal"; + using var services = new ServiceCollection() + .AddLogging() + .AddRedaction(x => x.SetFakeRedactor(Setup.GetFakesConfiguration(), dc)) + .BuildServiceProvider(); + + var provider = services.GetRequiredService(); + + var r = provider.GetRedactor(dc); + var redacted = r.Redact(data); + + Assert.IsAssignableFrom(r); + Assert.Contains(data, redacted); + } + + [Fact] + public void Can_Register_And_Use_Fake_Redactor_With_Action_Options_With_DataClassification() + { + var dc = new DataClassification("Foo", 0x1); + + var data = "Lalaalal"; + using var services = new ServiceCollection() + .AddLogging() + .AddRedaction(x => x.SetFakeRedactor(x => { x.RedactionFormat = "xxx{0}xxx"; }, dc)) + .BuildServiceProvider(); + + var provider = services.GetRequiredService(); + + var r = provider.GetRedactor(dc); + var redacted = r.Redact(data); + + Assert.IsAssignableFrom(r); + Assert.Contains(data, redacted); + } + + [Fact] + public void AddRedactionAndSetFakeRedactor_Pick_Up_Options_Correctly() + { + var dc = new DataClassification("TAX", 1); + var data = "Lalaalal"; + using var services = new ServiceCollection() + .AddLogging() + .AddFakeRedaction() + .AddRedaction(builder => builder.SetFakeRedactor(options => { options.RedactionFormat = "xxx{0}xxx"; }, dc)) + .BuildServiceProvider(); + + var provider = services.GetRequiredService(); + + var r = provider.GetRedactor(dc); + var redacted = r.Redact(data); + + Assert.IsAssignableFrom(r); + Assert.Equal($"xxx{data}xxx", redacted); + } + + [Fact] + public void AddRedactionWithActionAndSetFakeRedactor_Pick_Up_Options_Correctly() + { + var dc = new DataClassification("TAX", 1); + var data = "Lalaalal"; + using var services = new ServiceCollection() + .AddLogging() + .AddFakeRedaction(_ => { }) + .AddRedaction(builder => builder.SetFakeRedactor(options => { options.RedactionFormat = "xxx{0}xxx"; }, dc)) + .BuildServiceProvider(); + + var provider = services.GetRequiredService(); + + var r = provider.GetRedactor(dc); + var redacted = r.Redact(data); + + Assert.IsAssignableFrom(r); + Assert.Equal($"xxx{data}xxx", redacted); + } + + [Fact] + public void SetFakeRedactorAndAddRedaction_Pick_Up_Options_Correctly() + { + var dc = new DataClassification("TAX", 1); + var data = "Lalaalal"; + using var services = new ServiceCollection() + .AddLogging() + .AddRedaction(builder => builder.SetFakeRedactor(options => { options.RedactionFormat = "xxx{0}xxx"; }, dc)) + .AddFakeRedaction() + .BuildServiceProvider(); + + var provider = services.GetRequiredService(); + + var r = provider.GetRedactor(dc); + var redacted = r.Redact(data); + + Assert.IsAssignableFrom(r); + Assert.Equal($"xxx{data}xxx", redacted); + } + + [Fact] + public void SetFakeRedactorAndAddRedactionWithAction_Pick_Up_Options_Correctly() + { + var dc = new DataClassification("TAX", 1); + var data = "Lalaalal"; + using var services = new ServiceCollection() + .AddLogging() + .AddRedaction(builder => builder.SetFakeRedactor(options => { options.RedactionFormat = "xxx{0}xxx"; }, dc)) + .AddFakeRedaction(_ => { }) + .BuildServiceProvider(); + + var provider = services.GetRequiredService(); + + var r = provider.GetRedactor(dc); + var redacted = r.Redact(data); + + Assert.IsAssignableFrom(r); + Assert.Equal($"xxx{data}xxx", redacted); + } + + [Fact] + public void RedactionFakesEventCollector_Can_Be_Obtained_From_DI_And_Show_Redaction_History() + { + var data = "Lalaalal"; + var data2 = "Lalaalal222222222"; + var redactionFormat = "xxx"; + + using var services = new ServiceCollection() + .AddLogging() + .AddFakeRedaction(x => x.RedactionFormat = redactionFormat) + .BuildServiceProvider(); + + var provider = services.GetRequiredService(); + var collector = services.GetFakeRedactionCollector(); + + var dc = new DataClassification("TAX", 1); + var r = provider.GetRedactor(dc); + var redacted = r.Redact(data); + var redacted2 = r.Redact(data2); + + Assert.Equal(2, collector.AllRedactedData.Count); + Assert.Equal(data2, collector.LastRedactedData.Original); + Assert.Equal(redacted2, collector.LastRedactedData.Redacted); + Assert.Equal(2, collector.LastRedactedData.SequenceNumber); + + Assert.Equal(1, collector.AllRedactedData[0].SequenceNumber); + Assert.Equal(data, collector.AllRedactedData[0].Original); + Assert.Equal(redacted, collector.AllRedactedData[0].Redacted); + + Assert.Equal(1, collector.LastRedactorRequested.SequenceNumber); + Assert.Equal(dc, collector.LastRedactorRequested.DataClassification); + Assert.Equal(1, collector.AllRedactorRequests.Count); + } + + [Fact] + public void Fake_Redaction_Extensions_Does_Not_Allow_Null_Arguments() + { + var dc = new DataClassification("TAX", 1); + + Assert.Throws(() => ((IRedactionBuilder)null!).SetFakeRedactor(dc)); + Assert.Throws(() => ((IRedactionBuilder)null!).SetFakeRedactor(Setup.GetFakesConfiguration(), dc)); + Assert.Throws(() => ((IRedactionBuilder)null!).SetFakeRedactor(x => x.RedactionFormat = "2", dc)); + Assert.Throws(() => new ServiceCollection().AddRedaction(x => x.SetFakeRedactor((Action)null!, dc))); + + Assert.Throws(() => ((IServiceCollection)null!).AddRedaction(x => x.SetFakeRedactor((Action)null!, dc))); + Assert.Throws(() => ((IServiceCollection)null!).AddFakeRedaction()); + Assert.Throws(() => ((IServiceCollection)null!).AddFakeRedaction(_ => { })); + Assert.Throws(() => new ServiceCollection().AddFakeRedaction(null!)); + } + + [Fact] + public void Fake_Redaction_Works_Fine_Without_Any_Config() + { + using var sp = new ServiceCollection().AddFakeRedaction().BuildServiceProvider(); + + var rp = sp.GetRequiredService(); + var dc = new DataClassification("TAX", 1); + var r = rp.GetRedactor(dc); + var collector = sp.GetFakeRedactionCollector(); + + var redacted = r.Redact("dddd"); + + Assert.Equal(dc, collector.LastRedactorRequested.DataClassification); + Assert.Equal(redacted, collector.LastRedactedData.Redacted); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/RedactionFakesEventCollectorTest.cs b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/RedactionFakesEventCollectorTest.cs new file mode 100644 index 0000000000..cc4f064605 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/RedactionFakesEventCollectorTest.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Compliance.Testing.Test; + +public class RedactionFakesEventCollectorTest +{ + [Fact] + public void When_No_Records_Cannot_Obtain_Last_Events() + { + var c = new FakeRedactionCollector(); + + Assert.Throws(() => c.LastRedactorRequested); + Assert.Throws(() => c.LastRedactedData); + } + + [Fact] + public void RedactionFakesEventCollector_Cannot_Be_Retrieved_From_DI_When_Not_Registered() + { + using var sp = new ServiceCollection().BuildServiceProvider(); + + Assert.Throws(() => sp.GetFakeRedactionCollector()); + } + + [Fact] + public void DataRedacted_Collected_By_Collector_Support_Value_Semantic_Comparisons() + { + var first = new RedactedData(string.Empty, string.Empty, 0); + var second = new RedactedData(string.Empty, string.Empty, 0); + var third = new RedactedData("d", string.Empty, 0); + var fourth = new RedactedData(string.Empty, string.Empty, 1); + var fifth = new RedactedData(string.Empty, "d", 1); + var @object = new object(); + + Assert.True(first.Equals(second)); + Assert.False(first.Equals(third)); + Assert.False(first.Equals(@object)); + Assert.True(first.Equals((object)second)); + Assert.True(first == second); + Assert.True(first != third); + Assert.True(first != fifth); + Assert.False(first == fourth); + Assert.NotEqual(first.GetHashCode(), third.GetHashCode()); + } + + [Fact] + public void RedactorRequested_Supports_Value_Semantic_Comparisons() + { + var dc = new DataClassification("TAX", 1); + var first = new RedactorRequested(dc, 0); + var second = new RedactorRequested(dc, 0); + var third = new RedactorRequested(dc, 1); + var fourth = new RedactorRequested(dc, 0); + var @object = new object(); + + Assert.True(first.Equals(second)); + Assert.False(first.Equals(third)); + Assert.False(first.Equals(@object)); + Assert.True(first.Equals((object)second)); + Assert.True(first == second); + Assert.True(first != third); + Assert.True(first == fourth); + Assert.NotEqual(first.GetHashCode(), third.GetHashCode()); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/Setup.cs b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/Setup.cs new file mode 100644 index 0000000000..3266e05d78 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/Setup.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Extensions.Compliance.Testing.Test; + +public static class Setup +{ + public static IConfigurationSection GetFakesConfiguration() + { + FakeRedactorOptions options; + + return new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { $"{nameof(FakeRedactorOptions)}:{nameof(options.RedactionFormat)}", "What is it? O_o '{0}'" }, + }) + .Build() + .GetSection(nameof(FakeRedactorOptions)); + } +} + diff --git a/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/TaxonomyExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/TaxonomyExtensionsTest.cs new file mode 100644 index 0000000000..d2980a52fc --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Compliance.Testing.Tests/TaxonomyExtensionsTest.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Compliance.Classification; +using Xunit; + +namespace Microsoft.Extensions.Compliance.Testing.Tests; + +public static class TaxonomyExtensionsTest +{ + [Fact] + public static void AsSimpleTaxonomy() + { + var dc = new DataClassification("Foo", 123); + Assert.Throws(() => dc.AsSimpleTaxonomy()); + + Assert.Equal(SimpleTaxonomy.None, DataClassification.None.AsSimpleTaxonomy()); + Assert.Equal(SimpleTaxonomy.Unknown, DataClassification.Unknown.AsSimpleTaxonomy()); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Abstractions/ExceptionSummaryTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Abstractions/ExceptionSummaryTest.cs new file mode 100644 index 0000000000..9e441cebd7 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Abstractions/ExceptionSummaryTest.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests; + +public class ExceptionSummaryTest +{ + [Fact] + public void Equals_WhenComparingTwoIdentical_ReturnsTrue() + { + var exceptionSummary1 = new ExceptionSummary("ExceptionType1", "Exception Description 1", "Exception Additional Details 1"); + var exceptionSummary2 = new ExceptionSummary("ExceptionType1", "Exception Description 1", "Exception Additional Details 1"); + + Assert.True(exceptionSummary1.Equals(exceptionSummary2)); + Assert.True(exceptionSummary2.Equals(exceptionSummary1)); + Assert.Equal(exceptionSummary1, exceptionSummary2); + Assert.Equal(exceptionSummary1.GetHashCode(), exceptionSummary2.GetHashCode()); + + Assert.True(exceptionSummary1 == exceptionSummary2); + Assert.True(exceptionSummary1.Equals((object)exceptionSummary2)); + Assert.False(exceptionSummary1 != exceptionSummary2); + } + + [Fact] + public void Equals_WhenComparingTwoDifferent_ReturnsTrue() + { + var exceptionSummary1 = new ExceptionSummary("ExceptionType1", "Exception Description 1", "Exception Additional Details 1"); + var exceptionSummary2 = new ExceptionSummary("ExceptionType2", "Exception Description 2", "Exception Additional Details 2"); + + Assert.False(exceptionSummary1.Equals(exceptionSummary2)); + Assert.False(exceptionSummary2.Equals(exceptionSummary1)); + Assert.NotEqual(exceptionSummary1, exceptionSummary2); + Assert.NotEqual(exceptionSummary1.GetHashCode(), exceptionSummary2.GetHashCode()); + + Assert.False(exceptionSummary1 == exceptionSummary2); + Assert.False(exceptionSummary1.Equals((object)exceptionSummary2)); + Assert.False(exceptionSummary2.Equals((object)exceptionSummary1)); + Assert.True(exceptionSummary1 != exceptionSummary2); + } + + [Fact] + public void Equals_WhenComparingTwoDifferentObject_ReturnsTrue() + { + var exceptionSummary = new ExceptionSummary("ExceptionType1", "Exception Description 1", "Exception Additional Details 1"); + var exceptionTest = new ExceptionTest(); + + Assert.False(exceptionSummary.Equals(exceptionTest)); + Assert.False(exceptionTest.Equals(exceptionSummary)); + Assert.NotEqual(exceptionSummary.GetHashCode(), exceptionTest.GetHashCode()); + } + + [Fact] + public void Equals_Misc() + { + var s1 = new ExceptionSummary("One", "Two", "Three"); + var s2 = new ExceptionSummary("One", "Two", "Three"); + Assert.True(s1.Equals(s2)); + + var s3 = new ExceptionSummary("One", "Two", "Three"); + var s4 = new ExceptionSummary("One", "Four", "Three"); + Assert.False(s3.Equals(s4)); + + var s5 = new ExceptionSummary("One", "Four", "Three"); + var s6 = new ExceptionSummary("One", "Two", "Three"); + Assert.False(s5.Equals(s6)); + + var s7 = new ExceptionSummary("One", "Two", "Three"); + var s8 = new ExceptionSummary("Four", "Two", "Three"); + Assert.False(s7.Equals(s8)); + + var s9 = new ExceptionSummary("Four", "Two", "Three"); + var s10 = new ExceptionSummary("One", "Two", "Three"); + Assert.False(s9.Equals(s10)); + + var s12 = new ExceptionSummary("One", "Two", "Three"); + var s13 = new ExceptionSummary("Four", "Five", "Six"); + Assert.False(s12.Equals(s13)); + } + + [Fact] + public void ToStringTest() + { + var exceptionSummary1 = new ExceptionSummary("ExceptionType", "Exception Description", "Exception Long Description"); + Assert.Equal("ExceptionType:Exception Description:Exception Long Description", exceptionSummary1.ToString()); + + Assert.Throws(() => new ExceptionSummary("", "Exception Description", "Exception Long Description")); + Assert.Throws(() => new ExceptionSummary(" ", "Exception Description", "Exception Long Description")); + + Assert.Throws(() => new ExceptionSummary("ExceptionType", "", "Exception Long Description")); + Assert.Throws(() => new ExceptionSummary("ExceptionType", " ", "Exception Long Description")); + + Assert.Throws(() => new ExceptionSummary("ExceptionType", "Exception Description", null!)); + } + + private class ExceptionTest + { + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/ExceptionSummarizerTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/ExceptionSummarizerTests.cs new file mode 100644 index 0000000000..f2a36086aa --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/ExceptionSummarizerTests.cs @@ -0,0 +1,297 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests; + +public class ExceptionSummarizerTests +{ + private const string DefaultDescription = "Unknown"; + private const string AdditionalDetails = "Some additional details"; + private static readonly List _httpDescriptions = new List { "TaskTimeout", "TaskCanceled" } + .Concat(Enum.GetNames(typeof(WebExceptionStatus)).ToList()) + .Concat(Enum.GetNames(typeof(SocketError)).ToList()).ToList(); + private static readonly List _httpSupportedExceptionTypes = new() + { + typeof(TaskCanceledException), + typeof(OperationCanceledException), + typeof(WebException), + typeof(SocketException), + }; + + private readonly Mock _httpExceptionProviderMock; + private readonly IExceptionSummarizer _exceptionSummarizer; + + public ExceptionSummarizerTests() + { + _httpExceptionProviderMock = new Mock(MockBehavior.Strict); + _httpExceptionProviderMock.Setup(mock => mock.SupportedExceptionTypes).Returns(_httpSupportedExceptionTypes); + _httpExceptionProviderMock.Setup(mock => mock.Descriptions).Returns(_httpDescriptions); + + _exceptionSummarizer = new ExceptionSummarizer(new List { _httpExceptionProviderMock.Object }); + } + + [Fact] + public void Summarize_WithProviderSummaryAndInvalidIndex_ReturnSummary() + { + var exception = new WebException("test", WebExceptionStatus.RequestCanceled); + var descriptionIndex = 1000; + var additionalDetails = $"Exception summary provider {_httpExceptionProviderMock.Object.GetType().Name} returned invalid short description index {descriptionIndex}"; + var exceptionSummary = new ExceptionSummary("WebException", DefaultDescription, additionalDetails); + + _httpExceptionProviderMock + .Setup(mock => mock.Describe(exception, out additionalDetails)).Returns(1000); + + var summary = _exceptionSummarizer.Summarize(exception); + + Assert.Equal(exceptionSummary, summary); + Assert.Equal(exceptionSummary.ToString(), summary.ToString()); + Assert.Equal(exceptionSummary.AdditionalDetails, summary.AdditionalDetails); + } + + [Fact] + public void Summarize_WithProviderSummaryAndIndexSameAsCount_ReturnSummary() + { + var exception = new WebException("test", WebExceptionStatus.RequestCanceled); + var descriptionIndex = _httpDescriptions.Count; + var additionalDetails = $"Exception summary provider {_httpExceptionProviderMock.Object.GetType().Name} returned invalid short description index {descriptionIndex}"; + var exceptionSummary = new ExceptionSummary("WebException", DefaultDescription, additionalDetails); + + _httpExceptionProviderMock + .Setup(mock => mock.Describe(exception, out additionalDetails)).Returns(descriptionIndex); + + var summary = _exceptionSummarizer.Summarize(exception); + + Assert.Equal(exceptionSummary, summary); + Assert.Equal(exceptionSummary.ToString(), summary.ToString()); + Assert.Equal(exceptionSummary.AdditionalDetails, summary.AdditionalDetails); + } + + [Fact] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Justification = "Intended.")] + public void Summarize_WithInnerWebExceptionAndInvalidIndex_ReturnSummary() + { + var exception = new Exception("test", new WebException("Error", (WebExceptionStatus)30)); + var descriptionIndex = -1; + var additionalDetails = $"Exception summary provider {_httpExceptionProviderMock.Object.GetType().Name} returned invalid short description index {descriptionIndex}"; + var innerExceptionSummary = new ExceptionSummary("Exception->WebException", DefaultDescription, additionalDetails); + + if (exception.InnerException != null) + { + _httpExceptionProviderMock + .Setup(mock => mock.Describe(exception.InnerException, out additionalDetails)) + .Returns(descriptionIndex); + } + + var summary = _exceptionSummarizer.Summarize(exception); + + Assert.Equal(innerExceptionSummary, summary); + Assert.Equal(innerExceptionSummary.ToString(), summary.ToString()); + } + + [Fact] + public void Summarize_WithProviderSummaryAndValidIndex_ReturnSummary() + { + var exception = new WebException("test", WebExceptionStatus.RequestCanceled); + var descriptionIndex = _httpDescriptions.FindIndex(x => x.Equals(WebExceptionStatus.RequestCanceled.ToString())); + var additionalDetails = DefaultDescription; + var exceptionSummary = new ExceptionSummary("WebException", "RequestCanceled", additionalDetails); + + _httpExceptionProviderMock + .Setup(mock => mock.Describe(exception, out additionalDetails)) + .Returns(descriptionIndex); + + var summary = _exceptionSummarizer.Summarize(exception); + + Assert.Equal(exceptionSummary, summary); + Assert.Equal(exceptionSummary.ToString(), summary.ToString()); + } + + [Fact] + public void Summarize_WithProviderSummaryAndAdditionalDetails_ReturnSummary() + { + var exception = new WebException("test", WebExceptionStatus.RequestCanceled); + var descriptionIndex = _httpDescriptions.FindIndex(x => x.Equals(WebExceptionStatus.RequestCanceled.ToString())); + var additionalDetails = AdditionalDetails; + var exceptionSummary = new ExceptionSummary("WebException", "RequestCanceled", additionalDetails); + + _httpExceptionProviderMock + .Setup(mock => mock.Describe(exception, out additionalDetails)) + .Returns(descriptionIndex); + + var summary = _exceptionSummarizer.Summarize(exception); + + Assert.Equal(exceptionSummary, summary); + Assert.Equal(exceptionSummary.ToString(), summary.ToString()); + } + + [Theory] + [InlineData(AdditionalDetails)] + [InlineData(DefaultDescription)] + public void Summarize_WithWebException_ReturnSummary(string? additionalDetails) + { + var exception = new WebException("test", WebExceptionStatus.ConnectFailure); + var descriptionIndex = _httpDescriptions.FindIndex(x => x.Equals(WebExceptionStatus.ConnectFailure.ToString())); + var exceptionSummary = new ExceptionSummary("WebException", "ConnectFailure", additionalDetails ?? DefaultDescription); + + _httpExceptionProviderMock + .Setup(mock => mock.Describe(exception, out additionalDetails)) + .Returns(descriptionIndex); + + var summary = _exceptionSummarizer.Summarize(exception); + + Assert.Equal(exceptionSummary, summary); + } + + [Theory] + [InlineData(AdditionalDetails)] + [InlineData(DefaultDescription)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Justification = "Intended.")] + public void Summarize_WithInnerWebException_ReturnSummary(string? additionalDetails) + { + var exception = new Exception("test", new WebException("test", WebExceptionStatus.RequestCanceled)); + var descriptionIndex = _httpDescriptions + .FindIndex(x => x.Equals(WebExceptionStatus.RequestCanceled.ToString())); + var innerExceptionSummary = new ExceptionSummary("Exception->WebException", "RequestCanceled", additionalDetails ?? DefaultDescription); + + if (exception.InnerException != null) + { + _httpExceptionProviderMock + .Setup(mock => mock.Describe(exception.InnerException, out additionalDetails)) + .Returns(descriptionIndex); + } + + var summary = _exceptionSummarizer.Summarize(exception); + + Assert.Equal(innerExceptionSummary, summary); + Assert.Equal(innerExceptionSummary.ToString(), summary.ToString()); + } + + [Theory] + [InlineData(AdditionalDetails)] + [InlineData(DefaultDescription)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Justification = "Intended.")] + public void Summarize_WithInnerSocketException_ReturnSummary(string? additionalDetails) + { + var exception = new Exception("test", new SocketException((int)SocketError.TimedOut)); + var descriptionIndex = _httpDescriptions.FindIndex(x => x.Equals(SocketError.TimedOut.ToString())); + var innerExceptionSummary = new ExceptionSummary("Exception->SocketException", "TimedOut", additionalDetails ?? DefaultDescription); + + if (exception.InnerException != null) + { + _httpExceptionProviderMock + .Setup(mock => mock.Describe(exception.InnerException, out additionalDetails)) + .Returns(descriptionIndex); + } + + var summary = _exceptionSummarizer.Summarize(exception); + + Assert.Equal(innerExceptionSummary, summary); + Assert.Equal(innerExceptionSummary.ToString(), summary.ToString()); + } + + [Theory] + [InlineData(AdditionalDetails)] + [InlineData(DefaultDescription)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Justification = "Intended.")] + public async Task Summarize_WithInnerTaskCanceledException_ReturnSummary(string? additionalDetails) + { + var descriptionIndex = _httpDescriptions.FindIndex(x => x.Equals("TaskCanceled")); + + try + { + using var tokenSource = new CancellationTokenSource(TimeSpan.Zero); + tokenSource.Cancel(); + var task = Task.Run(() => { }, tokenSource.Token); + + await task; + } + catch (TaskCanceledException ex) + { + var exception = new Exception("test", ex); + var innerExceptionSummary = new ExceptionSummary("Exception->TaskCanceledException", "TaskCanceled", additionalDetails ?? DefaultDescription); + + if (exception.InnerException != null) + { + _httpExceptionProviderMock + .Setup(mock => mock.Describe(exception.InnerException, out additionalDetails)) + .Returns(descriptionIndex); + } + + var summary = _exceptionSummarizer.Summarize(exception); + + Assert.Equal(innerExceptionSummary, summary); + Assert.Equal(innerExceptionSummary.ToString(), summary.ToString()); + } + } + + [Theory] + [InlineData(AdditionalDetails)] + [InlineData(DefaultDescription)] + [InlineData(null)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Justification = "Intended.")] + public void Summarize_WithInnerTaskTimeout_ReturnSummary(string? additionalDetails) + { + var exception = new Exception("test", new TaskCanceledException()); + var descriptionIndex = _httpDescriptions.FindIndex(x => x.Equals("TaskTimeout")); + var innerExceptionSummary = new ExceptionSummary("Exception->TaskCanceledException", "TaskTimeout", additionalDetails ?? DefaultDescription); + + if (exception.InnerException != null) + { + _httpExceptionProviderMock + .Setup(mock => mock.Describe(exception.InnerException, out additionalDetails)) + .Returns(descriptionIndex); + } + + var summary = _exceptionSummarizer.Summarize(exception); + + Assert.Equal(innerExceptionSummary, summary); + Assert.Equal(innerExceptionSummary.ToString(), summary.ToString()); + } + + [Fact] + public void Summarize_WithNotDefaultHResult_ReturnSummary() + { + uint resultCode = 0x80131501; + var exception = new TestException(resultCode); + var exceptionHResultSummary = new ExceptionSummary("TestException", "Unknown", "-2146233087"); + + var summary = _exceptionSummarizer.Summarize(exception); + + Assert.Equal(exceptionHResultSummary, summary); + Assert.Equal(exceptionHResultSummary.ToString(), summary.ToString()); + } + + [Fact] + public void Summarize_WithDefaultHResultAndWithoutInnerException_ReturnDefaultSummary() + { + var exception = new TestException(0); + var exceptionHResultSummary = new ExceptionSummary("TestException", "Unknown", "Unknown"); + + var summary = _exceptionSummarizer.Summarize(exception); + + Assert.Equal(exceptionHResultSummary, summary); + Assert.Equal(exceptionHResultSummary.ToString(), summary.ToString()); + } + + [Fact] + public void Summarize_WithDefaultHResultAndInnerException_ReturnSummary() + { + var exception = new TestException(0, "Test", new TestException(0)); + var exceptionHResultSummary = new ExceptionSummary("TestException", "TestException", "Unknown"); + + var summary = _exceptionSummarizer.Summarize(exception); + + Assert.Equal(exceptionHResultSummary, summary); + Assert.Equal(exceptionHResultSummary.ToString(), summary.ToString()); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/ExceptionSummaryExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/ExceptionSummaryExtensionsTests.cs new file mode 100644 index 0000000000..19f9480af2 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/ExceptionSummaryExtensionsTests.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests; + +public class ExceptionSummaryExtensionsTests +{ + [Fact] + public void AddExceptionSummarizer_WithServiceCollection_AddsExceptionSummarizer() + { + IServiceCollection services = new ServiceCollection(); + services = services.AddExceptionSummarizer(); + + var summarizer = services.BuildServiceProvider().GetService(); + + Assert.NotNull(summarizer); + Assert.IsType(summarizer); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/HttpExceptionSummaryProviderExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/HttpExceptionSummaryProviderExtensionsTests.cs new file mode 100644 index 0000000000..2a1cfcf314 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/HttpExceptionSummaryProviderExtensionsTests.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests; + +public class HttpExceptionSummaryProviderExtensionsTests +{ + [Fact] + public void AddHttpExceptionSummaryProvider_WithServiceCollection_AddsHttpExceptionSummaryProvider() + { + IServiceCollection services = new ServiceCollection(); + services = services.AddExceptionSummarizer(b => b.AddHttpProvider()); + + var exceptionSummaryProvider = services.BuildServiceProvider().GetService(); + + Assert.NotNull(exceptionSummaryProvider); + Assert.IsType(exceptionSummaryProvider); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/HttpExceptionSummaryProviderTests.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/HttpExceptionSummaryProviderTests.cs new file mode 100644 index 0000000000..89f7623eeb --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/HttpExceptionSummaryProviderTests.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests; + +public class HttpExceptionSummaryProviderTests +{ + private const int DefaultDescriptionIndex = -1; + private readonly HttpExceptionSummaryProvider _exceptionSummaryProvider = new(); + + public static IEnumerable SocketErrors() + { + foreach (var socketError in Enum.GetValues(typeof(SocketError))) + { + if (socketError != null) + { + yield return new[] { socketError }; + } + } + } + + public static IEnumerable WebExceptionStatuses() + { + foreach (var webException in Enum.GetValues(typeof(WebExceptionStatus))) + { + if (webException != null) + { + yield return new[] { webException }; + } + } + } + + [Fact] + public void Describe_WithNullException_ThrowsArgumentNullException() + { + Exception? ex = null!; + Assert.ThrowsAny(() => + _exceptionSummaryProvider.Describe(ex, out var additionalDetails)); + } + + [Fact] + public void SupportedExceptionTypes_ContainsIntendedHttpExceptions() + { + var httpExceptionTypes = new[] + { + typeof(TaskCanceledException), + typeof(OperationCanceledException), + typeof(WebException), + typeof(SocketException) + }; + + Assert.Equal(httpExceptionTypes, _exceptionSummaryProvider.SupportedExceptionTypes); + Assert.Contains(typeof(SocketException), _exceptionSummaryProvider.SupportedExceptionTypes); + Assert.Contains(typeof(WebException), _exceptionSummaryProvider.SupportedExceptionTypes); + Assert.Contains(typeof(OperationCanceledException), _exceptionSummaryProvider.SupportedExceptionTypes); + } + + [Theory] + [MemberData(nameof(WebExceptionStatuses))] + public void Describe_WithKnownWebException_ReturnDetails(WebExceptionStatus webExceptionStatus) + { + Exception exception = new WebException("test", webExceptionStatus); + var descriptionIndex = _exceptionSummaryProvider + .Describe(exception, out var additionalDetails); + + Assert.Equal(webExceptionStatus.ToString(), _exceptionSummaryProvider.Descriptions[descriptionIndex]); + Assert.Null(additionalDetails); + } + + [Fact] + public void Describe_WithUnknownWebExceptionStatus_ReturnDefaultDetails() + { + var exception = new WebException("test", (WebExceptionStatus)12345); + var descriptionIndex = _exceptionSummaryProvider + .Describe(exception, out var additionalDetails); + + Assert.Equal(DefaultDescriptionIndex, descriptionIndex); + Assert.Null(additionalDetails); + } + + [Theory] + [MemberData(nameof(SocketErrors))] + public void Describe_WithKnownSocketException_ReturnDetails(SocketError socketError) + { + Exception exception = new SocketException((int)socketError); + var descriptionIndex = _exceptionSummaryProvider.Describe(exception, out var additionalDetails); + + Assert.Equal(socketError.ToString(), _exceptionSummaryProvider.Descriptions[descriptionIndex]); + Assert.Null(additionalDetails); + } + + [Fact] + public void Describe_WithUnknownSocketError_ReturnDefaultDetails() + { + var errorCode = -2; + Exception exception = new SocketException(errorCode); + + var descriptionIndex = _exceptionSummaryProvider + .Describe(exception, out var additionalDetails); + + Assert.Equal(DefaultDescriptionIndex, descriptionIndex); + Assert.Null(additionalDetails); + } + + [Fact] + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Intended.")] + public async Task Describe_WithTaskCanceledException_ReturnDetails() + { + try + { + using var tokenSource = new CancellationTokenSource(TimeSpan.Zero); + tokenSource.Cancel(); + var task = Task.Run(() => { }, tokenSource.Token); + + await task; + } + catch (Exception exception) + { + var descriptionIndex = _exceptionSummaryProvider.Describe(exception, out var additionalDetails); + + Assert.Equal("TaskCanceled", _exceptionSummaryProvider.Descriptions[descriptionIndex]); + Assert.Null(additionalDetails); + } + + Exception exception2 = new TaskCanceledException(); + + var descriptionIndex2 = _exceptionSummaryProvider.Describe(exception2, out var additionalDetails2); + + Assert.Equal("TaskTimeout", _exceptionSummaryProvider.Descriptions[descriptionIndex2]); + Assert.Null(additionalDetails2); + } + + [Fact] + public void Describe_WithUnknownException_ReturnDefaultDetails() + { + Exception exception = new ArgumentException("This is not an exception that HttpExceptionProvider understands"); + + var descriptionIndex = _exceptionSummaryProvider + .Describe(exception, out var additionalDetails); + + Assert.Equal(DefaultDescriptionIndex, descriptionIndex); + Assert.Null(additionalDetails); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/TestException.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/TestException.cs new file mode 100644 index 0000000000..5487e9a50f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Implementation/TestException.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests; + +public class TestException : Exception +{ + public TestException() + { + } + + public TestException(string message) + : base(message) + { + } + + public TestException(string message, Exception innerException) + : base(message, innerException) + { + } + + public TestException(uint hresult, string message, Exception innerException) + : base(message, innerException) + { + HResult = (int)hresult; + } + + public TestException(uint hresult) + { + HResult = (int)hresult; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests.csproj b/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests.csproj new file mode 100644 index 0000000000..39cde60837 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests/Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests.csproj @@ -0,0 +1,14 @@ + + + Microsoft.Extensions.Diagnostics.ExceptionSummarization.Tests + Unit tests for Microsoft.Extensions.Diagnostics.ExceptionSummarization + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ApplicationLifecycleHealthCheckTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ApplicationLifecycleHealthCheckTest.cs new file mode 100644 index 0000000000..a28347a4f4 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ApplicationLifecycleHealthCheckTest.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests; + +public class ApplicationLifecycleHealthCheckTest +{ + [Fact] + public async Task CheckHealthAsync_AssertHealthStatusAfterApplicationLifecycleEvents() + { + using MockHostApplicationLifetime mockHostApplicationLifetime = new MockHostApplicationLifetime(); + ApplicationLifecycleHealthCheck healthCheck = new ApplicationLifecycleHealthCheck(mockHostApplicationLifetime); + HealthCheckContext context = new HealthCheckContext(); + + Assert.Equal(HealthStatus.Unhealthy, (await healthCheck.CheckHealthAsync(context, CancellationToken.None)).Status); + + mockHostApplicationLifetime.StartApplication(); + Assert.Equal(HealthStatus.Healthy, (await healthCheck.CheckHealthAsync(context, CancellationToken.None)).Status); + + mockHostApplicationLifetime.StoppingApplication(); + Assert.Equal(HealthStatus.Unhealthy, (await healthCheck.CheckHealthAsync(context, CancellationToken.None)).Status); + + mockHostApplicationLifetime.StopApplication(); + Assert.Equal(HealthStatus.Unhealthy, (await healthCheck.CheckHealthAsync(context, CancellationToken.None)).Status); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ApplicationLifecycleHealthChecksExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ApplicationLifecycleHealthChecksExtensionsTest.cs new file mode 100644 index 0000000000..3101fc5620 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ApplicationLifecycleHealthChecksExtensionsTest.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests; + +public class ApplicationLifecycleHealthChecksExtensionsTest +{ + [Fact] + public void AddApplicationLifecycleHealthCheck_DependenciesAreRegistered() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(new Mock().Object); + serviceCollection.AddHealthChecks().AddApplicationLifecycleHealthCheck(); + + AssertAddedHealthCheck(serviceCollection); + } + + [Fact] + public void AddApplicationLifecycleHealthCheck_WithTags_DependenciesAreRegistered() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(new Mock().Object); + serviceCollection.AddHealthChecks().AddApplicationLifecycleHealthCheck(new[] { "test1", "test2" }); + + AssertAddedHealthCheck(serviceCollection); + } + + [Fact] + public void TestNullChecks() + { + Assert.Throws(() => ((IHealthChecksBuilder)null!).AddApplicationLifecycleHealthCheck()); + Assert.Throws(() => ((IHealthChecksBuilder)null!).AddApplicationLifecycleHealthCheck(null!)); + } + + private static void AssertAddedHealthCheck(IServiceCollection serviceCollection) + { + using var serviceProvider = serviceCollection.BuildServiceProvider(); + var registrations = serviceProvider.GetRequiredService>().Value.Registrations; + + Assert.Single(registrations); + foreach (var r in registrations) + { + Assert.True(r.Factory(serviceProvider) is T); + Assert.Equal("ApplicationLifecycleHealthCheck", r.Name); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/KubernetesHealthCheckPublisherExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/KubernetesHealthCheckPublisherExtensionsTest.cs new file mode 100644 index 0000000000..8cf2e96cd3 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/KubernetesHealthCheckPublisherExtensionsTest.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests; + +public class KubernetesHealthCheckPublisherExtensionsTest +{ + [Fact] + public void AddKubernetesHealthCheckPublisherTest() + { + var serviceCollection = new ServiceCollection(); + + _ = serviceCollection.Configure(o => { }); + serviceCollection.AddKubernetesHealthCheckPublisher(); + + using var serviceProvider = serviceCollection.BuildServiceProvider(); + + var publishers = serviceProvider.GetRequiredService>(); + + Assert.Single(publishers); + foreach (var p in publishers) + { + Assert.True(p is KubernetesHealthCheckPublisher); + } + } + + [Fact] + public void AddKubernetesHealthCheckPublisherTest_WithAction() + { + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddKubernetesHealthCheckPublisher(o => + { + o.TcpPort = 2305; + o.MaxPendingConnections = 10; + }); + + using var serviceProvider = serviceCollection.BuildServiceProvider(); + + var publishers = serviceProvider.GetRequiredService>(); + + Assert.Single(publishers); + foreach (var p in publishers) + { + Assert.True(p is KubernetesHealthCheckPublisher); + } + } + + [Fact] + public void AddKubernetesHealthCheckPublisherTest_WithConfigurationSection() + { + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddKubernetesHealthCheckPublisher(SetupKubernetesHealthCheckPublisherConfiguration("2305", "10") + .GetSection(nameof(KubernetesHealthCheckPublisherOptions))); + + using var serviceProvider = serviceCollection.BuildServiceProvider(); + + var publishers = serviceProvider.GetRequiredService>(); + + Assert.Single(publishers); + foreach (var p in publishers) + { + Assert.True(p is KubernetesHealthCheckPublisher); + } + } + + [Fact] + public void TestNullChecks() + { + Assert.Throws(() => new ServiceCollection().AddKubernetesHealthCheckPublisher((Action)null!)); + Assert.Throws(() => new ServiceCollection().AddKubernetesHealthCheckPublisher((IConfigurationSection)null!)); + } + + private static IConfiguration SetupKubernetesHealthCheckPublisherConfiguration( + string tcpPort, + string maxLengthOfPendingConnectionsQueue) + { + KubernetesHealthCheckPublisherOptions kubernetesHealthCheckPublisherOptions; + + var configurationDict = new Dictionary + { + { + $"{nameof(KubernetesHealthCheckPublisherOptions)}:{nameof(kubernetesHealthCheckPublisherOptions.TcpPort)}", + tcpPort + }, + { + $"{nameof(KubernetesHealthCheckPublisherOptions)}:{nameof(kubernetesHealthCheckPublisherOptions.MaxPendingConnections)}", + maxLengthOfPendingConnectionsQueue + } + }; + + return new ConfigurationBuilder().AddInMemoryCollection(configurationDict).Build(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/KubernetesHealthCheckPublisherTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/KubernetesHealthCheckPublisherTest.cs new file mode 100644 index 0000000000..a56ebfd615 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/KubernetesHealthCheckPublisherTest.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Microsoft.TestUtilities; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests; + +public class KubernetesHealthCheckPublisherTest +{ + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux)] + public async Task PublishAsync_CheckIfTcpPortIsOpenedAfterHealthStatusEvents() + { + KubernetesHealthCheckPublisherOptions options = new KubernetesHealthCheckPublisherOptions(); + KubernetesHealthCheckPublisher publisher = new KubernetesHealthCheckPublisher(Microsoft.Extensions.Options.Options.Create(options)); + + Assert.False(IsTcpPortOpened()); + + await publisher.PublishAsync(CreateHealthReport(HealthStatus.Healthy), CancellationToken.None); + Assert.True(IsTcpPortOpened()); + + await publisher.PublishAsync(CreateHealthReport(HealthStatus.Healthy), CancellationToken.None); + Assert.True(IsTcpPortOpened()); + + await publisher.PublishAsync(CreateHealthReport(HealthStatus.Unhealthy), CancellationToken.None); + Assert.False(IsTcpPortOpened()); + + await publisher.PublishAsync(CreateHealthReport(HealthStatus.Unhealthy), CancellationToken.None); + Assert.False(IsTcpPortOpened()); + + await publisher.PublishAsync(CreateHealthReport(HealthStatus.Healthy), CancellationToken.None); + Assert.True(IsTcpPortOpened()); + } + + [Fact] + public void Ctor_ThrowsWhenOptionsValueNull() + { + Assert.Throws(() => new KubernetesHealthCheckPublisher(Microsoft.Extensions.Options.Options.Create(null!))); + } + + private static HealthReport CreateHealthReport(HealthStatus healthStatus) + { + HealthReportEntry entry = new HealthReportEntry(healthStatus, null, TimeSpan.Zero, null, null); + var healthStatusRecords = new Dictionary { { "id", entry } }; + return new HealthReport(healthStatusRecords, TimeSpan.Zero); + } + + private static bool IsTcpPortOpened() + { + try + { + using TcpClient tcpClient = new TcpClient("localhost", 2305); + return true; + } + catch (SocketException e) + { + if (e.SocketErrorCode == SocketError.ConnectionRefused) + { + return false; + } + else + { + throw; + } + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ManualHealthCheckExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ManualHealthCheckExtensionsTest.cs new file mode 100644 index 0000000000..f22510dd16 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ManualHealthCheckExtensionsTest.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests; + +public class ManualHealthCheckExtensionsTest +{ + [Fact] + public void AddManualHealthCheck_DependenciesAreRegistered() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddHealthChecks().AddManualHealthCheck(); + + AssertAddedHealthCheck(serviceCollection, "ManualHealthCheck"); + } + + [Fact] + public void AddManualHealthCheck_WithTags_DependenciesAreRegistered() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddHealthChecks().AddManualHealthCheck(new[] { "test1", "test2" }); + + AssertAddedHealthCheck(serviceCollection, "ManualHealthCheck"); + } + + [Fact] + public void TestNullChecks() + { + Assert.Throws(() => ((IServiceCollection)null!).AddHealthChecks().AddManualHealthCheck()); + Assert.Throws(() => ((IServiceCollection)null!).AddHealthChecks().AddManualHealthCheck(null!)); + } + + private static void AssertAddedHealthCheck(IServiceCollection serviceCollection, string name) + { + using var serviceProvider = serviceCollection.BuildServiceProvider(); + var registrations = serviceProvider.GetRequiredService>().Value.Registrations; + + Assert.Single(registrations); + foreach (var r in registrations) + { + Assert.True(r.Factory(serviceProvider) is T); + Assert.Equal(r.Name, name); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ManualHealthCheckTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ManualHealthCheckTest.cs new file mode 100644 index 0000000000..9c13bce64d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/ManualHealthCheckTest.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests; + +public class ManualHealthCheckTest +{ + [Fact] + public async Task CheckHealthAsync_Initial() + { + ManualHealthCheckTracker manualHealthCheckTracker = new ManualHealthCheckTracker(); + ManualHealthCheckService manualHealthCheckService = new ManualHealthCheckService(manualHealthCheckTracker); + HealthCheckContext context = new HealthCheckContext(); + + var healthCheckResult = await manualHealthCheckService.CheckHealthAsync(context, CancellationToken.None); + Assert.Equal(HealthStatus.Healthy, healthCheckResult.Status); + Assert.Null(healthCheckResult.Description); + + var manualHealthCheck = new ManualHealthCheck(manualHealthCheckTracker); + Assert.Equal(HealthStatus.Unhealthy, manualHealthCheck.Result.Status); + Assert.Equal("Initial state", manualHealthCheck.Result.Description); + manualHealthCheck.Dispose(); + } + + [Fact] + public async Task CheckHealthAsync_AssertHealthStatus_AfterChange() + { + ManualHealthCheckTracker manualHealthCheckTracker = new ManualHealthCheckTracker(); + ManualHealthCheckService manualHealthCheckService = new ManualHealthCheckService(manualHealthCheckTracker); + HealthCheckContext context = new HealthCheckContext(); + var manualHealthCheck = new ManualHealthCheck(manualHealthCheckTracker); + + Assert.Equal(HealthStatus.Unhealthy, (await manualHealthCheckService.CheckHealthAsync(context, CancellationToken.None)).Status); + Assert.Equal("Initial state", manualHealthCheck.Result.Description); + + manualHealthCheck.ReportUnhealthy("Test reason"); + var healthCheckResultUnhealthy = await manualHealthCheckService.CheckHealthAsync(context, CancellationToken.None); + Assert.Equal(HealthStatus.Unhealthy, healthCheckResultUnhealthy.Status); + Assert.Equal("Test reason", healthCheckResultUnhealthy.Description); + + manualHealthCheck.ReportHealthy(); + var healthCheckResultHealthy = await manualHealthCheckService.CheckHealthAsync(context, CancellationToken.None); + Assert.Equal(HealthStatus.Healthy, healthCheckResultHealthy.Status); + Assert.Null(healthCheckResultHealthy.Description); + Assert.Null(manualHealthCheck.Result.Description); + manualHealthCheck.Dispose(); + } + + [Fact] + public async Task CheckHealthAsync_AssertHealthStatus_AfterChangeWithReport() + { + ManualHealthCheckTracker manualHealthCheckTracker = new ManualHealthCheckTracker(); + ManualHealthCheckService manualHealthCheckService = new ManualHealthCheckService(manualHealthCheckTracker); + HealthCheckContext context = new HealthCheckContext(); + var manualHealthCheck = new ManualHealthCheck(manualHealthCheckTracker); + + Assert.Equal(HealthStatus.Unhealthy, (await manualHealthCheckService.CheckHealthAsync(context, CancellationToken.None)).Status); + Assert.Equal("Initial state", manualHealthCheck.Result.Description); + + var unhealthyCheck = HealthCheckResult.Unhealthy("Test reason"); + manualHealthCheck.Result = unhealthyCheck; + var healthCheckResultUnhealthy = await manualHealthCheckService.CheckHealthAsync(context, CancellationToken.None); + Assert.Equal(HealthStatus.Unhealthy, healthCheckResultUnhealthy.Status); + Assert.Equal("Test reason", healthCheckResultUnhealthy.Description); + + var healthyCheck = HealthCheckResult.Healthy(); + manualHealthCheck.Result = healthyCheck; + var healthCheckResultHealthy = await manualHealthCheckService.CheckHealthAsync(context, CancellationToken.None); + Assert.Equal(HealthStatus.Healthy, healthCheckResultHealthy.Status); + Assert.Null(healthCheckResultHealthy.Description); + Assert.Null(manualHealthCheck.Result.Description); + manualHealthCheck.Dispose(); + } + + [Fact] + public async Task CheckHealthAsync_AssertHealthStatusAfterChange_MultipleModules() + { + ManualHealthCheckTracker manualHealthCheckTracker = new ManualHealthCheckTracker(); + ManualHealthCheckService manualHealthCheckService = new ManualHealthCheckService(manualHealthCheckTracker); + HealthCheckContext context = new HealthCheckContext(); + var manualHealthCheck1 = new ManualHealthCheck(manualHealthCheckTracker); + var manualHealthCheck2 = new ManualHealthCheck(manualHealthCheckTracker); + + Assert.Equal(HealthStatus.Unhealthy, (await manualHealthCheckService.CheckHealthAsync(context, CancellationToken.None)).Status); + Assert.Equal("Initial state", manualHealthCheck1.Result.Description); + Assert.Equal("Initial state", manualHealthCheck2.Result.Description); + + // Both unhealthy + manualHealthCheck1.ReportUnhealthy("Test reason 1"); + manualHealthCheck2.ReportUnhealthy("Test reason 2"); + var healthCheckResult2Unhealthy = await manualHealthCheckService.CheckHealthAsync(context, CancellationToken.None); + Assert.Equal(HealthStatus.Unhealthy, healthCheckResult2Unhealthy.Status); + Assert.True(healthCheckResult2Unhealthy.Description!.Equals("Test reason 1, Test reason 2") + || healthCheckResult2Unhealthy.Description.Equals("Test reason 2, Test reason 1")); + + // One unhealthy + manualHealthCheck1.ReportHealthy(); + var healthCheckResult1Unhealthy = await manualHealthCheckService.CheckHealthAsync(context, CancellationToken.None); + Assert.Equal(HealthStatus.Unhealthy, healthCheckResult1Unhealthy.Status); + Assert.Equal("Test reason 2", healthCheckResult1Unhealthy.Description); + Assert.Null(manualHealthCheck1.Result.Description); + + // Both healthy + manualHealthCheck2.ReportHealthy(); + var healthCheckResultHealthy = await manualHealthCheckService.CheckHealthAsync(context, CancellationToken.None); + Assert.Equal(HealthStatus.Healthy, healthCheckResultHealthy.Status); + Assert.Null(healthCheckResultHealthy.Description); + Assert.Null(manualHealthCheck1.Result.Description); + Assert.Null(manualHealthCheck2.Result.Description); + + manualHealthCheck1.Dispose(); + manualHealthCheck2.Dispose(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests.csproj b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests.csproj new file mode 100644 index 0000000000..b59be01cd0 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests.csproj @@ -0,0 +1,17 @@ + + + Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests + Unit tests for Microsoft.Extensions.Diagnostics.HealthChecks.Core + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/MockHostApplicationLifetime.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/MockHostApplicationLifetime.cs new file mode 100644 index 0000000000..1dd58df8e2 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/MockHostApplicationLifetime.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests; + +internal class MockHostApplicationLifetime : IHostApplicationLifetime, IDisposable +{ + private readonly CancellationTokenSource _started = new(); + private readonly CancellationTokenSource _stopping = new(); + private readonly CancellationTokenSource _stopped = new(); + + public CancellationToken ApplicationStarted { get; } + + public CancellationToken ApplicationStopping { get; } + + public CancellationToken ApplicationStopped { get; } + + public MockHostApplicationLifetime() + { + ApplicationStarted = _started.Token; + ApplicationStopping = _stopping.Token; + ApplicationStopped = _stopped.Token; + } + + public void StartApplication() + { + _started.Cancel(); + } + + public void StoppingApplication() + { + _stopping.Cancel(); + } + + public void StopApplication() + { + _stopped.Cancel(); + } + + public void Dispose() + { + _started.Dispose(); + _stopping.Dispose(); + _stopped.Dispose(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/TelemetryHealthChecksPublisherExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/TelemetryHealthChecksPublisherExtensionsTest.cs new file mode 100644 index 0000000000..e2b07b3dd9 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/TelemetryHealthChecksPublisherExtensionsTest.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests; + +public class TelemetryHealthChecksPublisherExtensionsTest +{ + [Fact] + public void AddHealthCheckTelemetryTest() + { + var serviceCollection = new ServiceCollection(); + + serviceCollection + .AddLogging() + .AddSingleton() + .AddTelemetryHealthCheckPublisher(); + + using var serviceProvider = serviceCollection.BuildServiceProvider(); + var publishers = serviceProvider.GetRequiredService>(); + + Assert.Single(publishers); + foreach (var p in publishers) + { + Assert.True(p is TelemetryHealthCheckPublisher); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/TelemetryHealthChecksPublisherTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/TelemetryHealthChecksPublisherTest.cs new file mode 100644 index 0000000000..9e9272b7e6 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests/TelemetryHealthChecksPublisherTest.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Microsoft.Extensions.Telemetry.Testing.Metering; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Core.Tests; + +public class TelemetryHealthChecksPublisherTest +{ + private const string HealthReportMetricName = @"R9\HealthCheck\Report"; + private const string UnhealthyHealthCheckMetricName = @"R9\HealthCheck\UnhealthyHealthCheck"; + + public static TheoryData, string, LogLevel, string, string> PublishAsyncArgs => new() + { + { + new List { HealthStatus.Healthy }, + "Process reporting healthy: Healthy.", + LogLevel.Debug, + bool.TrueString, + HealthStatus.Healthy.ToString() + }, + { + new List { HealthStatus.Degraded }, + "Process reporting unhealthy: Degraded. Health check entries are id0: {status: Degraded, description: desc0}", + LogLevel.Warning, + bool.FalseString, + HealthStatus.Degraded.ToString() + }, + { + new List { HealthStatus.Unhealthy }, + "Process reporting unhealthy: Unhealthy. Health check entries are id0: {status: Unhealthy, description: desc0}", + LogLevel.Warning, + bool.FalseString, + HealthStatus.Unhealthy.ToString() + }, + { + new List { HealthStatus.Healthy, HealthStatus.Healthy }, + "Process reporting healthy: Healthy.", + LogLevel.Debug, + bool.TrueString, + HealthStatus.Healthy.ToString() + }, + { + new List { HealthStatus.Healthy, HealthStatus.Unhealthy }, + "Process reporting unhealthy: Unhealthy. Health check entries are id0: {status: Healthy, description: desc0}, id1: {status: Unhealthy, description: desc1}", + LogLevel.Warning, + bool.FalseString, + HealthStatus.Unhealthy.ToString() + }, + { + new List { HealthStatus.Healthy, HealthStatus.Degraded, HealthStatus.Unhealthy }, + "Process reporting unhealthy: Unhealthy. Health check entries are " + + "id0: {status: Healthy, description: desc0}, id1: {status: Degraded, description: desc1}, id2: {status: Unhealthy, description: desc2}", + LogLevel.Warning, + bool.FalseString, + HealthStatus.Unhealthy.ToString() + }, + }; + + [Theory] + [MemberData(nameof(PublishAsyncArgs))] + public async Task PublishAsync( + IList healthStatuses, + string expectedLogMessage, + LogLevel expectedLogLevel, + string expectedMetricHealthy, + string expectedMetricStatus) + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + var logger = new FakeLogger(); + var collector = logger.Collector; + + var publisher = new TelemetryHealthCheckPublisher(meter, logger); + + await publisher.PublishAsync(CreateHealthReport(healthStatuses), CancellationToken.None); + Assert.Equal(1, collector.Count); + Assert.Equal(expectedLogMessage, collector.LatestRecord.Message); + Assert.Equal(expectedLogLevel, collector.LatestRecord.Level); + + var latest = metricCollector.GetCounterValues(HealthReportMetricName)!.LatestWritten!; + + latest.Value.Should().Be(1); + latest.GetDimension("healthy").Should().Be(expectedMetricHealthy); + latest.GetDimension("status").Should().Be(expectedMetricStatus); + + var unhealthyCounters = metricCollector.GetCounterValues(UnhealthyHealthCheckMetricName)!.AllValues; + + for (int i = 0; i < healthStatuses.Count; i++) + { + var healthStatus = healthStatuses[i]; + if (healthStatus != HealthStatus.Healthy) + { + Assert.Equal(1, GetValue(unhealthyCounters, GetKey(i), healthStatuses[i].ToString())); + } + } + } + + private static long GetValue(IReadOnlyCollection> counters, string healthy, string status) + { + foreach (var counter in counters) + { + if (counter!.GetDimension("name")!.ToString() == healthy && + counter!.GetDimension("status")!.ToString() == status) + { + return counter.Value; + } + } + + return 0; + } + + private static HealthReport CreateHealthReport(IEnumerable healthStatuses) + { + var healthStatusRecords = new Dictionary(); + + int index = 0; + foreach (var status in healthStatuses) + { + var entry = new HealthReportEntry(status, $"desc{index}", TimeSpan.Zero, null, null); + healthStatusRecords.Add(GetKey(index), entry); + index++; + } + + return new HealthReport(healthStatusRecords, TimeSpan.Zero); + } + + private static string GetKey(int index) => $"id{index}"; +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests.csproj b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests.csproj new file mode 100644 index 0000000000..de6efb35c9 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests.csproj @@ -0,0 +1,16 @@ + + + Microsoft.Extensions.Diagnostics.HealthChecks.Tests + Unit tests for Microsoft.Extensions.HealthChecks.ResourceUtilization. + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTest.cs new file mode 100644 index 0000000000..618133aa41 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckExtensionsTest.cs @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Tests; + +public class ResourceHealthCheckExtensionsTest +{ + [Fact] + public async Task Extensions_AddResourceUtilizationHealthCheck() + { + var dataTracker = new Mock(); + var samplingWindow = TimeSpan.FromSeconds(1); + + var serviceCollection = new ServiceCollection(); + serviceCollection + .AddLogging() + .AddSingleton(dataTracker.Object) + .AddHealthChecks() + .AddResourceUtilizationHealthCheck(options => + options.SamplingWindow = samplingWindow); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + var service = serviceProvider.GetRequiredService(); + _ = await service.CheckHealthAsync(); + dataTracker.Verify(tracker => tracker.GetUtilization(samplingWindow), Times.Once); + } + + [Fact] + public async Task Extensions_AddResourceUtilizationHealthCheck_WithAction() + { + var dataTracker = new Mock(); + var samplingWindow = TimeSpan.FromSeconds(1); + + var serviceCollection = new ServiceCollection(); + serviceCollection + .AddLogging() + .AddSingleton(dataTracker.Object) + .AddHealthChecks() + .AddResourceUtilizationHealthCheck(o => + { + o.CpuThresholds = new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }; + o.SamplingWindow = samplingWindow; + }); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + var service = serviceProvider.GetRequiredService(); + _ = await service.CheckHealthAsync(); + dataTracker.Verify(tracker => tracker.GetUtilization(samplingWindow), Times.Once); + } + + [Fact] + public async Task Extensions_AddResourceUtilizationHealthCheck_WithActionAndTags() + { + var dataTracker = new Mock(); + var samplingWindow = TimeSpan.FromSeconds(1); + + var serviceCollection = new ServiceCollection(); + serviceCollection + .AddLogging() + .AddSingleton(dataTracker.Object) + .AddHealthChecks() + .AddResourceUtilizationHealthCheck(o => + { + o.CpuThresholds = new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }; + o.SamplingWindow = samplingWindow; + }, + new[] { "test" }); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + var service = serviceProvider.GetRequiredService(); + _ = await service.CheckHealthAsync(); + dataTracker.Verify(tracker => tracker.GetUtilization(samplingWindow), Times.Once); + } + + [Fact] + public async Task Extensions_AddResourceUtilizationHealthCheck_WithConfigurationSection() + { + var dataTracker = new Mock(); + + var samplingWindow = TimeSpan.FromSeconds(5); + var serviceCollection = new ServiceCollection(); + serviceCollection + .AddLogging() + .AddSingleton(dataTracker.Object) + .AddHealthChecks() + .AddResourceUtilizationHealthCheck(SetupResourceHealthCheckConfiguration("0.5", "0.7", "0.5", "0.7", "00:00:05").GetSection(nameof(ResourceUtilizationHealthCheckOptions))); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + var service = serviceProvider.GetRequiredService(); + _ = await service.CheckHealthAsync(); + dataTracker.Verify(tracker => tracker.GetUtilization(samplingWindow), Times.Once); + } + + [Fact] + public async Task Extensions_AddResourceUtilizationHealthCheck_WithConfigurationSectionAndTags() + { + var dataTracker = new Mock(); + + var samplingWindow = TimeSpan.FromSeconds(5); + var serviceCollection = new ServiceCollection(); + serviceCollection + .AddLogging() + .AddSingleton(dataTracker.Object) + .AddHealthChecks() + .AddResourceUtilizationHealthCheck( + SetupResourceHealthCheckConfiguration("0.5", "0.7", "0.5", "0.7", "00:00:05").GetSection(nameof(ResourceUtilizationHealthCheckOptions)), + new[] { "test" }); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + var service = serviceProvider.GetRequiredService(); + _ = await service.CheckHealthAsync(); + dataTracker.Verify(tracker => tracker.GetUtilization(samplingWindow), Times.Once); + } + + [Fact] + public void TestNullChecks() + { + Assert.Throws(() => ResourceUtilizationHealthChecksExtensions.AddResourceUtilizationHealthCheck(null!)); + Assert.Throws(() => ((IHealthChecksBuilder)null!).AddResourceUtilizationHealthCheck((IEnumerable)null!)); + Assert.Throws(() => ((IHealthChecksBuilder)null!).AddResourceUtilizationHealthCheck((Action)null!)); + Assert.Throws(() => ((IHealthChecksBuilder)null!).AddResourceUtilizationHealthCheck((IConfigurationSection)null!)); + } + + private static IConfiguration SetupResourceHealthCheckConfiguration( + string cpuDegradedThreshold, + string cpuUnhealthyThreshold, + string memoryDegradedThreshold, + string memoryUnhealthyThreshold, + string samplingWindow) + { + ResourceUtilizationHealthCheckOptions resourceHealthCheckOptions; + +#pragma warning disable S103 // Lines should not be too long + var configurationDict = new Dictionary + { + { + $"{nameof(ResourceUtilizationHealthCheckOptions)}:{nameof(resourceHealthCheckOptions.CpuThresholds)}:{nameof(resourceHealthCheckOptions.CpuThresholds.DegradedUtilizationPercentage)}", + cpuDegradedThreshold + }, + { + $"{nameof(ResourceUtilizationHealthCheckOptions)}:{nameof(resourceHealthCheckOptions.CpuThresholds)}:{nameof(resourceHealthCheckOptions.CpuThresholds.UnhealthyUtilizationPercentage)}", + cpuUnhealthyThreshold + }, + { + $"{nameof(ResourceUtilizationHealthCheckOptions)}:{nameof(resourceHealthCheckOptions.MemoryThresholds)}:{nameof(resourceHealthCheckOptions.MemoryThresholds.DegradedUtilizationPercentage)}", + memoryDegradedThreshold + }, + { + $"{nameof(ResourceUtilizationHealthCheckOptions)}:{nameof(resourceHealthCheckOptions.MemoryThresholds)}:{nameof(resourceHealthCheckOptions.MemoryThresholds.UnhealthyUtilizationPercentage)}", + memoryUnhealthyThreshold + }, + { + $"{nameof(ResourceUtilizationHealthCheckOptions)}:{nameof(resourceHealthCheckOptions.SamplingWindow)}", samplingWindow + } + }; +#pragma warning restore S103 // Lines should not be too long + + return new ConfigurationBuilder().AddInMemoryCollection(configurationDict).Build(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckTest.cs new file mode 100644 index 0000000000..0c848af9ac --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.ResourceUtilization.Tests/ResourceHealthCheckTest.cs @@ -0,0 +1,158 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks.Tests; + +public class ResourceHealthCheckTest +{ + public static IEnumerable Data => + new List + { + new object[] + { + HealthStatus.Healthy, + 0.1, + 0UL, + 1000UL, + null!, + "", + }, + new object[] + { + HealthStatus.Healthy, + 0.2, + 0UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.2 }, + "" + }, + new object[] + { + HealthStatus.Healthy, + 0.2, + 2UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.2 }, + "" + }, + new object[] + { + HealthStatus.Degraded, + 0.4, + 3UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, + " usage is close to the limit" + }, + new object[] + { + HealthStatus.Unhealthy, + 0.5, + 5UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2, UnhealthyUtilizationPercentage = 0.4 }, + " usage is above the limit" + }, + new object[] + { + HealthStatus.Unhealthy, + 0.5, + 5UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.4, UnhealthyUtilizationPercentage = 0.2 }, + " usage is above the limit" + }, + new object[] + { + HealthStatus.Degraded, + 0.3, + 3UL, + 1000UL, + new ResourceUsageThresholds { DegradedUtilizationPercentage = 0.2 }, + " usage is close to the limit" + }, + new object[] + { + HealthStatus.Unhealthy, + 0.5, + 5UL, + 1000UL, + new ResourceUsageThresholds { UnhealthyUtilizationPercentage = 0.4 }, + " usage is above the limit" + }, + }; + + [Theory] + [MemberData(nameof(Data))] +#pragma warning disable xUnit1026 // Theory methods should use all of their parameters + public async Task TestCpuChecks(HealthStatus expected, double utilization, ulong _, ulong totalMemory, ResourceUsageThresholds thresholds, string expectedDescription) +#pragma warning restore xUnit1026 // Theory methods should use all of their parameters + { + var systemResources = new SystemResources(1.0, 1.0, totalMemory, totalMemory); + var dataTracker = new Mock(); + var samplingWindow = TimeSpan.FromSeconds(1); + dataTracker + .Setup(tracker => tracker.GetUtilization(samplingWindow)) + .Returns(new Utilization(cpuUsedPercentage: utilization, memoryUsedInBytes: 0, systemResources)); + + var checkContext = new HealthCheckContext(); + var cpuCheckOptions = new ResourceUtilizationHealthCheckOptions + { + CpuThresholds = thresholds, + SamplingWindow = samplingWindow + }; + + var options = Microsoft.Extensions.Options.Options.Create(cpuCheckOptions); + var healthCheck = new ResourceUtilizationHealthCheck(options, dataTracker.Object); + var healthCheckResult = await healthCheck.CheckHealthAsync(checkContext); + Assert.Equal(expected, healthCheckResult.Status); + if (healthCheckResult.Status != HealthStatus.Healthy) + { + Assert.Equal("CPU" + expectedDescription, healthCheckResult.Description); + } + } + + [Theory] + [MemberData(nameof(Data))] +#pragma warning disable xUnit1026 // Theory methods should use all of their parameters + public async Task TestMemoryChecks(HealthStatus expected, double _, ulong memoryUsed, ulong totalMemory, ResourceUsageThresholds thresholds, string expectedDescription) +#pragma warning restore xUnit1026 // Theory methods should use all of their parameters + { + var systemResources = new SystemResources(1.0, 1.0, totalMemory, totalMemory); + var dataTracker = new Mock(); + var samplingWindow = TimeSpan.FromSeconds(1); + dataTracker + .Setup(tracker => tracker.GetUtilization(samplingWindow)) + .Returns(new Utilization(cpuUsedPercentage: 0, memoryUsedInBytes: memoryUsed, systemResources)); + + var checkContext = new HealthCheckContext(); + var memCheckOptions = new ResourceUtilizationHealthCheckOptions + { + MemoryThresholds = thresholds, + SamplingWindow = samplingWindow + }; + + var options = Microsoft.Extensions.Options.Options.Create(memCheckOptions); + var healthCheck = new ResourceUtilizationHealthCheck(options, dataTracker.Object); + var healthCheckResult = await healthCheck.CheckHealthAsync(checkContext); + Assert.Equal(expected, healthCheckResult.Status); + if (healthCheckResult.Status != HealthStatus.Healthy) + { + Assert.Equal("Memory" + expectedDescription, healthCheckResult.Description); + } + } + + [Fact] + public void TestNullChecks() + { + Assert.Throws(() => new ResourceUtilizationHealthCheck(Mock.Of>(), null!)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/Helpers/DummyProvider.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/Helpers/DummyProvider.cs new file mode 100644 index 0000000000..756147486f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/Helpers/DummyProvider.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Extensions.Time.Testing; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Helpers; + +internal class DummyProvider : ISnapshotProvider +{ + public static readonly double CpuUnits = 4; + public static readonly TimeSpan KernelTimeSinceStart = TimeSpan.FromTicks(1000); + public static readonly ulong MemoryTotalInBytes = 1024; + public static readonly ulong MemoryUsageInBytes = 512; + public static readonly FakeTimeProvider SnapshotTimeClock = new(); + public static readonly double TotalCoreLimitPercentage = 100.0; + public static readonly TimeSpan UserTimeSinceStart = TimeSpan.FromTicks(1000); + + public SystemResources Resources => new( + DummyProvider.CpuUnits, + DummyProvider.CpuUnits, + DummyProvider.MemoryTotalInBytes, + DummyProvider.MemoryTotalInBytes); + + public ResourceUtilizationSnapshot GetSnapshot() + { + return new ResourceUtilizationSnapshot( + totalTimeSinceStart: TimeSpan.FromTicks(SnapshotTimeClock.GetUtcNow().Ticks), + kernelTimeSinceStart: DummyProvider.KernelTimeSinceStart, + userTimeSinceStart: DummyProvider.UserTimeSinceStart, + memoryUsageInBytes: DummyProvider.MemoryUsageInBytes); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/Helpers/DummyTracker.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/Helpers/DummyTracker.cs new file mode 100644 index 0000000000..e77d6eb321 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/Helpers/DummyTracker.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Helpers; + +internal class DummyTracker : IResourceUtilizationTracker +{ + public const double CpuPercentage = 50.0; + public const double MemoryPercentage = 10.0; + public const ulong MemoryUsed = 100; + public const ulong MemoryTotal = 1000; + public const uint CpuUnits = 1; + + public Utilization GetUtilization(TimeSpan aggregationPeriod) => new(CpuPercentage, MemoryUsed, new SystemResources(CpuUnits, CpuUnits, MemoryTotal, MemoryTotal)); +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/IResourceUtilizationSnapshotProviderTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/IResourceUtilizationSnapshotProviderTest.cs new file mode 100644 index 0000000000..7c9dcec51f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/IResourceUtilizationSnapshotProviderTest.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Helpers; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test; + +public class IResourceUtilizationSnapshotProviderTest +{ + [Fact] + public void Resources_Gets_ValidSystemResources() + { + var provider = new DummyProvider(); + Assert.Equal(DummyProvider.CpuUnits, provider.Resources.GuaranteedCpuUnits); + Assert.Equal(DummyProvider.CpuUnits, provider.Resources.MaximumCpuUnits); + Assert.Equal(DummyProvider.MemoryTotalInBytes, provider.Resources.GuaranteedMemoryInBytes); + Assert.Equal(DummyProvider.MemoryTotalInBytes, provider.Resources.MaximumMemoryInBytes); + } + + [Fact] + public void GetSnapshot_Returns_ValidResourceUilizationSnapshot() + { + var snapshot = new DummyProvider().GetSnapshot(); + Assert.Equal(DummyProvider.KernelTimeSinceStart, snapshot.KernelTimeSinceStart); + Assert.Equal(DummyProvider.MemoryUsageInBytes, snapshot.MemoryUsageInBytes); + Assert.Equal(DummyProvider.SnapshotTimeClock.GetUtcNow().Ticks, snapshot.TotalTimeSinceStart.Ticks); + Assert.Equal(DummyProvider.UserTimeSinceStart, snapshot.UserTimeSinceStart); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/IResourceUtilizationTrackerTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/IResourceUtilizationTrackerTest.cs new file mode 100644 index 0000000000..b87ff4ea73 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/IResourceUtilizationTrackerTest.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Helpers; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test; + +public class IResourceUtilizationTrackerTest +{ + [Fact] + public void GetAverageUtilization_Gets_ValidUtilization() + { + var tracker = new DummyTracker(); + var utilization = tracker.GetUtilization(TimeSpan.Zero); + Assert.Equal(DummyTracker.CpuPercentage, utilization.CpuUsedPercentage); + Assert.Equal(DummyTracker.MemoryPercentage, utilization.MemoryUsedPercentage); + Assert.Equal(DummyTracker.MemoryUsed, utilization.MemoryUsedInBytes); + Assert.Equal(DummyTracker.MemoryTotal, utilization.SystemResources.GuaranteedMemoryInBytes); + Assert.Equal(DummyTracker.MemoryTotal, utilization.SystemResources.MaximumMemoryInBytes); + Assert.Equal(DummyTracker.CpuUnits, utilization.SystemResources.GuaranteedCpuUnits); + Assert.Equal(DummyTracker.CpuUnits, utilization.SystemResources.MaximumCpuUnits); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/NullResourceUtilizationTest/NullResourceUtilizationTrackerServiceTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/NullResourceUtilizationTest/NullResourceUtilizationTrackerServiceTest.cs new file mode 100644 index 0000000000..671af67ac5 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/NullResourceUtilizationTest/NullResourceUtilizationTrackerServiceTest.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.NullImplementationTest; + +public sealed class NullResourceUtilizationTrackerServiceTest +{ + private const double CpuUnits = 1.0; + private static readonly SystemResources _systemResources = new(CpuUnits, CpuUnits, long.MaxValue, long.MaxValue); + private static readonly Utilization _utilization = new(0.0, 0U, _systemResources); + + [Fact] + public void GetAverageUtilization_ReturnsFixedUtilizationValue() + { + var tracker = new NullResourceUtilizationTrackerService(); + + Assert.Equal(_utilization, tracker.GetUtilization(TimeSpan.Zero)); + Assert.Equal(_utilization, tracker.GetUtilization(TimeSpan.FromSeconds(1))); + Assert.Equal(_utilization, tracker.GetUtilization(TimeSpan.FromMinutes(1))); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/NullResourceUtilizationTest/NullSnapshotProviderTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/NullResourceUtilizationTest/NullSnapshotProviderTest.cs new file mode 100644 index 0000000000..e34c7955ef --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/NullResourceUtilizationTest/NullSnapshotProviderTest.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test; +public class NullSnapshotProviderTest +{ + private const double CpuUnits = 1.0; + private const ulong MemoryTotalInBytes = long.MaxValue; + private const ulong MemoryUsageInBytes = 0UL; + + private static readonly FakeTimeProvider _clock = new(); + + [Fact] + public void BasicConstructor_InitializesResourcesProperty() + { + var provider = new NullSnapshotProvider(); + + Assert.Equal(CpuUnits, provider.Resources.GuaranteedCpuUnits); + Assert.Equal(CpuUnits, provider.Resources.MaximumCpuUnits); + Assert.Equal(MemoryTotalInBytes, provider.Resources.GuaranteedMemoryInBytes); + Assert.Equal(MemoryTotalInBytes, provider.Resources.MaximumMemoryInBytes); + } + + [Fact] + public void GetSnapshot_GetsProperSnapshot() + { + var provider = new NullSnapshotProvider(_clock); + + var snapshot = provider.GetSnapshot(); + + Assert.Equal(TimeSpan.FromTicks(_clock.GetUtcNow().Ticks), snapshot.TotalTimeSinceStart); + Assert.Equal(TimeSpan.Zero, snapshot.KernelTimeSinceStart); + Assert.Equal(TimeSpan.Zero, snapshot.UserTimeSinceStart); + Assert.Equal(MemoryUsageInBytes, snapshot.MemoryUsageInBytes); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/ResouceUtilizationAbstractionsExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/ResouceUtilizationAbstractionsExtensionsTest.cs new file mode 100644 index 0000000000..9352410a51 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/ResouceUtilizationAbstractionsExtensionsTest.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test; +public sealed class ResouceUtilizationAbstractionsExtensionsTest +{ + [Fact] + public void AddNullResourceUtilizationProvider_AddsNullSnapshotProvider_ToServicesCollection() + { + var services = new ServiceCollection(); + + var builder = new Mock(MockBehavior.Loose); + builder.Setup(builder => builder.Services).Returns(services); + + using var servicesProvider = builder.Object.AddNullResourceUtilizationProvider() + .Services.BuildServiceProvider(); + + var snapshotProvider = servicesProvider.GetRequiredService(); + + Assert.NotNull(snapshotProvider); + Assert.IsType(snapshotProvider); + Assert.IsAssignableFrom(snapshotProvider); + } + + [Fact] + public void AddNullResourceUtilization_AddsNullResourceUtilizationTrackerService_ToServicesCollection() + { + var services = new ServiceCollection(); + + using var servicesProvider = services.AddNullResourceUtilization().BuildServiceProvider(); + + var tracker = servicesProvider.GetRequiredService(); + + Assert.NotNull(tracker); + Assert.IsType(tracker); + Assert.IsAssignableFrom(tracker); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/ResourceUtilizationSnapshotTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/ResourceUtilizationSnapshotTest.cs new file mode 100644 index 0000000000..aee7fde2da --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/ResourceUtilizationSnapshotTest.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test; + +public class ResourceUtilizationSnapshotTest +{ + [Fact] + public void BasicInitializaiton() + { + var time = new FakeTimeProvider(); + + // Constructor provided TimeSpan + var snapshot = new ResourceUtilizationSnapshot(TimeSpan.FromTicks(time.GetUtcNow().Ticks), TimeSpan.Zero, TimeSpan.FromSeconds(1), 10); + Assert.Equal(time.GetUtcNow().Ticks, snapshot.TotalTimeSinceStart.Ticks); + + // Constructor provided IClock + snapshot = new ResourceUtilizationSnapshot(time, TimeSpan.Zero, TimeSpan.FromSeconds(1), 10); + Assert.Equal(time.GetUtcNow().Ticks, snapshot.TotalTimeSinceStart.Ticks); + } + + [Fact] + public void Constructor_ProvidedWithNegativeValueOfKernelTimeSinceStart_ThrowsArgumentOutOfRangeException() + { + Assert.Throws(() + => new ResourceUtilizationSnapshot(new FakeTimeProvider(), TimeSpan.MinValue, TimeSpan.FromSeconds(1), 1000)); + } + + [Fact] + public void Constructor_ProvidedWithNegativeValueOfUserTimeSinceStart_ThrowsArgumentOutOfRangeException() + { + Assert.Throws(() + => new ResourceUtilizationSnapshot(new FakeTimeProvider(), TimeSpan.Zero, TimeSpan.MinValue, 1000)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/SystemResourcesTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/SystemResourcesTest.cs new file mode 100644 index 0000000000..625a7b233f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/SystemResourcesTest.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test; + +public class SystemResourcesTest +{ + [Fact] + public void BasicConstructor() + { + const double CpuUnits = 1.0; + const uint MemoryTotalInBytes = 1000U; + + var systemResources = new SystemResources(CpuUnits, CpuUnits, MemoryTotalInBytes, MemoryTotalInBytes); + + Assert.Equal(CpuUnits, systemResources.GuaranteedCpuUnits); + Assert.Equal(CpuUnits, systemResources.MaximumCpuUnits); + Assert.Equal(MemoryTotalInBytes, systemResources.GuaranteedMemoryInBytes); + Assert.Equal(MemoryTotalInBytes, systemResources.MaximumMemoryInBytes); + } + + [Fact] + public void Constructor_ProvidedInvalidParameters_Throws() + { + // Zero Cpu Units + Assert.Throws(() => new SystemResources(0.0, 1.0, 1000UL, 1000UL)); + + Assert.Throws(() => new SystemResources(1.0, 0.0, 1000UL, 1000UL)); + + // Zero Memory + Assert.Throws(() => new SystemResources(1.0, 1.0, 0UL, 1000UL)); + + Assert.Throws(() => new SystemResources(1.0, 1.0, 1000UL, 0UL)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/UtilizationTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/UtilizationTest.cs new file mode 100644 index 0000000000..79337d279a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Abstractions/UtilizationTest.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test; + +public class UtilizationTest +{ + private const double CpuPercentage = 50.0; + private const ulong MemoryUsed = 100; + private const ulong MemoryTotal = 1000; + private const double CpuUnits = 1.0; + private readonly SystemResources _systemResources = new(CpuUnits, CpuUnits, MemoryTotal, MemoryTotal); + + [Fact] + public void BasicConstructor() + { + var utilization = new Utilization(CpuPercentage, MemoryUsed, _systemResources); + Assert.Equal(CpuPercentage, utilization.CpuUsedPercentage); + Assert.Equal(MemoryUsed, utilization.MemoryUsedInBytes); + Assert.Equal(Math.Min(1.0, (double)MemoryUsed / MemoryTotal) * 100.0, utilization.MemoryUsedPercentage); + } + + [Fact] + public void Constructor_ProvidedNegativeCpuUtilization_ThrowsArgumentOutOfRangeException() + { + Assert.Throws(() => new Utilization(-50.0, 500, _systemResources)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/CalculatorTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/CalculatorTest.cs new file mode 100644 index 0000000000..bf0c705031 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/CalculatorTest.cs @@ -0,0 +1,196 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test; + +/// +/// Tests for the DataTracker utilization calculator. +/// +public sealed class CalculatorTest +{ + private const double CpuUnits = 1; + private const ulong TotalMemoryInBytes = 1000; + + private readonly ResourceUtilizationSnapshot _firstSnapshot = new( + totalTimeSinceStart: TimeSpan.FromTicks(new FakeTimeProvider().GetUtcNow().Ticks), + kernelTimeSinceStart: TimeSpan.FromTicks(0), + userTimeSinceStart: TimeSpan.FromTicks(0), + memoryUsageInBytes: 0); + private readonly SystemResources _resources = new(CpuUnits, CpuUnits, TotalMemoryInBytes, TotalMemoryInBytes); + + /// + /// Ensure that CPU stats work appropriately. + /// + [Fact] + public void BasicCalculation() + { + var secondSnapshotTimeSpan = _firstSnapshot.TotalTimeSinceStart.Add(TimeSpan.FromSeconds(5)); + + // Now, what's the total number of available ticks between the two samples (for a single core) + var totalAvailableTicks = secondSnapshotTimeSpan.Ticks - _firstSnapshot.TotalTimeSinceStart.Ticks; + + var second = new ResourceUtilizationSnapshot( + totalTimeSinceStart: secondSnapshotTimeSpan, + + // assign 25% to kernel time + kernelTimeSinceStart: TimeSpan.FromTicks(totalAvailableTicks / 4), + + // assign 25% to user time + userTimeSinceStart: TimeSpan.FromTicks(totalAvailableTicks / 4), + memoryUsageInBytes: 500); + + // Now, when we run the calculator, CPU should be at 50%. + var record = Calculator.CalculateUtilization(_firstSnapshot, second, _resources); + Assert.Equal(50.0, record.CpuUsedPercentage); + + // Because we set it basically, memory should also clearly be at 50%. + Assert.Equal(50.0, record.MemoryUsedPercentage); + + // Memory totals should reflect the second sample + Assert.Equal(500UL, record.MemoryUsedInBytes); + Assert.Equal(1000UL, record.SystemResources.GuaranteedMemoryInBytes); + } + + /// + /// Ensure that CPU stats work appropriately. + /// + [Fact] + public void BasicCalculation_WithHalfCpuUnits() + { + var limitedResources = new SystemResources(0.5, 0.5, TotalMemoryInBytes, TotalMemoryInBytes); + + var secondSnapshotTimeSpan = _firstSnapshot.TotalTimeSinceStart.Add(TimeSpan.FromSeconds(5)); + + // Now, what's the total number of available ticks between the two samples (for a single core) + var totalAvailableTicks = secondSnapshotTimeSpan.Ticks - _firstSnapshot.TotalTimeSinceStart.Ticks; + + var second = new ResourceUtilizationSnapshot( + totalTimeSinceStart: secondSnapshotTimeSpan, + + // assign 25% to kernel time + kernelTimeSinceStart: TimeSpan.FromTicks(totalAvailableTicks / 4), + + // assign 25% to user time + userTimeSinceStart: TimeSpan.FromTicks(totalAvailableTicks / 4), + memoryUsageInBytes: 500); + + // Using the limited resources, CPU time is now cut in half. So, when we run + // the calculator, the CPU utilization should be at 100%. + var record = Calculator.CalculateUtilization(_firstSnapshot, second, limitedResources); + Assert.Equal(100.0, record.CpuUsedPercentage); + } + + /// + /// Ensure that stats work appropriately at zero percent. + /// + [Fact] + public void Zeroes() + { + // No changes in the second snapshot + var secondSnapshot = new ResourceUtilizationSnapshot( + totalTimeSinceStart: _firstSnapshot.TotalTimeSinceStart.Add(TimeSpan.FromSeconds(5)), + memoryUsageInBytes: 0, + kernelTimeSinceStart: _firstSnapshot.KernelTimeSinceStart, + userTimeSinceStart: _firstSnapshot.UserTimeSinceStart); + + // Now, let's set each of kernel and user time to no time elapsed. + + // Now, when we run the calculator, CPU should be at 0%. + var record = Calculator.CalculateUtilization(_firstSnapshot, secondSnapshot, _resources); + Assert.Equal(0.0, record.CpuUsedPercentage); + + // Because we set it basically, memory should also clearly be at 0%. + Assert.Equal(0.0, record.MemoryUsedPercentage); + + // Memory totals should reflect the second sample + Assert.Equal(0UL, record.MemoryUsedInBytes); + Assert.Equal(1000UL, record.SystemResources.GuaranteedMemoryInBytes); + } + + /// + /// Ensure that stats work appropriately if time goes backwards. + /// + /// The lowest possible CPU percentage should be zero. + [Fact] + public void TimeGoesBackwards() + { + var firstSnapshot = new ResourceUtilizationSnapshot( + totalTimeSinceStart: TimeSpan.FromTicks(new FakeTimeProvider().GetUtcNow().Ticks), + kernelTimeSinceStart: TimeSpan.FromTicks(1000), + userTimeSinceStart: TimeSpan.FromTicks(1000), + memoryUsageInBytes: 0); + var secondSnapshot = new ResourceUtilizationSnapshot( + totalTimeSinceStart: firstSnapshot.TotalTimeSinceStart.Add(TimeSpan.FromSeconds(5)), + memoryUsageInBytes: 0, + kernelTimeSinceStart: TimeSpan.FromTicks(firstSnapshot.KernelTimeSinceStart.Ticks - 1), + userTimeSinceStart: TimeSpan.FromTicks(firstSnapshot.UserTimeSinceStart.Ticks - 1)); + + // Now, when we run the calculator, CPU should be at 0%. + var record = Calculator.CalculateUtilization(firstSnapshot, secondSnapshot, _resources); + Assert.Equal(0.0, record.CpuUsedPercentage); + } + + /// + /// Ensure that stats work appropriately if we seem to spend all the CPU time. + /// + /// The highest possible CPU percentage should be 100%. + [Fact] + public void FullyUtilized() + { + var secondSnapshotTimeSpan = _firstSnapshot.TotalTimeSinceStart.Add(TimeSpan.FromSeconds(5)); + + // Now, what's the total number of available ticks between the two samples. + var totalAvailableTicks = secondSnapshotTimeSpan.Ticks - _firstSnapshot.TotalTimeSinceStart.Ticks; + + var secondSnapshot = new ResourceUtilizationSnapshot( + totalTimeSinceStart: secondSnapshotTimeSpan, + kernelTimeSinceStart: TimeSpan.FromTicks(totalAvailableTicks / 2), + userTimeSinceStart: TimeSpan.FromTicks(totalAvailableTicks / 2), + memoryUsageInBytes: 1000); + + // Now, when we run the calculator, CPU should 100%. + var record = Calculator.CalculateUtilization(_firstSnapshot, secondSnapshot, _resources); + Assert.Equal(100.0, record.CpuUsedPercentage); + + // Assert that memory is at 100%. + Assert.Equal(100.0, record.MemoryUsedPercentage); + Assert.Equal(1000UL, record.MemoryUsedInBytes); + Assert.Equal(1000UL, record.SystemResources.GuaranteedMemoryInBytes); + } + + /// + /// Ensure that stats work appropriately if the resources are overutilized. + /// + /// The highest possible CPU and memory percentages should be 100%. + [Fact] + public void OverUtilized() + { + var secondSnapshotTimeSpan = _firstSnapshot.TotalTimeSinceStart.Add(TimeSpan.FromSeconds(5)); + + // Now, what's the total number of available ticks between the two samples. + var totalAvailableTicks = secondSnapshotTimeSpan.Ticks - _firstSnapshot.TotalTimeSinceStart.Ticks; + + var secondSnapshot = new ResourceUtilizationSnapshot( + totalTimeSinceStart: secondSnapshotTimeSpan, + + // Set each of kernel and uesr time to all the available ticks between + // the 2 snapshots to make them sum to 200% of the total CPU time. + kernelTimeSinceStart: TimeSpan.FromTicks(totalAvailableTicks), + userTimeSinceStart: TimeSpan.FromTicks(totalAvailableTicks), + memoryUsageInBytes: 1500); + + // Now, when we run the calculator, CPU should be at 100%. + var record = Calculator.CalculateUtilization(_firstSnapshot, secondSnapshot, _resources); + Assert.Equal(100.0, record.CpuUsedPercentage); + + // Assert that memory is at 100% + Assert.Equal(100.0, record.MemoryUsedPercentage); + Assert.Equal(1500UL, record.MemoryUsedInBytes); + Assert.Equal(1000UL, record.SystemResources.GuaranteedMemoryInBytes); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/CircularBufferTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/CircularBufferTest.cs new file mode 100644 index 0000000000..3696ffade9 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/CircularBufferTest.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test; + +public sealed class CircularBufferTest +{ + [Fact] + public void Constructor_InvalidSizePassed_Throws() + { + Assert.Throws(() => new CircularBuffer(0, 0)); + } + + [Fact] + public void GetFirstAndLastFromWindow_NoElements_DefaultsReturned() + { + const int DefaultElement = 5; + var buffer = new CircularBuffer(2, DefaultElement); + + var (firstElement, lastElement) = buffer.GetFirstAndLastFromWindow(2); + Assert.Equal(DefaultElement, firstElement); + Assert.Equal(DefaultElement, lastElement); + } + + [Fact] + public void GetFirstAndLastFromWindow_MoreElementsThanSizeAdded_RecentElementsReturned() + { + const int BufferSize = 5; + var elementsToAdd = Enumerable.Range(1, BufferSize * 3).ToList(); + var expectedBufferElements = elementsToAdd.Skip(Math.Max(0, elementsToAdd.Count - BufferSize)).ToList(); + + var buffer = new CircularBuffer(BufferSize, 0); + foreach (var element in elementsToAdd) + { + buffer.Add(element); + } + + var (firstElement, lastElement) = buffer.GetFirstAndLastFromWindow(BufferSize); + Assert.Equal(expectedBufferElements.First(), firstElement); + Assert.Equal(expectedBufferElements.Last(), lastElement); + } + + [Fact] + public void GetFirstAndLastFromWindow_ProvidedBufferSizeGreaterThanActualBufferLength() + { + const int BufferSize = 5; + var bufferElements = Enumerable.Range(1, BufferSize).ToList(); + var buffer = new CircularBuffer(BufferSize, 0); + + foreach (var element in bufferElements) + { + buffer.Add(element); + } + + var (firstElement, lastElement) = buffer.GetFirstAndLastFromWindow(BufferSize * 5); + Assert.Equal(bufferElements[0], firstElement); + Assert.Equal(bufferElements[4], lastElement); + } + + [Fact] + public void GetFirstAndLastFromWindow_ProvidedBufferSizeSmallerThanActualBufferLength() + { + const int BufferSize = 5; + const int RequestedBufferSize = 3; + + var bufferElements = Enumerable.Range(1, BufferSize).ToList(); + var buffer = new CircularBuffer(BufferSize, 0); + + foreach (var element in bufferElements) + { + buffer.Add(element); + } + + // Requesting 1st and 2nd items using a window of size 3, should returns + // the 3rd and 5th elements from the internal buffer. + var (firstElement, lastElement) = buffer.GetFirstAndLastFromWindow(RequestedBufferSize); + Assert.Equal(bufferElements[2], firstElement); + Assert.Equal(bufferElements[4], lastElement); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Providers/ConditionallyFaultProvider.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Providers/ConditionallyFaultProvider.cs new file mode 100644 index 0000000000..0d34c5a452 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Providers/ConditionallyFaultProvider.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Extensions.Time.Testing; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Providers; + +internal sealed class ConditionallyFaultProvider : ISnapshotProvider +{ + private const double CpuUnits = 1.0; + private const ulong TotalMemory = 1000UL; + private const ulong UsedMemory = 100; + + private readonly Guid _errorGuid; + + public bool CanThrow { get; set; } + + public ConditionallyFaultProvider(Guid errorGuid) + { + _errorGuid = errorGuid; + CanThrow = false; + } + + public SystemResources Resources => new(CpuUnits, CpuUnits, TotalMemory, TotalMemory); + + public ResourceUtilizationSnapshot GetSnapshot() + { + if (CanThrow) + { + throw new InvalidOperationException(_errorGuid.ToString()); + } + + return new ResourceUtilizationSnapshot( + TimeSpan.FromTicks(new FakeTimeProvider().GetUtcNow().Ticks), + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(1), + UsedMemory); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Providers/FakeProvider.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Providers/FakeProvider.cs new file mode 100644 index 0000000000..944cebd94a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Providers/FakeProvider.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Extensions.Time.Testing; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Providers; + +internal sealed class FakeProvider : ISnapshotProvider +{ + private ResourceUtilizationSnapshot _snapshot = new( + TimeSpan.FromTicks(new FakeTimeProvider().GetUtcNow().Ticks), + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(1), + 500); + + public SystemResources Resources => new(1.0, 1.0, 1000, 1000); + + public ResourceUtilizationSnapshot GetSnapshot() + { + return _snapshot; + } + + public void SetNextSnapshot(ResourceUtilizationSnapshot snapshot) + { + _snapshot = snapshot; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Providers/FaultProvider.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Providers/FaultProvider.cs new file mode 100644 index 0000000000..6037c44ca8 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Providers/FaultProvider.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Extensions.Time.Testing; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Providers; + +internal sealed class FaultProvider : ISnapshotProvider +{ + private readonly FakeTimeProvider _clock = new(); + + public bool ShouldThrow { get; set; } = true; + + public SystemResources Resources => new(1.0, 1.0, 1000, 1000); + + public ResourceUtilizationSnapshot GetSnapshot() + { + if (ShouldThrow) + { + throw new InvalidOperationException(); + } + + // return a dummy value. + return new ResourceUtilizationSnapshot(TimeSpan.FromTicks(_clock.GetUtcNow().Ticks), TimeSpan.Zero, TimeSpan.Zero, ulong.MaxValue); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Publishers/AnotherPublisher.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Publishers/AnotherPublisher.cs new file mode 100644 index 0000000000..9e9f22e505 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Publishers/AnotherPublisher.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Publishers; + +/// +/// Another publisher that do nothing, added to test scenarios where multiple publishers are added to the services collections. +/// +internal sealed class AnotherPublisher : IResourceUtilizationPublisher +{ + /// + public ValueTask PublishAsync(Utilization utilization, CancellationToken cancellationToken) + { + return default; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Publishers/EmptyPublisher.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Publishers/EmptyPublisher.cs new file mode 100644 index 0000000000..0a287a19eb --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Publishers/EmptyPublisher.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Publishers; + +/// +/// A publisher that do nothing. +/// +internal sealed class EmptyPublisher : IResourceUtilizationPublisher +{ + /// + public ValueTask PublishAsync(Utilization utilization, CancellationToken cancellationToken) + { + return default; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Publishers/FaultPublisher.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Publishers/FaultPublisher.cs new file mode 100644 index 0000000000..ee2ad48776 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Publishers/FaultPublisher.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Publishers; + +/// +/// A publisher that throws an error. +/// +internal sealed class FaultPublisher : IResourceUtilizationPublisher +{ + /// + public async ValueTask PublishAsync(Utilization utilization, CancellationToken cancellationToken) + { + await default(ValueTask); + throw new InvalidOperationException(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Publishers/GenericPublisher.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Publishers/GenericPublisher.cs new file mode 100644 index 0000000000..6ff6c0a539 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/Publishers/GenericPublisher.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Publishers; + +/// +/// A publisher that accept in its constructor. +/// +internal sealed class GenericPublisher : IResourceUtilizationPublisher +{ + private readonly Action _publish; + public GenericPublisher(Action publish) + { + _publish = publish; + } + + /// + public ValueTask PublishAsync(Utilization utilization, CancellationToken cancellationToken) + { + _publish(utilization); + return default; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationBuilderTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationBuilderTest.cs new file mode 100644 index 0000000000..71e8690bae --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationBuilderTest.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Publishers; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test; + +public sealed class ResourceUtilizationBuilderTest +{ + [Fact] + public void AddPublisher_CalledOnce_AddsSinglePublisherToServiceCollection() + { + using var provider = new ServiceCollection() + .AddResourceUtilization(configureTracker => + { + configureTracker.AddPublisher(); + }) + .BuildServiceProvider(); + + var publisher = provider.GetRequiredService(); + var publishersArray = provider.GetServices(); + + Assert.NotNull(publisher); + Assert.IsType(publisher); + Assert.NotNull(publishersArray); + Assert.Single(publishersArray); + Assert.IsAssignableFrom(publishersArray.First()); + } + + [Fact] + public void AddPublisher_CalledMultipleTimes_AddsMultiplePublishersToServiceCollection() + { + using var provider = new ServiceCollection() + .AddResourceUtilization(configureTracker => + { + configureTracker + .AddPublisher() + .AddPublisher(); + }) + .BuildServiceProvider(); + + var publishersArray = provider.GetServices(); + + Assert.NotNull(publishersArray); + Assert.Equal(2, publishersArray.Count()); + Assert.IsAssignableFrom(publishersArray.First()); + Assert.IsAssignableFrom(publishersArray.Last()); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerExtensionsTest.cs new file mode 100644 index 0000000000..06ff5632ee --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerExtensionsTest.cs @@ -0,0 +1,174 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Providers; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Publishers; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test; + +public sealed class ResourceUtilizationTrackerExtensionsTest +{ + [Fact] + public void AddResourceUtilization_AddsResourceUtilizationTrackerService_ToServicesCollection() + { + using var provider = new ServiceCollection() + .AddLogging() + .AddSingleton(TimeProvider.System) + .AddResourceUtilization(builder => + { + builder.Services.AddSingleton(); + builder.AddPublisher(); + }) + .BuildServiceProvider(); + + var trackerService = provider.GetRequiredService(); + + Assert.NotNull(trackerService); + Assert.IsType(trackerService); + Assert.IsAssignableFrom(trackerService); + } + + [Fact] + public void AddResourceUtilization_AddsResourceUtilizationTrackerService_AsHostedService() + { + using var provider = new ServiceCollection() + .AddLogging() + .AddSingleton(TimeProvider.System) + .AddResourceUtilization(builder => + { + builder.Services.AddSingleton(); + builder.AddPublisher(); + }) + .BuildServiceProvider(); + + var allHostedServices = provider.GetServices(); + var trackerService = allHostedServices.Single(s => s is ResourceUtilizationTrackerService); + + Assert.NotNull(trackerService); + Assert.IsType(trackerService); + Assert.IsAssignableFrom(trackerService); + } + + [Fact] + public void ConfigureResourceUtilization_InitializeTrackerProperly() + { + using var host = FakeHost.CreateBuilder() + .ConfigureResourceUtilization( + builder => + { + builder.Services.AddSingleton(TimeProvider.System); + builder.Services.AddSingleton(); + builder.AddPublisher(); + }) + .Build(); + + var tracker = host.Services.GetService(); + var options = host.Services.GetService>(); + var provider = host.Services.GetService(); + var publisher = host.Services.GetService(); + + Assert.NotNull(tracker); + Assert.NotNull(options); + Assert.NotNull(provider); + Assert.NotNull(publisher); + } + + [Fact] + public void ConfigureTracker_GivenOptionsDelegate_InitializeTrackerWithOptionsProperly() + { + const int SamplingWindowValue = 3; + const int CalculationPeriodValue = 2; + + using var host = FakeHost.CreateBuilder() + .ConfigureResourceUtilization( + builder => + { + builder.Services.AddSingleton(); + builder.AddPublisher(); + builder.ConfigureTracker(options => + { + options.CollectionWindow = TimeSpan.FromSeconds(SamplingWindowValue); + options.CalculationPeriod = TimeSpan.FromSeconds(CalculationPeriodValue); + }); + }) + .Build(); + + var options = host.Services.GetService>(); + + Assert.NotNull(options); + Assert.Equal(TimeSpan.FromSeconds(SamplingWindowValue), options!.Value.CollectionWindow); + Assert.Equal(TimeSpan.FromSeconds(CalculationPeriodValue), options!.Value.CalculationPeriod); + } + + [Fact] + public void ConfigureTracker_GivenIConfigurationSection_InitializeTrackerWithOptionsProperly() + { + const int SamplingWindowValue = 3; + const int CalculationPeriod = 2; + const int SamplingPeriodValue = 1; + + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [$"{nameof(ResourceUtilizationTrackerOptions)}:{nameof(ResourceUtilizationTrackerOptions.CollectionWindow)}"] + = TimeSpan.FromSeconds(SamplingWindowValue).ToString(), + [$"{nameof(ResourceUtilizationTrackerOptions)}:{nameof(ResourceUtilizationTrackerOptions.SamplingInterval)}"] + = TimeSpan.FromSeconds(SamplingPeriodValue).ToString(), + [$"{nameof(ResourceUtilizationTrackerOptions)}:{nameof(ResourceUtilizationTrackerOptions.CalculationPeriod)}"] + = TimeSpan.FromSeconds(CalculationPeriod).ToString() + }) + .Build(); + + var configurationSection = config + .GetSection(nameof(ResourceUtilizationTrackerOptions)); + + using var host = FakeHost.CreateBuilder() + .ConfigureResourceUtilization( + builder => + { + builder.Services.AddSingleton(); + builder.AddPublisher(); + builder.ConfigureTracker(configurationSection); + }) + .Build(); + + var options = host.Services.GetService>(); + + Assert.NotNull(options); + Assert.Equal(TimeSpan.FromSeconds(SamplingWindowValue), options!.Value.CollectionWindow); + Assert.Equal(TimeSpan.FromSeconds(SamplingPeriodValue), options!.Value.SamplingInterval); + Assert.Equal(TimeSpan.FromSeconds(CalculationPeriod), options!.Value.CalculationPeriod); + } + + [Fact] + public void Registering_Resource_Utilization_Adds_Only_One_Object_Of_Type_ResourceUtilizationService_To_DI_Container() + { + using var host = FakeHost.CreateBuilder() + .ConfigureResourceUtilization( + builder => + { + builder.Services.AddSingleton(); + builder.AddPublisher(); + }) + .Build(); + + var trackers = host.Services.GetServices().ToArray(); + var backgrounds = host.Services.GetServices().Where(x => x is ResourceUtilizationTrackerService).ToArray(); + + var tracker = Assert.Single(trackers); + var background = Assert.Single(backgrounds); + Assert.IsAssignableFrom(tracker); + Assert.IsAssignableFrom(background); + Assert.Same(tracker as ResourceUtilizationTrackerService, background as ResourceUtilizationTrackerService); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerOptionsManualValidatorTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerOptionsManualValidatorTest.cs new file mode 100644 index 0000000000..b4164a4159 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerOptionsManualValidatorTest.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +#if NETCOREAPP3_1_OR_GREATER +using System.Linq; +#endif +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test; + +public sealed class ResourceUtilizationTrackerOptionsManualValidatorTest +{ + [Theory] + [InlineData(6, 5)] + [InlineData(6, 6)] + public void Validator_GivenValidOptions_Succeeds(int collectionWindow, int calculationPeriod) + { + var options = new ResourceUtilizationTrackerOptions + { + CollectionWindow = TimeSpan.FromSeconds(collectionWindow), + CalculationPeriod = TimeSpan.FromSeconds(calculationPeriod) + }; + + var validator = new ResourceUtilizationTrackerOptionsManualValidator(); + var isValid = validator.Validate(nameof(options), options).Succeeded; + Assert.True(isValid); + } + + [Fact] + public void Validator_CalculationPeriodBiggerThanCollectionWindow_Fails() + { + var options = new ResourceUtilizationTrackerOptions + { + CollectionWindow = TimeSpan.FromSeconds(1), + CalculationPeriod = TimeSpan.FromSeconds(2) + }; + + var validator = new ResourceUtilizationTrackerOptionsManualValidator(); + var validationResult = validator.Validate(nameof(options), options); + + Assert.True(validationResult.Failed); + +#if NETCOREAPP3_1_OR_GREATER + var failureMessage = validationResult.Failures.Single(); +#else + var failureMessage = validationResult.FailureMessage; +#endif + Assert.Equal("Property CalculationPeriod: Value must be <= to CollectionWindow (00:00:01), but is 00:00:02.", failureMessage); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerOptionsTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerOptionsTest.cs new file mode 100644 index 0000000000..9a1347185f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerOptionsTest.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test; + +public sealed class ResourceUtilizationTrackerOptionsTest +{ + [Fact] + public void Basic() + { + var options = new ResourceUtilizationTrackerOptions + { + CollectionWindow = TimeSpan.FromMilliseconds(100), + SamplingInterval = TimeSpan.FromMilliseconds(10), + CalculationPeriod = TimeSpan.FromSeconds(50) + }; + + Assert.NotNull(options); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerOptionsValidatorTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerOptionsValidatorTest.cs new file mode 100644 index 0000000000..9ba5c151a8 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerOptionsValidatorTest.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test; + +public sealed class ResourceUtilizationTrackerOptionsValidatorTest +{ + [Fact] + public void Validator_GivenValidOptions_Succeeds() + { + var options = new ResourceUtilizationTrackerOptions + { + CollectionWindow = TimeSpan.FromMilliseconds(100), + SamplingInterval = TimeSpan.FromMilliseconds(10), + CalculationPeriod = TimeSpan.FromSeconds(200) + }; + + var validator = new ResourceUtilizationTrackerOptionsValidator(); + + var isValid = validator.Validate(nameof(options), options).Succeeded; + + Assert.True(isValid); + } + + [Fact] + public void Validator_GivenOptionsWithInvalidSamplingWindow_Fails() + { + var options = new ResourceUtilizationTrackerOptions + { + CollectionWindow = TimeSpan.FromTicks(1), + SamplingInterval = TimeSpan.FromSeconds(1), + CalculationPeriod = TimeSpan.FromSeconds(200) + }; + + var validator = new ResourceUtilizationTrackerOptionsValidator(); + + Assert.True(validator.Validate(nameof(options), options).Failed); + } + + [Fact] + public void Validator_GivenOptionsWithInvalidSamplingPeriod_Fails() + { + var options = new ResourceUtilizationTrackerOptions + { + CollectionWindow = TimeSpan.FromMilliseconds(100), + SamplingInterval = TimeSpan.FromMilliseconds(0), + CalculationPeriod = TimeSpan.FromSeconds(200) + }; + + var validator = new ResourceUtilizationTrackerOptionsValidator(); + + Assert.True(validator.Validate(nameof(options), options).Failed); + } + + [Fact] + public void Validator_GivenOptionsWithInvalidMinimalRetentionPeriod_Fails() + { + var options = new ResourceUtilizationTrackerOptions + { + CollectionWindow = TimeSpan.FromMilliseconds(100), + SamplingInterval = TimeSpan.FromMilliseconds(1), + CalculationPeriod = TimeSpan.FromSeconds(-5) + }; + + var validator = new ResourceUtilizationTrackerOptionsValidator(); + + Assert.True(validator.Validate(nameof(options), options).Failed); + } + + [Theory] + [InlineData(-100, 1, ResourceUtilizationTrackerOptions.MinimumSamplingWindow, true)] + [InlineData(-1, 1, ResourceUtilizationTrackerOptions.MinimumSamplingWindow, true)] + [InlineData(0, -100, ResourceUtilizationTrackerOptions.MinimumSamplingWindow, true)] + [InlineData(0, -1, ResourceUtilizationTrackerOptions.MinimumSamplingWindow, true)] + [InlineData(0, 0, ResourceUtilizationTrackerOptions.MinimumSamplingWindow, true)] + [InlineData(0, ResourceUtilizationTrackerOptions.MinimumSamplingPeriod + 1, ResourceUtilizationTrackerOptions.MinimumSamplingWindow, true)] + [InlineData(0, 1, ResourceUtilizationTrackerOptions.MinimumSamplingWindow, true)] + [InlineData(1, 1, ResourceUtilizationTrackerOptions.MinimumSamplingWindow, true)] + [InlineData(ResourceUtilizationTrackerOptions.MinimumSamplingWindow, 1, ResourceUtilizationTrackerOptions.MinimumSamplingWindow, false)] + [InlineData(ResourceUtilizationTrackerOptions.MinimumSamplingWindow, 100, ResourceUtilizationTrackerOptions.MinimumSamplingWindow, false)] + [InlineData( + ResourceUtilizationTrackerOptions.MaximumSamplingWindow - 1, + ResourceUtilizationTrackerOptions.MaximumSamplingPeriod - 1, + ResourceUtilizationTrackerOptions.MaximumSamplingPeriod - 1, + false)] + [InlineData( + ResourceUtilizationTrackerOptions.MaximumSamplingWindow, + ResourceUtilizationTrackerOptions.MaximumSamplingPeriod, + ResourceUtilizationTrackerOptions.MaximumSamplingPeriod, + false)] + [InlineData( + ResourceUtilizationTrackerOptions.MinimumSamplingWindow, + ResourceUtilizationTrackerOptions.MinimumSamplingPeriod, + -1, + true)] + [InlineData( + ResourceUtilizationTrackerOptions.MinimumSamplingWindow, + ResourceUtilizationTrackerOptions.MinimumSamplingPeriod, + ResourceUtilizationTrackerOptions.MaximumSamplingWindow + 1, + true)] + public void Validator_With_Multiple_Options_Scenarios(int samplingWindow, int samplingPeriod, int calculationPeriod, bool isError) + { + var options = new ResourceUtilizationTrackerOptions + { + CollectionWindow = TimeSpan.FromMilliseconds(samplingWindow), + SamplingInterval = TimeSpan.FromMilliseconds(samplingPeriod), + CalculationPeriod = TimeSpan.FromMilliseconds(calculationPeriod) + }; + + var validator = new ResourceUtilizationTrackerOptionsValidator(); + + Assert.Equal(isError, validator.Validate(nameof(options), options).Failed); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerServiceTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerServiceTest.cs new file mode 100644 index 0000000000..b181e79622 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Core/ResourceUtilizationTrackerServiceTest.cs @@ -0,0 +1,715 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Providers; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test.Publishers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Microsoft.Extensions.Time.Testing; +using Moq; +using Xunit; +using static Microsoft.Extensions.Options.Options; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test; + +/// +/// Tests for the DataTracker class. +/// +public sealed class ResourceUtilizationTrackerServiceTest +{ + private const string ProviderUnableToGatherData = "Unable to gather utilization statistics."; + + // We use this static fake clock in tests that doesn't advance the time. + private static readonly FakeTimeProvider _clock = new(); + private static readonly string _publisherUnableToPublishErrorMessage = $"Publisher `{typeof(FaultPublisher).FullName}` was unable to publish utilization statistics."; + + /// + /// Simply construct the object. + /// + /// Tests that look into internals like this are evil. Consider removing long term. + [Fact] + public void BasicConstructor() + { + var mockProvider = new Mock(MockBehavior.Loose); + var mockLogger = new Mock>(MockBehavior.Loose); + var publishersList = new List + { + new EmptyPublisher(), + new AnotherPublisher() + }; + + using var tracker = new ResourceUtilizationTrackerService( + mockProvider.Object, + mockLogger.Object, + Create(new ResourceUtilizationTrackerOptions()), + publishersList, + _clock); + var provider = GetDataTrackerField(tracker, "_provider"); + var logger = GetDataTrackerField>(tracker, "_logger"); + var publishers = + GetDataTrackerField(tracker, "_publishers"); + + Assert.NotNull(provider); + Assert.NotNull(logger); + Assert.NotNull(publishers); + Assert.Equal(2, publishers?.Length); + } + + [Fact] + public void BasicConstructor_NullOptions_Throws() + { + var mockProvider = new Mock(MockBehavior.Loose); + var mockLogger = new Mock>(MockBehavior.Loose); + var mockPublishers = new Mock>(MockBehavior.Loose); + + Assert.Throws(() => + new ResourceUtilizationTrackerService(mockProvider.Object, mockLogger.Object, Create((ResourceUtilizationTrackerOptions)null!), mockPublishers.Object, _clock)); + } + + /// + /// Simply construct the object (publisher constructor). + /// + /// Tests that look into internals like this are evil. Consider removing long term. + [Fact] + public void BasicConstructor_NullPublishers_Throws() + { + var mockProvider = new Mock(MockBehavior.Loose); + var mockLogger = new Mock>(MockBehavior.Loose); + + Assert.Throws(() => + new ResourceUtilizationTrackerService(mockProvider.Object, mockLogger.Object, Create(new ResourceUtilizationTrackerOptions()), null!, TimeProvider.System)); + } + + /// + /// Construct the object configured with maximum allowed values of and + /// . + /// + [Fact] + public void BasicConstructor_ConfiguredWithMaxValuesOfSamplingWindowAndSamplingPeriod_DoesNotThrow() + { + var mockProvider = new Mock(MockBehavior.Loose); + var mockLogger = new Mock>(MockBehavior.Loose); + var publishersList = new List + { + new EmptyPublisher(), + new AnotherPublisher() + }; + + var exception = Record.Exception(() => + { + using var tracker = new ResourceUtilizationTrackerService( + mockProvider.Object, + mockLogger.Object, + Create(new ResourceUtilizationTrackerOptions + { + CollectionWindow = TimeSpan.FromMilliseconds(int.MaxValue), + SamplingInterval = TimeSpan.FromMilliseconds(int.MaxValue) + }), + publishersList, + _clock); + }); + + Assert.Null(exception); + } + + /// + /// Simply construct the object (publisher constructor). + /// + /// Tests that look into internals like this are evil. Consider removing long term. + [Fact] + public void BasicConstructor_WithTwoPublishers() + { + var mockProvider = new Mock(MockBehavior.Loose); + var mockLogger = new Mock>(MockBehavior.Loose); + var publishersList = new List + { + new EmptyPublisher(), + new EmptyPublisher() + }; + + using var tracker = new ResourceUtilizationTrackerService( + mockProvider.Object, + mockLogger.Object, + Create(new ResourceUtilizationTrackerOptions()), + publishersList, + _clock); + Assert.NotNull(tracker); + + var provider = GetDataTrackerField(tracker, "_provider"); + var publishers + = GetDataTrackerField(tracker, "_publishers"); + var logger = GetDataTrackerField(tracker, "_logger"); + + Assert.NotNull(logger); + Assert.NotNull(provider); + Assert.NotNull(logger); + Assert.NotNull(publishers); + Assert.IsType(publishers); + Assert.Equal(2, publishers?.Length); + } + + /// + /// Simply construct the object (complex constructor, with counter publishing). + /// + /// Tests that look into internals like this are evil. Consider removing long term. + [Fact] + public void BasicConstructor_Complex_WithEmptyPublisher() + { + var mockProvider = new Mock(MockBehavior.Loose); + var mockLogger = new Mock>(MockBehavior.Loose); + using var tracker = new ResourceUtilizationTrackerService(mockProvider.Object, mockLogger.Object, + Create(new ResourceUtilizationTrackerOptions + { + CollectionWindow = TimeSpan.FromSeconds(1), + CalculationPeriod = TimeSpan.FromSeconds(1) + }), + new List + { + new EmptyPublisher() + }, + _clock); + + var publishers = GetDataTrackerField(tracker, "_publishers"); + + Assert.NotNull(publishers); + Assert.IsType(publishers); + Assert.Equal(1, publishers?.Length); + } + + [Fact] + public async Task StartAsync_WithSimulatingThatTimeDidNotPass_NoUtilizationDataWillBeGathered() + { + const int TimerPeriod = 100; + var numberOfSnapshots = 0; + var logger = new FakeLogger(); + var provider = new FakeProvider(); + + using var tracker = new ResourceUtilizationTrackerService( + provider, + logger, + + // Here we set the number of options to 1 so the internal timer + // period will equal the AverageWindow. + Create(new ResourceUtilizationTrackerOptions + { + CollectionWindow = TimeSpan.FromMilliseconds(TimerPeriod), + SamplingInterval = TimeSpan.FromMilliseconds(TimerPeriod), + CalculationPeriod = TimeSpan.FromMilliseconds(TimerPeriod) + }), + new List + { + new GenericPublisher(_ => numberOfSnapshots++) + }, + _clock); + + // Start running the tracker. + _ = tracker.StartAsync(CancellationToken.None); + + // waiting for 3 cycles of execution, however the tracker's timer won't + // be triggered as we didn't advance time via the fake clock. + await Task.Delay(TimeSpan.FromMilliseconds(TimerPeriod * 3)); + + await tracker.StopAsync(CancellationToken.None); + + // Since we did not advance the clock, then time did not pass. So, the timer should remain idle. + Assert.Equal(0, numberOfSnapshots); + } + + [Fact] + public async Task RunTrackerAsync_IfProviderThrows_LogsError() + { + var clock = new FakeTimeProvider(); + var logger = new FakeLogger(); + var provider = new FaultProvider + { + // prevent the provider from throwing exception just to not fail the test + // while initializing the tracker. + ShouldThrow = false + }; + using var e = new ManualResetEventSlim(); + + using var tracker = new ResourceUtilizationTrackerService( + provider, + logger, + Create(new ResourceUtilizationTrackerOptions + { + CollectionWindow = TimeSpan.FromMilliseconds(100), + CalculationPeriod = TimeSpan.FromMilliseconds(100), + SamplingInterval = TimeSpan.FromMilliseconds(1) + }), + new List + { + new GenericPublisher((_) => e.Set()) + }, + clock); + + await tracker.StartAsync(CancellationToken.None); + + // Now, allow the faulted provider to throw. + provider.ShouldThrow = true; + + clock.Advance(TimeSpan.FromMilliseconds(1)); + clock.Advance(TimeSpan.FromMilliseconds(1)); + + e.Wait(); + + Assert.Contains(ProviderUnableToGatherData, logger.Collector.LatestRecord.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task RunTrackerAsync_IfPublisherThrows_LogsError() + { + var logger = new FakeLogger(); + + using var tracker = new ResourceUtilizationTrackerService( + new FakeProvider(), + logger, + Create(new ResourceUtilizationTrackerOptions + { + CollectionWindow = TimeSpan.FromMilliseconds(100), + CalculationPeriod = TimeSpan.FromMilliseconds(100) + }), + new List + { + // initialize with a publisher that throws an exception when trying to publish. + new FaultPublisher() + }, + _clock); + + // running the tracker logic for a single time without starting the tracker + // service itself. By this we avoid starting the timer and the async behavior + // and thus eliminate the flakiness introduced because of it. + await tracker.PublishUtilizationAsync(CancellationToken.None); + + Assert.Contains(_publisherUnableToPublishErrorMessage, logger.Collector.LatestRecord.Message, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Validate that the tracker invokes the publisher's Publish method. + /// + /// Tests that look into internals like this are evil. Consider removing long term. + [Fact] + public async Task ResourceUtilizationTracker_InitializedProperly_InvokesPublishers() + { + const int TimerPeriod = 100; + bool publisherCalled = false; + + // Here we use local clock to keep its state local to the test. + var clock = new FakeTimeProvider(); + + using var autoResetEvent = new AutoResetEvent(false); + + using var tracker = new ResourceUtilizationTrackerService( + new FakeProvider(), + new NullLogger(), + + // Here we set the number of options to 1 so the internal timer + // period will equal the AverageWindow. + Create(new ResourceUtilizationTrackerOptions + { + CollectionWindow = TimeSpan.FromMilliseconds(TimerPeriod), + CalculationPeriod = TimeSpan.FromMilliseconds(TimerPeriod), + SamplingInterval = TimeSpan.FromMilliseconds(TimerPeriod) + }), + new List + { + new GenericPublisher(_ => + { + publisherCalled = true; + autoResetEvent.Set(); + }) + }, + clock); + + // Start running the tracker. + await tracker.StartAsync(CancellationToken.None); + + do + { + // Advance the clock 100 milliseconds to simulate passing of time + // and allow the tracker to execute one cycle of gathering utilization data. + clock.Advance(TimeSpan.FromMilliseconds(TimerPeriod)); + } + while (!autoResetEvent.WaitOne(1)); + + await tracker.StopAsync(CancellationToken.None); + + // Asserts that the publisher was called. + Assert.True(publisherCalled); + } + + [Fact(Skip = "Broken test, see https://github.com/dotnet/r9/issues/404")] + public async Task ResourceUtilizationTracker_WhenInitializedWithZeroSnapshots_ReportsHighCpuSpikesThenConvergeInFewCycles() + { + // This test shows that initializing the internal buffer of the tracker with snapshots + // with zeros for the kernel time and user time will cause the first values of CPU + // utilization to be very high in a way that don't reflect the change in CPU time. + + const int TimerPeriod = 100; + + var clock = new FakeTimeProvider(); + var zerosSnapshot = new ResourceUtilizationSnapshot( + TimeSpan.FromTicks(clock.GetUtcNow().Ticks), + TimeSpan.Zero, + TimeSpan.Zero, + 0); + + // This array simulates a series of snapshot values where the CPU kernel time + // and CPU user time are constant and don't change, with this series of snapshots + // the tracker should emit 0% CPU all the time, however, because the tracker + // internal buffer was initialized with zero values snapshot, we will find that + // the produced CPU% will start with high values then with time it will converge + // to 0%. + var snapshotsSequence = new[] + { + new ResourceUtilizationSnapshot( + TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod).Ticks), + TimeSpan.FromMilliseconds(250), + TimeSpan.FromMilliseconds(250), + 200), + new ResourceUtilizationSnapshot( + TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod * 2).Ticks), + TimeSpan.FromMilliseconds(250), + TimeSpan.FromMilliseconds(250), + 200), + new ResourceUtilizationSnapshot( + TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod * 3).Ticks), + TimeSpan.FromMilliseconds(250), + TimeSpan.FromMilliseconds(250), + 200), + new ResourceUtilizationSnapshot( + TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod * 4).Ticks), + TimeSpan.FromMilliseconds(250), + TimeSpan.FromMilliseconds(250), + 200), + new ResourceUtilizationSnapshot( + TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod * 5).Ticks), + TimeSpan.FromMilliseconds(250), + TimeSpan.FromMilliseconds(250), + 200) + }; + + using var autoResetEvent = new AutoResetEvent(false); + var provider = new FakeProvider(); + + // Initially set the provider to return zero snapshot, and use this snapshot + // to initialize the tracker's internal buffer with zero snapshots. + provider.SetNextSnapshot(zerosSnapshot); + + var options = new ResourceUtilizationTrackerOptions + { + CollectionWindow = TimeSpan.FromMilliseconds(TimerPeriod * 2), + CalculationPeriod = TimeSpan.FromMilliseconds(TimerPeriod * 2), + SamplingInterval = TimeSpan.FromMilliseconds(TimerPeriod) + }; + + using var tracker = new ResourceUtilizationTrackerService( + provider, + new NullLogger(), + Create(options), + new List + { + new GenericPublisher(_ => + { + autoResetEvent.Set(); + }), + }, + clock); + + // Start running the tracker. + _ = tracker.StartAsync(CancellationToken.None); + + var cpuValuesWithHighSpikes = new int[5]; + + // In the following cycles the CPU% will be reported with high + // values (100%) then should reduce with each cycle until reach + // the value of 0% + for (int i = 0; i < snapshotsSequence.Length; i++) + { + provider.SetNextSnapshot(snapshotsSequence[i]); + + // Advance time. + clock.Advance(TimeSpan.FromMilliseconds(TimerPeriod)); + + // This is to allow the service code to execute until it gets blocked in Task.Delay, and then + // make sure it gets unblocked from the Delay. + while (!autoResetEvent.WaitOne(10)) + { + clock.Advance(TimeSpan.FromTicks(1)); + } + + var utilization = tracker.GetUtilization(options.CollectionWindow); + cpuValuesWithHighSpikes[i] = (int)utilization.CpuUsedPercentage; + } + + // Stop the tracker. + await tracker.StopAsync(CancellationToken.None); + + Assert.Equal(0, cpuValuesWithHighSpikes[cpuValuesWithHighSpikes.Length - 1]); + } + + [Fact] + public async Task ResourceUtilizationTracker_WhenInitializedWithProperSnapshots_ReportsNoHighCpuSpikes() + { + // This test shows that initializing the internal buffer of the tracker with snapshots + // with normal values for the kernel time and user time will eliminate any high CPU + // utilization spikes that may appear in the first readings from the tracker. + + const int TimerPeriod = 100; + + var clock = new FakeTimeProvider(); + var properInitSnapshot = new ResourceUtilizationSnapshot( + TimeSpan.FromTicks(clock.GetUtcNow().Ticks), + TimeSpan.FromMilliseconds(250), + TimeSpan.FromMilliseconds(250), + 200); + + // This array simulates a series of snapshot values where the CPU kernel time + // and CPU user time are constant and don't change, with this series of snapshots + // the tracker should emit 0% CPU all the time. Since in this test the tracker + // is initialized with proper values, the tracker will produce 0% CPU all the + // time. + var snapshotsSequence = new[] + { + new ResourceUtilizationSnapshot( + TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod).Ticks), + TimeSpan.FromMilliseconds(250), + TimeSpan.FromMilliseconds(250), + 200), + new ResourceUtilizationSnapshot( + TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod * 2).Ticks), + TimeSpan.FromMilliseconds(250), + TimeSpan.FromMilliseconds(250), + 200), + new ResourceUtilizationSnapshot( + TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod * 3).Ticks), + TimeSpan.FromMilliseconds(250), + TimeSpan.FromMilliseconds(250), + 200), + new ResourceUtilizationSnapshot( + TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod * 4).Ticks), + TimeSpan.FromMilliseconds(250), + TimeSpan.FromMilliseconds(250), + 200), + new ResourceUtilizationSnapshot( + TimeSpan.FromTicks(clock.GetUtcNow().AddMilliseconds(TimerPeriod * 5).Ticks), + TimeSpan.FromMilliseconds(250), + TimeSpan.FromMilliseconds(250), + 200) + }; + + // These are the expected CPU% values. + var expectedCpuValues = new[] { 0, 0, 0, 0, 0 }; + + using var autoResetEvent = new AutoResetEvent(false); + var provider = new FakeProvider(); + + // Initially set the provider to return zero snapshot, and use this snapshot + // to initialize the tracker's internal buffer with zero snapshots. + provider.SetNextSnapshot(properInitSnapshot); + + var options = new ResourceUtilizationTrackerOptions + { + CollectionWindow = TimeSpan.FromMilliseconds(TimerPeriod * 2), + CalculationPeriod = TimeSpan.FromMilliseconds(TimerPeriod * 2), + SamplingInterval = TimeSpan.FromMilliseconds(TimerPeriod) + }; + + using var tracker = new ResourceUtilizationTrackerService( + provider, + new NullLogger(), + Create(options), + new List + { + new GenericPublisher(_ => + { + autoResetEvent.Set(); + }), + }, + clock); + + // Start running the tracker. + _ = tracker.StartAsync(CancellationToken.None); + + var cpuValuesWithNoHighSpikes = new int[5]; + + // In the following cycles the CPU% will be 0% all the time. + for (int i = 0; i < snapshotsSequence.Length; i++) + { + provider.SetNextSnapshot(snapshotsSequence[i]); + + do + { + // Advance time. + clock.Advance(TimeSpan.FromMilliseconds(TimerPeriod)); + } + while (!autoResetEvent.WaitOne(1)); + + var utilization = tracker.GetUtilization(options.CollectionWindow); + cpuValuesWithNoHighSpikes[i] = (int)utilization.CpuUsedPercentage; + } + + // Stop the tracker. + await tracker.StopAsync(CancellationToken.None); + + Assert.Equal(expectedCpuValues, cpuValuesWithNoHighSpikes); + } + + [Fact] + public void Dispose_CalledMultipleTimes_DoNotThrow() + { + using var tracker = new ResourceUtilizationTrackerService( + new FakeProvider(), + new NullLogger(), + Create(new ResourceUtilizationTrackerOptions()), + new List + { + new EmptyPublisher() + }, + _clock); + + var exception = Record.Exception(() => + { + tracker.Dispose(); + tracker.Dispose(); + }); + + Assert.Null(exception); + } + + [Fact] + public async Task StopAsync_CalledTwice_DoesNotThrow() + { + using var tracker = new ResourceUtilizationTrackerService( + new FakeProvider(), + new NullLogger(), + Create(new ResourceUtilizationTrackerOptions()), + new List + { + new EmptyPublisher() + }, + _clock); + + _ = tracker.StartAsync(CancellationToken.None); + + var exception = await Record.ExceptionAsync(async () => + { + await tracker.StopAsync(CancellationToken.None); + await tracker.StopAsync(CancellationToken.None); + }); + + Assert.Null(exception); + } + + [Fact] + public void GetUtilization_BasicTest() + { + var providerMock = new Mock(MockBehavior.Loose); + + providerMock.Setup(x => x.Resources) + .Returns(new SystemResources(1.0, 1.0, 100, 100)); + providerMock.Setup(x => x.GetSnapshot()) + .Returns(new ResourceUtilizationSnapshot( + TimeSpan.FromTicks(_clock.GetUtcNow().Ticks), + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(1), + 50)); + + using var tracker = new ResourceUtilizationTrackerService( + providerMock.Object, + new NullLogger(), + Create(new ResourceUtilizationTrackerOptions + { + CollectionWindow = TimeSpan.FromSeconds(1), + SamplingInterval = TimeSpan.FromMilliseconds(100), + CalculationPeriod = TimeSpan.FromSeconds(1) + }), + new List + { + new EmptyPublisher() + }, + _clock); + + var exception = Record.Exception(() => _ = tracker.GetUtilization(TimeSpan.FromSeconds(1))); + + Assert.Null(exception); + } + + [Fact] + public void GetUtilization_ProvidedByWindowGreaterThanSamplingWindowButLesserThanCollectionWindow_Successes() + { + var providerMock = new Mock(MockBehavior.Loose); + + using var tracker = new ResourceUtilizationTrackerService( + providerMock.Object, + new NullLogger(), + Create(new ResourceUtilizationTrackerOptions + { + CollectionWindow = TimeSpan.FromSeconds(6), + CalculationPeriod = TimeSpan.FromSeconds(5) + }), + new List + { + new EmptyPublisher(), + }, + _clock); + + var exception = Record.Exception(() => tracker.GetUtilization(TimeSpan.FromSeconds(6))); + + Assert.Null(exception); + } + + [Fact] + public void GetUtilization_ProvidedByWindowGreaterThanBuffer_ThrowsArgumentOutOfRangeException() + { + var providerMock = new Mock(MockBehavior.Loose); + + using var tracker = new ResourceUtilizationTrackerService( + providerMock.Object, + new NullLogger(), + Create(new ResourceUtilizationTrackerOptions + { + CollectionWindow = TimeSpan.FromSeconds(5) + }), + new List + { + new EmptyPublisher(), + }, + _clock); + + var exception = Record.Exception(() => tracker.GetUtilization(TimeSpan.FromSeconds(6))); + + Assert.NotNull(exception); + Assert.IsType(exception); + } + + [Fact] + public async Task Disposing_Service_Twice_Does_Not_Throw() + { + using var s = new ResourceUtilizationTrackerService(new FakeProvider(), NullLogger.Instance, + Microsoft.Extensions.Options.Options.Create(new ResourceUtilizationTrackerOptions()), Array.Empty(), TimeProvider.System); + + var r = await Record.ExceptionAsync(async () => + { + await s.StopAsync(CancellationToken.None); + await s.StopAsync(CancellationToken.None); + await s.StopAsync(CancellationToken.None); + }); + + Assert.Null(r); + } + + private static T? GetDataTrackerField(ResourceUtilizationTrackerService tracker, string name) + { + var typ = typeof(ResourceUtilizationTrackerService); + var type = typ.GetField(name, BindingFlags.NonPublic | BindingFlags.Instance); + return (T?)type?.GetValue(tracker); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTestResourceUtilizationLinux.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTestResourceUtilizationLinux.cs new file mode 100644 index 0000000000..a3d954de20 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/AcceptanceTestResourceUtilizationLinux.cs @@ -0,0 +1,241 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Time.Testing; +using Microsoft.TestUtilities; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; + +public sealed class AcceptanceTestResourceUtilizationLinux +{ + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific package.")] + public void Adding_Linux_Resource_Utilization_Allows_To_Query_Snapshot_Provider() + { + using var services = new ServiceCollection() + .AddResourceUtilization(x => x.AddLinuxProvider()) + .BuildServiceProvider(); + + var provider = services.GetRequiredService(); + + Assert.NotEqual(default, provider.Resources); + Assert.NotEqual(default, provider.GetSnapshot()); + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Want to see if it throws on windows.")] + public async Task Adding_Linux_Resource_Utilization_On_Windows_Throws() + { + var e = await Record.ExceptionAsync(async () => + { + var h = FakeHost.CreateBuilder().ConfigureServices((_, s) => s.AddResourceUtilization(x => x.AddLinuxProvider()) + .AddSingleton(new FakeUserHz(100))) + .Build(); + + await h.StartAsync(); + await h.StopAsync(); + }); + + Assert.IsAssignableFrom(e); + } + + [Fact] + [SuppressMessage("Minor Code Smell", "S3257:Declarations and initializations should be as concise as possible", Justification = "Broken analyzer.")] + public void Adding_Linux_Resource_Utilization_Can_Be_Configured_With_Section() + { + var cpuRefresh = TimeSpan.FromMinutes(13); + var memoryRefresh = TimeSpan.FromMinutes(14); + + var config = new KeyValuePair[] + { + new($"{nameof(LinuxResourceUtilizationProviderOptions)}:{nameof(LinuxResourceUtilizationProviderOptions.CpuConsumptionRefreshInterval)}", cpuRefresh.ToString()), + new($"{nameof(LinuxResourceUtilizationProviderOptions)}:{nameof(LinuxResourceUtilizationProviderOptions.MemoryConsumptionRefreshInterval)}", memoryRefresh.ToString()), + }; + + var section = new ConfigurationBuilder() + .AddInMemoryCollection(config) + .Build() + .GetSection(nameof(LinuxResourceUtilizationProviderOptions)); + + using var services = new ServiceCollection() + .AddSingleton(new FakeOperatingSystem(isLinux: true)) + .AddResourceUtilization(x => x.AddLinuxProvider(section)) + .BuildServiceProvider(); + + var options = services.GetRequiredService>(); + + Assert.NotNull(options.Value); + Assert.Equal(cpuRefresh, options.Value.CpuConsumptionRefreshInterval); + Assert.Equal(memoryRefresh, options.Value.MemoryConsumptionRefreshInterval); + } + + [Fact] + public void Adding_Linux_Resource_Utilization_Can_Be_Configured_With_Action() + { + var cpuRefresh = TimeSpan.FromMinutes(13); + var memoryRefresh = TimeSpan.FromMinutes(14); + + using var services = new ServiceCollection() + .AddSingleton(new FakeOperatingSystem(isLinux: true)) + .AddResourceUtilization(x => x.AddLinuxProvider(options => + { + options.CpuConsumptionRefreshInterval = cpuRefresh; + options.MemoryConsumptionRefreshInterval = memoryRefresh; + })) + .BuildServiceProvider(); + + var options = services.GetRequiredService>(); + + Assert.NotNull(options.Value); + Assert.Equal(cpuRefresh, options.Value.CpuConsumptionRefreshInterval); + Assert.Equal(memoryRefresh, options.Value.MemoryConsumptionRefreshInterval); + } + + [Fact] + [SuppressMessage("Minor Code Smell", "S3257:Declarations and initializations should be as concise as possible", Justification = "Broken analyzer.")] + public void Adding_Linux_Resource_Utilization_With_Section_Registers_SnapshotProvider() + { + var cpuRefresh = TimeSpan.FromMinutes(13); + var memoryRefresh = TimeSpan.FromMinutes(14); + + var config = new KeyValuePair[] + { + new($"{nameof(LinuxResourceUtilizationProviderOptions)}:{nameof(LinuxResourceUtilizationProviderOptions.CpuConsumptionRefreshInterval)}", cpuRefresh.ToString()), + new($"{nameof(LinuxResourceUtilizationProviderOptions)}:{nameof(LinuxResourceUtilizationProviderOptions.MemoryConsumptionRefreshInterval)}", memoryRefresh.ToString()), + }; + + var section = new ConfigurationBuilder() + .AddInMemoryCollection(config) + .Build() + .GetSection(nameof(LinuxResourceUtilizationProviderOptions)); + + using var services = new ServiceCollection() + .AddSingleton(new FakeOperatingSystem(isLinux: true)) + .AddSingleton(new FakeUserHz(100)) + .AddSingleton(new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/memory/memory.limit_in_bytes"), "100000" }, + { new FileInfo("/proc/stat"), "cpu 10 10 10 10 10 10 10 10 10 10"}, + { new FileInfo("/sys/fs/cgroup/cpuacct/cpuacct.usage"), "102312"}, + { new FileInfo("/proc/meminfo"), "MemTotal: 102312 kB"}, + { new FileInfo("/sys/fs/cgroup/cpuset/cpuset.cpus"), "0-19"}, + { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"), "12"}, + { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), "6"}, + })) + .AddResourceUtilization(x => x.AddLinuxProvider(section)) + .BuildServiceProvider(); + + var provider = services.GetService(); + + Assert.NotNull(provider); + Assert.Equal(1, provider.Resources.GuaranteedCpuUnits); // hack to make hardcoded calculation in resource utilization main package work. + Assert.Equal(20.0d, provider.Resources.MaximumCpuUnits); // read from cpuset.cpus + Assert.Equal(100_000UL, provider.Resources.GuaranteedMemoryInBytes); // read from memory.limit_in_bytes + Assert.Equal(104_767_488UL, provider.Resources.MaximumMemoryInBytes); // meminfo * 1024 + } + + [Fact(Skip = "Flaky test, see https://github.com/dotnet/r9/issues/406")] + [SuppressMessage("Minor Code Smell", "S3257:Declarations and initializations should be as concise as possible", Justification = "Broken analyzer.")] + public Task ResourceUtilizationTracker_Reports_The_Same_Values_As_One_Can_Observe_From_Gauges() + { + var cpuRefresh = TimeSpan.FromMinutes(13); + var memoryRefresh = TimeSpan.FromMinutes(14); + var fileSystem = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/memory/memory.limit_in_bytes"), "100000" }, + { new FileInfo("/sys/fs/cgroup/memory/memory.usage_in_bytes"), "450000" }, + { new FileInfo("/proc/stat"), "cpu 10 10 10 10 10 10 10 10 10 10"}, + { new FileInfo("/sys/fs/cgroup/cpuacct/cpuacct.usage"), "102312"}, + { new FileInfo("/proc/meminfo"), "MemTotal: 102312 kB"}, + { new FileInfo("/sys/fs/cgroup/cpuset/cpuset.cpus"), "0-19"}, + { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"), "12"}, + { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), "6"}, + { new FileInfo("/sys/fs/cgroup/memory/memory.stat"), "total_inactive_file 100"}, + }); + + using var listener = new MeterListener(); + var clock = new FakeTimeProvider(DateTimeOffset.UtcNow); + var cpuFromGauge = 0.0d; + var memoryFromGauge = 0.0d; + using var e = new ManualResetEventSlim(); + + listener.InstrumentPublished = (i, m) => + { + if (i.Name == LinuxResourceUtilizationCounters.CpuConsumptionPercentage + || i.Name == LinuxResourceUtilizationCounters.MemoryConsumptionPercentage) + { + m.EnableMeasurementEvents(i); + } + }; + + listener.SetMeasurementEventCallback((m, f, _, _) => + { + if (m.Name == LinuxResourceUtilizationCounters.CpuConsumptionPercentage) + { + cpuFromGauge = f; + } + else if (m.Name == LinuxResourceUtilizationCounters.MemoryConsumptionPercentage) + { + memoryFromGauge = f; + } + }); + + listener.Start(); + + using var host = FakeHost.CreateBuilder().ConfigureServices(x => + x.AddLogging() + .AddSingleton(clock) + .AddSingleton(new FakeOperatingSystem(isLinux: true)) + .AddSingleton(new FakeUserHz(100)) + .AddSingleton(fileSystem) + .AddSingleton(new GenericPublisher(_ => e.Set())) + .AddResourceUtilization(x => x.AddLinuxProvider())) + .Build(); + + var tracker = host.Services.GetService(); + Assert.NotNull(tracker); + + _ = host.RunAsync(); + + listener.RecordObservableInstruments(); + + var utilization = tracker.GetUtilization(TimeSpan.FromSeconds(5)); + + Assert.Equal(double.NaN, utilization.CpuUsedPercentage); + Assert.Equal(100, utilization.MemoryUsedPercentage); + Assert.Equal(utilization.CpuUsedPercentage, cpuFromGauge); + Assert.Equal(utilization.MemoryUsedPercentage, memoryFromGauge); + + fileSystem.ReplaceFileContent(new FileInfo("/sys/fs/cgroup/memory/memory.usage_in_bytes"), "50100"); + fileSystem.ReplaceFileContent(new FileInfo("/proc/stat"), "cpu 11 10 10 10 10 10 10 10 10 10"); + fileSystem.ReplaceFileContent(new FileInfo("/sys/fs/cgroup/cpuacct/cpuacct.usage"), "112312"); + + clock.Advance(TimeSpan.FromSeconds(6)); + listener.RecordObservableInstruments(); + + e.Wait(); + + utilization = tracker.GetUtilization(TimeSpan.FromSeconds(5)); + + Assert.Equal(1, utilization.CpuUsedPercentage); + Assert.Equal(utilization.CpuUsedPercentage, cpuFromGauge); + Assert.Equal(50, utilization.MemoryUsedPercentage); + Assert.Equal(utilization.MemoryUsedPercentage, memoryFromGauge); + + return Task.CompletedTask; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTest.cs new file mode 100644 index 0000000000..d90801c418 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxCountersTest.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.IO; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Extensions.Telemetry.Metering; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; + +public sealed class LinuxCountersTest +{ + [Fact] + public void LinuxCounters_Registers_Instruments() + { + var meterName = Guid.NewGuid().ToString(); + var options = Microsoft.Extensions.Options.Options.Create(new()); + using var meter = new Meter(); + var fileSystem = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/memory/memory.limit_in_bytes"), "9223372036854771712" }, + { new FileInfo("/proc/stat"), "cpu 10 10 10 10 10 10 10 10 10 10"}, + { new FileInfo("/sys/fs/cgroup/cpuacct/cpuacct.usage"), "50"}, + { new FileInfo("/proc/meminfo"), "MemTotal: 1024 kB"}, + { new FileInfo("/sys/fs/cgroup/cpuset/cpuset.cpus"), "0-19"}, + { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"), "60"}, + { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), "6"}, + { new FileInfo("/sys/fs/cgroup/memory/memory.stat"), "total_inactive_file 0"}, + { new FileInfo("/sys/fs/cgroup/memory/memory.usage_in_bytes"), "524288"}, + }); + var parser = new LinuxUtilizationParser(fileSystem: fileSystem, new FakeUserHz(100)); + var provider = new LinuxUtilizationProvider(options, parser, meter, new FakeOperatingSystem(isLinux: true), TimeProvider.System); + + using var listener = new MeterListener + { + InstrumentPublished = (instrument, listener) => + { + listener.EnableMeasurementEvents(instrument); + } + }; + + var samples = new List<(Instrument instrument, double value)>(); + listener.SetMeasurementEventCallback((instrument, value, _, _) => + { + if (ReferenceEquals(meter, instrument.Meter)) + { + samples.Add((instrument, value)); + } + }); + + listener.Start(); + listener.RecordObservableInstruments(); + + Assert.Equal(2, samples.Count); + Assert.Equal(LinuxResourceUtilizationCounters.CpuConsumptionPercentage, samples[0].instrument.Name); + Assert.Equal(double.NaN, samples[0].value); + Assert.Equal(LinuxResourceUtilizationCounters.MemoryConsumptionPercentage, samples[1].instrument.Name); + Assert.Equal(50, samples[1].value); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationExtensionsTest.cs new file mode 100644 index 0000000000..c284fece76 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationExtensionsTest.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.TestUtilities; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; + +public sealed class LinuxUtilizationExtensionsTest +{ + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Linux specific package")] + public void Throw_Null_When_Registration_Ingredients_Null() + { + var services = new ServiceCollection(); + + Assert.Throws(() => ((IResourceUtilizationTrackerBuilder)null!).AddLinuxProvider()); + Assert.Throws(() => ((IResourceUtilizationTrackerBuilder)null!).AddLinuxProvider((_) => { })); + Assert.Throws(() => ((IResourceUtilizationTrackerBuilder)null!).AddLinuxProvider((IConfigurationSection)null!)); + Assert.Throws(() => services.AddResourceUtilization((b) => b.AddLinuxProvider((IConfigurationSection)null!))); + Assert.Throws(() => services.AddResourceUtilization((b) => b.AddLinuxProvider((Action)null!))); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserTest.cs new file mode 100644 index 0000000000..6cae82393d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationParserTest.cs @@ -0,0 +1,323 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; +using Microsoft.TestUtilities; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; + +[OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Windows specific.")] +public sealed class LinuxUtilizationParserTest +{ + [ConditionalTheory] + [InlineData("DFIJEUWGHFWGBWEFWOMDOWKSLA")] + [InlineData("")] + [InlineData("________________________Asdasdasdas dd")] + [InlineData(" ")] + [InlineData("!@#!$%!@")] + public void Parser_Throws_When_Data_Is_Invalid(string line) + { + var parser = new LinuxUtilizationParser(new HardcodedValueFileSystem(line), new FakeUserHz(100)); + + Assert.Throws(() => parser.GetHostAvailableMemory()); + Assert.Throws(() => parser.GetAvailableMemoryInBytes()); + Assert.Throws(() => parser.GetMemoryUsageInBytes()); + Assert.Throws(() => parser.GetCgroupLimitedCpus()); + Assert.Throws(() => parser.GetHostCpuUsageInNanoseconds()); + Assert.Throws(() => parser.GetHostCpuCount()); + Assert.Throws(() => parser.GetCgroupCpuUsageInNanoseconds()); + } + + [ConditionalFact] + public void Parser_Can_Read_Host_And_Cgroup_Available_Cpu_Count() + { + var parser = new LinuxUtilizationParser(new FileNamesOnlyFileSystem(TestResources.TestFilesLocation), new FakeUserHz(100)); + var hostCpuCount = parser.GetHostCpuCount(); + var cgroupCpuCount = parser.GetCgroupLimitedCpus(); + + Assert.Equal(2.0, hostCpuCount); + Assert.Equal(1.0, cgroupCpuCount); + } + + [ConditionalFact] + public void Parser_Provides_Total_Available_Memory_In_Bytes() + { + var fs = new FileNamesOnlyFileSystem(TestResources.TestFilesLocation); + var parser = new LinuxUtilizationParser(fs, new FakeUserHz(100)); + + var totalMem = parser.GetHostAvailableMemory(); + + Assert.Equal(16_233_760UL * 1024, totalMem); + } + + [ConditionalTheory] + [InlineData("----------------------")] + [InlineData("@ @#dddada")] + [InlineData("1231234124124")] + [InlineData("1024 KB")] + [InlineData("1024 KB d \n\r 1024")] + [InlineData("\n\r")] + [InlineData("")] + [InlineData("Suspicious")] + [InlineData("string@")] + [InlineData("string12312")] + [InlineData("total-inactive-file")] + [InlineData("total_inactive-file")] + [InlineData("total_active_file")] + [InlineData("total_inactive_file:_ 213912")] + [InlineData("Total_Inactive_File 2")] + public void When_Calling_GetMemoryUsageInBytes_Parser_Throws_When_MemoryStat_Doesnt_Contain_Total_Inactive_File_Section(string content) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/memory/memory.stat"), content } + }); + + var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetMemoryUsageInBytes()); + + Assert.IsAssignableFrom(r); + Assert.Contains("/sys/fs/cgroup/memory/memory.stat", r.Message); + Assert.Contains("total_inactive_file", r.Message); + } + + [ConditionalTheory] + [InlineData("----------------------")] + [InlineData("@ @#dddada")] + [InlineData("_1231234124124")] + [InlineData("eee 1024 KB")] + [InlineData("\n\r")] + [InlineData("")] + [InlineData("Suspicious")] + [InlineData("Suspicious12312312")] + [InlineData("string@")] + [InlineData("string12312")] + public void When_Calling_GetMemoryUsageInBytes_Parser_Throws_When_UsageInBytes_Doesnt_Contain_Just_A_Number(string content) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/memory/memory.stat"), "total_inactive_file 0" }, + { new FileInfo("/sys/fs/cgroup/memory/memory.usage_in_bytes"), content } + }); + + var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetMemoryUsageInBytes()); + + Assert.IsAssignableFrom(r); + Assert.Contains("/sys/fs/cgroup/memory/memory.usage_in_bytes", r.Message); + } + + [ConditionalTheory] + [InlineData(10, 1)] + [InlineData(23, 22)] + [InlineData(100000, 10000)] + public void When_Calling_GetMemoryUsageInBytes_Parser_Throws_When_Inactive_Memory_Is_Bigger_Than_Total_Memory(int inactive, int total) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/memory/memory.stat"), $"total_inactive_file {inactive}" }, + { new FileInfo("/sys/fs/cgroup/memory/memory.usage_in_bytes"), total.ToString(CultureInfo.CurrentCulture) } + }); + + var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetMemoryUsageInBytes()); + + Assert.IsAssignableFrom(r); + Assert.Contains("lesser than", r.Message); + } + + [ConditionalTheory] + [InlineData("Mem")] + [InlineData("MemTotal:")] + [InlineData("MemTotal: 120")] + [InlineData("MemTotal: kb")] + [InlineData("MemTotal: KB")] + [InlineData("MemTotal: PB")] + [InlineData("MemTotal: 1024 PB")] + [InlineData("MemTotal: 1024 ")] + [InlineData("MemTotal: 1024 @@ ")] + [InlineData("MemoryTotal: 1024 MB ")] + [InlineData("MemoryTotal: 123123123123123123")] + public void When_Calling_GetHostAvailableMemory_Parser_Throws_When_MemInfo_Does_Not_Contain_TotalMemory(string totalMemory) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/proc/meminfo"), totalMemory }, + }); + + var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetHostAvailableMemory()); + + Assert.IsAssignableFrom(r); + Assert.Contains("/proc/meminfo", r.Message); + } + + [ConditionalTheory] + [InlineData("kB", 231, 236544)] + [InlineData("MB", 287, 300_941_312)] + [InlineData("GB", 372, 399_431_958_528)] + [InlineData("TB", 2, 219_902_325_555_2)] + [SuppressMessage("Critical Code Smell", "S3937:Number patterns should be regular", Justification = "Its OK.")] + public void When_Calling_GetHostAvailableMemory_Parser_Correctly_Transforms_Supported_Units_To_Bytes(string unit, int value, ulong bytes) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/proc/meminfo"), $"MemTotal: {value} {unit}" }, + }); + + var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var memory = p.GetHostAvailableMemory(); + + Assert.Equal(bytes, memory); + } + + [ConditionalFact] + public void When_No_Cgroup_Cpu_Limits_Are_Not_Set_We_Get_Available_Cpus_From_CpuSetCpus() + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/cpuset/cpuset.cpus"), $"0-11" }, + { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"), "-1" }, + { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), "-1" } + }); + + var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var cpus = p.GetCgroupLimitedCpus(); + + Assert.Equal(12, cpus); + } + + [ConditionalTheory] + [InlineData("-11")] + [InlineData("0-")] + [InlineData("d-22")] + [InlineData("22-d")] + [InlineData("22-18")] + [InlineData("aaaa")] + [InlineData(" d 182-1923")] + [InlineData("")] + [InlineData("1-18-22")] + [InlineData("1-18 \r\n")] + [InlineData("\r\n")] + public void Parser_Throws_When_CpuSet_Has_Invalid_Content(string content) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/cpuset/cpuset.cpus"), content }, + { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"), "12" }, + { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), "-1" } + }); + + var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetCgroupLimitedCpus()); + + Assert.IsAssignableFrom(r); + Assert.Contains("/sys/fs/cgroup/cpuset/cpuset.cpus", r.Message); + } + + [ConditionalTheory] + [InlineData("-1", "18")] + [InlineData("18", "-1")] + [InlineData("18", "")] + [InlineData("", "18")] + public void When_Quota_And_Period_Are_Minus_One_It_Fallbacks_To_Cpuset(string quota, string period) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/cpuset/cpuset.cpus"), "@" }, + { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"), quota }, + { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), period } + }); + + var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetCgroupLimitedCpus()); + + Assert.IsAssignableFrom(r); + Assert.Contains("/sys/fs/cgroup/cpuset/cpuset.cpus", r.Message); + } + + [Theory] + [InlineData("dd1d", "18")] + [InlineData("-18", "18")] + [InlineData("\r\r\r\r\r", "18")] + [InlineData("123", "\r\r\r\r\r")] + [InlineData("-", "d'")] + [InlineData("-", "d/:")] + [InlineData("2", "d/:")] + [InlineData("2d2d2d", "e3")] + [InlineData("3d", "d3")] + [InlineData(" 12", "eeeee 12")] + [InlineData("1 2", "eeeee 12")] + [InlineData("12 ", "")] + public void Parser_Throws_When_Cgroup_Cpu_Files_Contain_Invalid_Data(string quota, string period) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"), quota }, + { new FileInfo("/sys/fs/cgroup/cpu/cpu.cfs_period_us"), period } + }); + + var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetCgroupLimitedCpus()); + + Assert.IsAssignableFrom(r); + Assert.Contains("/sys/fs/cgroup/cpu/cpu.cfs_", r.Message); + } + + [Fact] + public void ReadingCpuUsage_Does_Not_Throw_For_Valid_Input() + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/proc/stat"), "cpu 2569530 36700 245693 4860924 82283 0 4360 0 0 0" } + }); + + var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetHostCpuUsageInNanoseconds()); + + Assert.Null(r); + } + + [Fact] + public void ReadingTotalMemory_Does_Not_Throw_For_Valid_Input() + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/sys/fs/cgroup/memory/memory.usage_in_bytes"), "32493514752\r\n" }, + { new FileInfo("/sys/fs/cgroup/memory/memory.stat"), "total_inactive_file 100" } + }); + + var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetMemoryUsageInBytes()); + + Assert.Null(r); + } + + [Theory] + [InlineData("2569530367000")] + [InlineData(" 2569530 36700 245693 4860924 82283 0 4360 0dsa 0 0 asdasd @@@@")] + [InlineData("asdasd 2569530 36700 245693 4860924 82283 0 4360 0 0 0")] + [InlineData(" 2569530 36700 245693")] + [InlineData("cpu 2569530 36700 245693")] + [InlineData(" 2")] + public void ReadingCpuUsage_Does_Throws_For_Valid_Input(string content) + { + var f = new HardcodedValueFileSystem(new Dictionary + { + { new FileInfo("/proc/stat"), content } + }); + + var p = new LinuxUtilizationParser(f, new FakeUserHz(100)); + var r = Record.Exception(() => p.GetHostCpuUsageInNanoseconds()); + + Assert.IsAssignableFrom(r); + Assert.Contains("proc/stat", r.Message); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTest.cs new file mode 100644 index 0000000000..642c46a550 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/LinuxUtilizationProviderTest.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; + +public sealed class LinuxUtilizationProviderTest +{ + [Fact] + public void Null_Checks() + { + Assert.Throws(() => new LinuxUtilizationProvider(Microsoft.Extensions.Options.Options.Create(null!), null!, null!, null!, null!)); + Assert.Throws(() => new LinuxUtilizationProvider(null!, null!, null!, null!)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTest.cs new file mode 100644 index 0000000000..e89d5ebca9 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/OSFileSystemTest.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Shared.Pools; +using Microsoft.TestUtilities; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; + +[OSSkipCondition(OperatingSystems.Windows | OperatingSystems.MacOSX, SkipReason = "Windows specific.")] +public sealed class OSFileSystemTest +{ + [ConditionalFact] + public void Reading_First_File_Line_Works() + { + const string Content = "Name: cat"; + var fileSystem = new OSFileSystem(); + var file = new FileInfo("fixtures/status"); + var bw = new BufferWriter(); + fileSystem.ReadFirstLine(file, bw); + var s = new string(bw.WrittenSpan).Replace("\r", ""); // Git is overwriting LF to CRLF all the time for windows, I am so tired of it I am hacking it!! + + Assert.Equal(Content, s); + } + + [ConditionalFact] + public void Reading_The_Whole_File_Works() + { + const string Content = "user 1399428\nsystem 1124053\n"; + var fileSystem = new OSFileSystem(); + var file = new FileInfo("fixtures/cpuacct.stat"); + var bw = new BufferWriter(); + fileSystem.ReadAll(file, bw); + + var s = new string(bw.WrittenSpan).Replace("\r", ""); // Git is overwriting LF to CRLF all the time for windows, I am so tired of it I am hacking it!! + + Assert.Equal(Content, s); + } + + [ConditionalTheory] + [InlineData(128)] + [InlineData(256)] + [InlineData(512)] + [InlineData(1024)] + public void Reading_Small_Portion_Of_Big_File_Works(int length) + { + const char Content = 'R'; + var b = new char[length]; + var fileSystem = new OSFileSystem(); + var file = new FileInfo("fixtures/FileWithRChars"); + var written = fileSystem.Read(file, length, b); + + Assert.True(length >= written); + Assert.True(b.AsSpan(0, written).SequenceEqual(new string(Content, written).AsSpan())); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FakeOperatingSystem.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FakeOperatingSystem.cs new file mode 100644 index 0000000000..9b5ca403bc --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FakeOperatingSystem.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; + +internal sealed class FakeOperatingSystem : IOperatingSystem +{ + public FakeOperatingSystem(bool isLinux) + { + IsLinux = isLinux; + } + + public bool IsLinux { get; } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FakeUserHz.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FakeUserHz.cs new file mode 100644 index 0000000000..3306d7c562 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FakeUserHz.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; + +internal sealed class FakeUserHz : IUserHz +{ + public FakeUserHz(long value) + { + Value = value; + } + + public long Value { get; } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs new file mode 100644 index 0000000000..c75e0f76ca --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/FileNamesOnlyFileSystem.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.IO; +using System.Linq; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; + +internal sealed class FileNamesOnlyFileSystem : IFileSystem +{ + private readonly string _directory; + + public FileNamesOnlyFileSystem(string directory) + { + _directory = directory; + } + + public void ReadFirstLine(FileInfo file, BufferWriter destination) + { + var a = File.ReadAllLines($"{_directory}/{file.Name}") + .FirstOrDefault() ?? string.Empty; + + destination.Write(a); + } + + public void ReadAll(FileInfo file, BufferWriter destination) + { + var c = File.ReadAllText($"{_directory}/{file.Name}"); + + destination.Write(c); + } + + public int Read(FileInfo file, int length, Span destination) + { + var c = File.ReadAllText($"{_directory}/{file.Name}"); + var min = Math.Min(length, c.Length); + + for (var i = 0; i < min; i++) + { + destination[i] = c[i]; + } + + return min; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/GenericPublisher.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/GenericPublisher.cs new file mode 100644 index 0000000000..4b110b642d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/GenericPublisher.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; + +/// +/// A publisher that accept in its constructor. +/// +internal sealed class GenericPublisher : IResourceUtilizationPublisher +{ + private readonly Action _publish; + public GenericPublisher(Action publish) + { + _publish = publish; + } + + /// + public ValueTask PublishAsync(Utilization utilization, CancellationToken cancellationToken) + { + _publish(utilization); + return default; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs new file mode 100644 index 0000000000..9bdc38f8fa --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/HardcodedValueFileSystem.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; + +internal sealed class HardcodedValueFileSystem : IFileSystem +{ + private readonly Dictionary _fileContent; + private readonly string _fallback; + + public HardcodedValueFileSystem(string fallback) + { + _fallback = fallback; + _fileContent = new(); + } + + public HardcodedValueFileSystem(Dictionary fileContent, string fallback = "") + { + _fileContent = fileContent.ToDictionary(static x => x.Key.FullName, static y => y.Value, StringComparer.OrdinalIgnoreCase); + _fallback = fallback; + } + + public void ReadFirstLine(FileInfo file, BufferWriter destination) + { + if (_fileContent.Count == 0 || !_fileContent.TryGetValue(file.FullName, out var content)) + { + destination.Write(_fallback); + + return; + } + + var newLineIndex = content.IndexOf('\n'); + + destination.Write(newLineIndex != -1 ? content.Substring(0, newLineIndex) : content); + } + + public void ReadAll(FileInfo file, BufferWriter destination) + { + if (_fileContent.Count == 0 || !_fileContent.TryGetValue(file.FullName, out var content)) + { + destination.Write(_fallback); + + return; + } + + destination.Write(content); + } + + public int Read(FileInfo file, int length, Span destination) + { + var toRead = _fallback; + + if (_fileContent.Count != 0 && _fileContent.TryGetValue(file.FullName, out var content)) + { + toRead = content; + } + + var min = Math.Min(toRead.Length, length); + + for (var i = 0; i < min; i++) + { + destination[i] = toRead[i]; + } + + return min; + } + + public void ReplaceFileContent(FileInfo file, string value) + { + _fileContent[file.FullName] = value; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/PathReturningFileSystem.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/PathReturningFileSystem.cs new file mode 100644 index 0000000000..8f5cb49f28 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/PathReturningFileSystem.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.IO; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; + +internal sealed class PathReturningFileSystem : IFileSystem +{ + public void ReadFirstLine(FileInfo file, BufferWriter destination) + { + destination.Write(file.FullName); + } + + public void ReadAll(FileInfo file, BufferWriter destination) + { + destination.Write(file.FullName); + } + + public int Read(FileInfo file, int length, Span destination) + { + var min = Math.Min(length, file.FullName.Length); + + for (var i = 0; i < min; i++) + { + destination[i] = file.FullName[i]; + } + + return min; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/TestResources.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/TestResources.cs new file mode 100644 index 0000000000..38bd3c22d1 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Linux/Resources/TestResources.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using Castle.Core.Internal; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux.Test; + +internal sealed class TestResources : IDisposable +{ + public static readonly string TestFilesLocation = "fixtures"; + + private static readonly Dictionary _files = new(StringComparer.OrdinalIgnoreCase) + { + { "/sys/fs/cgroup/cpu/cpu.shares", ""}, + { "/sys/fs/cgroup/cpuset/cpuset.cpus", "0-1"}, + { "/sys/fs/cgroup/memory/memory.limit_in_bytes", "1024"}, + { "/sys/fs/cgroup/cpu/cpu.cfs_quota_us", "1"}, + { "/sys/fs/cgroup/cpu/cpu.cfs_period_us", "1" }, + { "/proc/meminfo", "MemTotal: 1 kB\r\n"}, + }; + + private static readonly string[] _namesOfDirectories = + { + "/sys/fs/cgroup/memory", + "/sys/fs/cgroup/cpu", + "/proc" + }; + + private readonly HashSet _set = new(); + public readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + public void Dispose() + { + TearDown(); + } + + public void SetUp() + { + foreach (var directoryName in _namesOfDirectories) + { + if (!Directory.Exists(directoryName)) + { + Directory.CreateDirectory(directoryName); + } + } + + foreach (var files in _files) + { + if (!File.Exists(files.Key)) + { + if (files.Value.IsNullOrEmpty()) + { + File.Create(files.Key).Close(); + _set.Add(files.Key); + } + else + { + using var sw = File.CreateText(files.Key); + sw.Write(files.Value); + sw.Close(); + _set.Add(files.Key); + } + } + } + } + + public void TearDown() + { + foreach (var d in _set) + { + if (File.Exists(d)) + { + File.Delete(d); + } + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj new file mode 100644 index 0000000000..f204733abe --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests.csproj @@ -0,0 +1,29 @@ + + + Microsoft.Extensions.Diagnostics.ResourceMonitoring.Test + Unit tests for Microsoft.Extensions.Diagnostics.ResourceMonitoring + true + + + + + + + + + + PreserveNewest + + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/MemoryInfoTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/MemoryInfoTest.cs new file mode 100644 index 0000000000..d707268469 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/MemoryInfoTest.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.TestUtilities; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Test; + +/// +/// Memory Info Interop Tests. +/// +/// These tests are added for coverage reasons, but the code doesn't have +/// the necessary environment predictability to really test it. +public sealed class MemoryInfoTest +{ + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Windows specific.")] + public void GetGlobalMemory() + { + var memoryStatus = new MemoryInfo().GetMemoryStatus(); + Assert.True(memoryStatus.TotalPhys > 0); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/SystemInfoTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/SystemInfoTest.cs new file mode 100644 index 0000000000..e4410bb09f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/SystemInfoTest.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.TestUtilities; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Test; + +/// +/// System Info Interop Tests. +/// +/// These tests are added for coverage reasons, but the code doesn't have +/// the necessary environment predictability to really test it. +public sealed class SystemInfoTest +{ + /// + /// Get basic system info. + /// + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Windows specific.")] + public void GetSystemInfo() + { + var sysInfo = new SystemInfo().GetSystemInfo(); + Assert.True(sysInfo.NumberOfProcessors > 0); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/TcpTableInfoTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/TcpTableInfoTest.cs new file mode 100644 index 0000000000..9182eba17f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/TcpTableInfoTest.cs @@ -0,0 +1,262 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Test; + +[Collection("Tcp Connection Tests")] +public sealed class TcpTableInfoTest +{ + public static readonly TimeSpan DefaultTimeSpan = TimeSpan.FromSeconds(5); + public static DateTimeOffset StartTimestamp = DateTimeOffset.UtcNow; + public static DateTimeOffset NextTimestamp = StartTimestamp.Add(DefaultTimeSpan); + + // Each MIB_TCPROW needs 20. In experiments, the size should be 20 * MIB_TCPROW_count + 12. + private const uint FakeSize = 272; + + // Add 13 rows more. + private const uint FakeSize2 = FakeSize + (20 * 13); + private const uint FakeNumberOfEntries = 13; + private const uint FakeNumberOfEntries2 = FakeNumberOfEntries * 2; + + public static uint FakeGetTcpTableWithUnsuccessfulStatusAllTheTime(IntPtr pTcpTable, ref uint pdwSize, bool bOrder) + { + return (uint)NTSTATUS.UnsuccessfulStatus; + } + + public static uint FakeGetTcpTableWithInsufficientBufferAndInvalidParameter(IntPtr pTcpTable, ref uint pdwSize, bool bOrder) + { + if (pdwSize < FakeSize) + { + pdwSize = FakeSize; + return (uint)NTSTATUS.InsufficientBuffer; + } + + return (uint)NTSTATUS.InvalidParameter; + } + + public static unsafe uint FakeGetTcpTableWithFakeInformation(IntPtr pTcpTable, ref uint pdwSize, bool bOrder) + { + if (DateTimeOffset.UtcNow < NextTimestamp) + { + if (pdwSize < FakeSize) + { + pdwSize = FakeSize; + return (uint)NTSTATUS.InsufficientBuffer; + } + + MIB_TCPTABLE fakeTcpTable = new() + { + NumberOfEntries = FakeNumberOfEntries + }; + MIB_TCPROW[] fakeRows = new MIB_TCPROW[FakeNumberOfEntries]; + for (int i = 0; i < 12; ++i) + { + fakeRows[i] = new MIB_TCPROW + { + State = (MIB_TCP_STATE)(i + 1), + + // 16_777_343 means 127.0.0.1. + LocalAddr = 16_777_343 + }; + } + + fakeRows[12] = new MIB_TCPROW + { + State = MIB_TCP_STATE.DELETE_TCB, + }; + + // True means the result should be sorted. + if (bOrder) + { + fakeRows[12].LocalAddr = 16_777_343 + 1; + } + else + { + fakeRows[11].LocalAddr = 16_777_343 + 1; + fakeRows[12].LocalAddr = 16_777_343; + } + + fakeTcpTable.Table = fakeRows[0]; + Marshal.StructureToPtr(fakeTcpTable, pTcpTable, false); + var offset = Marshal.OffsetOf(nameof(MIB_TCPTABLE.Table)).ToInt32(); + var fakePtr = IntPtr.Add(pTcpTable, offset); + + for (int i = 0; i < fakeTcpTable.NumberOfEntries; ++i) + { + Marshal.StructureToPtr(fakeRows[i], fakePtr, false); + fakePtr = IntPtr.Add(fakePtr, sizeof(MIB_TCPROW)); + } + } + else + { + if (pdwSize < FakeSize2) + { + pdwSize = FakeSize2; + return (uint)NTSTATUS.InsufficientBuffer; + } + + MIB_TCPTABLE fakeTcpTable = new() + { + NumberOfEntries = FakeNumberOfEntries2 + }; + MIB_TCPROW[] fakeRows = new MIB_TCPROW[FakeNumberOfEntries2]; + for (int i = 0; i < 12; ++i) + { + fakeRows[i] = new MIB_TCPROW + { + State = (MIB_TCP_STATE)(i + 1), + + // 16_777_343 means 127.0.0.1. + LocalAddr = 16_777_343 + }; + } + + for (int i = 13; i < 25; ++i) + { + fakeRows[i] = new MIB_TCPROW + { + State = (MIB_TCP_STATE)(i + 1 - 13), + + // 16_777_343 means 127.0.0.1. + LocalAddr = 16_777_343 + }; + } + + fakeRows[12] = new MIB_TCPROW + { + State = MIB_TCP_STATE.DELETE_TCB, + }; + fakeRows[25] = new MIB_TCPROW + { + State = MIB_TCP_STATE.DELETE_TCB, + }; + + // True means the result should be sorted. + if (bOrder) + { + fakeRows[12].LocalAddr = 16_777_343 + 1; + fakeRows[25].LocalAddr = 16_777_343 + 1; + } + else + { + fakeRows[11].LocalAddr = 16_777_343 + 1; + fakeRows[12].LocalAddr = 16_777_343; + fakeRows[24].LocalAddr = 16_777_343 + 1; + fakeRows[25].LocalAddr = 16_777_343; + } + + fakeTcpTable.Table = fakeRows[0]; + Marshal.StructureToPtr(fakeTcpTable, pTcpTable, false); + var offset = Marshal.OffsetOf(nameof(MIB_TCPTABLE.Table)).ToInt32(); + var fakePtr = IntPtr.Add(pTcpTable, offset); + + for (int i = 0; i < fakeTcpTable.NumberOfEntries; ++i) + { + Marshal.StructureToPtr(fakeRows[i], fakePtr, false); + fakePtr = IntPtr.Add(fakePtr, sizeof(MIB_TCPROW)); + } + } + + return (uint)NTSTATUS.Success; + } + + [Fact] + public void Test_TcpTableInfo_Get_UnsuccessfulStatus_All_The_Time() + { + TcpTableInfo.SetGetTcpTableDelegate(FakeGetTcpTableWithUnsuccessfulStatusAllTheTime); + var options = new WindowsCountersOptions + { + InstanceIpAddresses = new HashSet { "127.0.0.1" }, + CachingInterval = DefaultTimeSpan + }; + TcpTableInfo tcpTableInfo = new TcpTableInfo(Microsoft.Extensions.Options.Options.Create(options)); + Assert.Throws(() => + { + var tcpStateInfo = tcpTableInfo.GetCachingSnapshot(); + }); + } + + [Fact] + public void Test_TcpTableInfo_Get_InsufficientBuffer_Then_Get_InvalidParameter() + { + TcpTableInfo.SetGetTcpTableDelegate(FakeGetTcpTableWithInsufficientBufferAndInvalidParameter); + var options = new WindowsCountersOptions + { + InstanceIpAddresses = new HashSet { "127.0.0.1" }, + CachingInterval = DefaultTimeSpan + }; + TcpTableInfo tcpTableInfo = new TcpTableInfo(Microsoft.Extensions.Options.Options.Create(options)); + Assert.Throws(() => + { + var tcpStateInfo = tcpTableInfo.GetCachingSnapshot(); + }); + } + + [Fact] + public void Test_TcpTableInfo_Get_Correct_Information() + { + StartTimestamp = DateTimeOffset.UtcNow; + NextTimestamp = StartTimestamp.Add(DefaultTimeSpan); + TcpTableInfo.SetGetTcpTableDelegate(FakeGetTcpTableWithFakeInformation); + var options = new WindowsCountersOptions + { + InstanceIpAddresses = new HashSet { "127.0.0.1" }, + CachingInterval = DefaultTimeSpan + }; + TcpTableInfo tcpTableInfo = new TcpTableInfo(Microsoft.Extensions.Options.Options.Create(options)); + var tcpStateInfo = tcpTableInfo.GetCachingSnapshot(); + Assert.NotNull(tcpStateInfo); + Assert.Equal(1, tcpStateInfo.ClosedCount); + Assert.Equal(1, tcpStateInfo.ListenCount); + Assert.Equal(1, tcpStateInfo.SynSentCount); + Assert.Equal(1, tcpStateInfo.SynRcvdCount); + Assert.Equal(1, tcpStateInfo.EstabCount); + Assert.Equal(1, tcpStateInfo.FinWait1Count); + Assert.Equal(1, tcpStateInfo.FinWait2Count); + Assert.Equal(1, tcpStateInfo.CloseWaitCount); + Assert.Equal(1, tcpStateInfo.ClosingCount); + Assert.Equal(1, tcpStateInfo.LastAckCount); + Assert.Equal(1, tcpStateInfo.TimeWaitCount); + Assert.Equal(1, tcpStateInfo.DeleteTcbCount); + + // Second calling in a small interval. + tcpStateInfo = tcpTableInfo.GetCachingSnapshot(); + Assert.NotNull(tcpStateInfo); + Assert.Equal(1, tcpStateInfo.ClosedCount); + Assert.Equal(1, tcpStateInfo.ListenCount); + Assert.Equal(1, tcpStateInfo.SynSentCount); + Assert.Equal(1, tcpStateInfo.SynRcvdCount); + Assert.Equal(1, tcpStateInfo.EstabCount); + Assert.Equal(1, tcpStateInfo.FinWait1Count); + Assert.Equal(1, tcpStateInfo.FinWait2Count); + Assert.Equal(1, tcpStateInfo.CloseWaitCount); + Assert.Equal(1, tcpStateInfo.ClosingCount); + Assert.Equal(1, tcpStateInfo.LastAckCount); + Assert.Equal(1, tcpStateInfo.TimeWaitCount); + Assert.Equal(1, tcpStateInfo.DeleteTcbCount); + + // Third calling in a long interval. + Thread.Sleep(6000); + tcpStateInfo = tcpTableInfo.GetCachingSnapshot(); + Assert.NotNull(tcpStateInfo); + Assert.Equal(2, tcpStateInfo.ClosedCount); + Assert.Equal(2, tcpStateInfo.ListenCount); + Assert.Equal(2, tcpStateInfo.SynSentCount); + Assert.Equal(2, tcpStateInfo.SynRcvdCount); + Assert.Equal(2, tcpStateInfo.EstabCount); + Assert.Equal(2, tcpStateInfo.FinWait1Count); + Assert.Equal(2, tcpStateInfo.FinWait2Count); + Assert.Equal(2, tcpStateInfo.CloseWaitCount); + Assert.Equal(2, tcpStateInfo.ClosingCount); + Assert.Equal(2, tcpStateInfo.LastAckCount); + Assert.Equal(2, tcpStateInfo.TimeWaitCount); + Assert.Equal(2, tcpStateInfo.DeleteTcbCount); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTest.cs new file mode 100644 index 0000000000..9104944556 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsContainerSnapshotProviderTest.cs @@ -0,0 +1,236 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Moq; +using Xunit; +using static Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal.JobObjectInfo; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Test; + +public sealed class WindowsContainerSnapshotProviderTest +{ + [Theory] + [InlineData(7_000, 1U, 0.7)] + [InlineData(10_000, 1U, 1.0)] + [InlineData(10_000, 2U, 2.0)] + [InlineData(5_000, 2U, 1.0)] + public void Resources_GetsCorrectSystemResourcesValues(uint cpuRate, uint numberOfProcessors, double expectedCpuUnits) + { + MEMORYSTATUSEX memStatus = default; + memStatus.TotalPhys = 3000UL; + + SYSTEM_INFO sysInfo = default; + sysInfo.NumberOfProcessors = numberOfProcessors; + + JOBOBJECT_CPU_RATE_CONTROL_INFORMATION cpuLimit = default; + + // This is customized to force the private method GetGuaranteedCpuUnits + // to use the value of CpuRate and divide it by 10_000. + cpuLimit.ControlFlags = 5; + + // The CpuRate is the Cpu percentage multiplied by 100, check this: + // https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_cpu_rate_control_information + cpuLimit.CpuRate = cpuRate; + + JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accountingInfo = default; + accountingInfo.TotalKernelTime = 1000; + accountingInfo.TotalUserTime = 1000; + + JOBOBJECT_EXTENDED_LIMIT_INFORMATION limitInfo = default; + limitInfo.JobMemoryLimit = new UIntPtr(2000); + + ProcessInfo.APP_MEMORY_INFORMATION appMemoryInfo = default; + appMemoryInfo.TotalCommitUsage = 1000UL; + + var memoryInfoMock = new Mock(); + memoryInfoMock.Setup(m => m.GetMemoryStatus()).Returns(memStatus); + + var systemInfoMock = new Mock(); + systemInfoMock.Setup(s => s.GetSystemInfo()).Returns(sysInfo); + + var processInfoMock = new Mock(); + processInfoMock.Setup(p => p.GetCurrentAppMemoryInfo()).Returns(appMemoryInfo); + + var jobHandleMock = new Mock(); + jobHandleMock.Setup(j => j.GetJobCpuLimitInfo()).Returns(cpuLimit); + jobHandleMock.Setup(j => j.GetBasicAccountingInfo()).Returns(accountingInfo); + jobHandleMock.Setup(j => j.GetExtendedLimitInfo()).Returns(limitInfo); + + var provider = new WindowsContainerSnapshotProvider( + memoryInfoMock.Object, + systemInfoMock.Object, + processInfoMock.Object, + () => jobHandleMock.Object); + + Assert.Equal(expectedCpuUnits, provider.Resources.GuaranteedCpuUnits); + Assert.Equal(expectedCpuUnits, provider.Resources.MaximumCpuUnits); + Assert.Equal(limitInfo.JobMemoryLimit.ToUInt64(), provider.Resources.GuaranteedMemoryInBytes); + Assert.Equal(limitInfo.JobMemoryLimit.ToUInt64(), provider.Resources.MaximumMemoryInBytes); + } + + [Fact] + public void GetSnapshot_ProducesCorrectSnapshot() + { + MEMORYSTATUSEX memStatus = default; + memStatus.TotalPhys = 3000UL; + + SYSTEM_INFO sysInfo = default; + sysInfo.NumberOfProcessors = 1; + + JOBOBJECT_CPU_RATE_CONTROL_INFORMATION cpuLimit = default; + + // The ControlFlags is customized to force the private method GetGuaranteedCpuUnits + // to not use the value of CpuRate in the calculation. + cpuLimit.ControlFlags = 1; + cpuLimit.CpuRate = 7_000; + + JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accountingInfo = default; + accountingInfo.TotalKernelTime = 1000; + accountingInfo.TotalUserTime = 1000; + + JOBOBJECT_EXTENDED_LIMIT_INFORMATION limitInfo = default; + limitInfo.JobMemoryLimit = new UIntPtr(2000); + + ProcessInfo.APP_MEMORY_INFORMATION appMemoryInfo = default; + appMemoryInfo.TotalCommitUsage = 1000UL; + + var memoryInfoMock = new Mock(); + memoryInfoMock.Setup(m => m.GetMemoryStatus()).Returns(memStatus); + + var systemInfoMock = new Mock(); + systemInfoMock.Setup(s => s.GetSystemInfo()).Returns(sysInfo); + + var processInfoMock = new Mock(); + processInfoMock.Setup(p => p.GetCurrentAppMemoryInfo()).Returns(appMemoryInfo); + + var jobHandleMock = new Mock(); + jobHandleMock.Setup(j => j.GetJobCpuLimitInfo()).Returns(cpuLimit); + jobHandleMock.Setup(j => j.GetBasicAccountingInfo()).Returns(accountingInfo); + jobHandleMock.Setup(j => j.GetExtendedLimitInfo()).Returns(limitInfo); + + var source = new WindowsContainerSnapshotProvider( + memoryInfoMock.Object, + systemInfoMock.Object, + processInfoMock.Object, + () => jobHandleMock.Object); + var data = source.GetSnapshot(); + Assert.Equal(accountingInfo.TotalKernelTime, data.KernelTimeSinceStart.Ticks); + Assert.Equal(accountingInfo.TotalUserTime, data.UserTimeSinceStart.Ticks); + Assert.Equal(limitInfo.JobMemoryLimit.ToUInt64(), source.Resources.GuaranteedMemoryInBytes); + Assert.Equal(limitInfo.JobMemoryLimit.ToUInt64(), source.Resources.MaximumMemoryInBytes); + Assert.Equal(appMemoryInfo.TotalCommitUsage, data.MemoryUsageInBytes); + Assert.True(data.MemoryUsageInBytes > 0); + } + + [Fact] + public void GetSnapshot_ProducesCorrectSnapshotForDifferentCpuRate() + { + MEMORYSTATUSEX memStatus = default; + memStatus.TotalPhys = 3000UL; + + SYSTEM_INFO sysInfo = default; + sysInfo.NumberOfProcessors = 1; + + JOBOBJECT_CPU_RATE_CONTROL_INFORMATION cpuLimit = default; + cpuLimit.ControlFlags = uint.MaxValue; // force all bits in ControlFlags to be 1. + cpuLimit.CpuRate = 7_000; + + JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accountingInfo = default; + accountingInfo.TotalKernelTime = 1000; + accountingInfo.TotalUserTime = 1000; + + JOBOBJECT_EXTENDED_LIMIT_INFORMATION limitInfo = default; + limitInfo.JobMemoryLimit = new UIntPtr(2000); + + ProcessInfo.APP_MEMORY_INFORMATION appMemoryInfo = default; + appMemoryInfo.TotalCommitUsage = 1000UL; + + var memoryInfoMock = new Mock(); + memoryInfoMock.Setup(m => m.GetMemoryStatus()).Returns(memStatus); + + var systemInfoMock = new Mock(); + systemInfoMock.Setup(s => s.GetSystemInfo()).Returns(sysInfo); + + var processInfoMock = new Mock(); + processInfoMock.Setup(p => p.GetCurrentAppMemoryInfo()).Returns(appMemoryInfo); + + var jobHandleMock = new Mock(); + jobHandleMock.Setup(j => j.GetJobCpuLimitInfo()).Returns(cpuLimit); + jobHandleMock.Setup(j => j.GetBasicAccountingInfo()).Returns(accountingInfo); + jobHandleMock.Setup(j => j.GetExtendedLimitInfo()).Returns(limitInfo); + + var source = new WindowsContainerSnapshotProvider( + memoryInfoMock.Object, + systemInfoMock.Object, + processInfoMock.Object, + () => jobHandleMock.Object); + var data = source.GetSnapshot(); + + Assert.Equal(accountingInfo.TotalKernelTime, data.KernelTimeSinceStart.Ticks); + Assert.Equal(accountingInfo.TotalUserTime, data.UserTimeSinceStart.Ticks); + Assert.Equal(0.7, source.Resources.GuaranteedCpuUnits); + Assert.Equal(0.7, source.Resources.MaximumCpuUnits); + Assert.Equal(limitInfo.JobMemoryLimit.ToUInt64(), source.Resources.GuaranteedMemoryInBytes); + Assert.Equal(limitInfo.JobMemoryLimit.ToUInt64(), source.Resources.MaximumMemoryInBytes); + Assert.Equal(appMemoryInfo.TotalCommitUsage, data.MemoryUsageInBytes); + Assert.True(data.MemoryUsageInBytes > 0); + } + + [Fact] + public void GetSnapshot_With_JobMemoryLimit_Set_To_Zero_ProducesCorrectSnapshot() + { + MEMORYSTATUSEX memStatus = default; + memStatus.TotalPhys = 3000UL; + + SYSTEM_INFO sysInfo = default; + sysInfo.NumberOfProcessors = 1; + + JOBOBJECT_CPU_RATE_CONTROL_INFORMATION cpuLimit = default; + + // This is customized to force the private method GetGuaranteedCpuUnits + // to set the GuaranteedCpuUnits and MaximumCpuUnits to 1.0. + cpuLimit.ControlFlags = 4; + cpuLimit.CpuRate = 7_000; + + JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accountingInfo = default; + accountingInfo.TotalKernelTime = 1000; + accountingInfo.TotalUserTime = 1000; + + JOBOBJECT_EXTENDED_LIMIT_INFORMATION limitInfo = default; + limitInfo.JobMemoryLimit = new UIntPtr(0); + + ProcessInfo.APP_MEMORY_INFORMATION appMemoryInfo = default; + appMemoryInfo.TotalCommitUsage = 3000UL; + + var memoryInfoMock = new Mock(); + memoryInfoMock.Setup(m => m.GetMemoryStatus()).Returns(memStatus); + + var systemInfoMock = new Mock(); + systemInfoMock.Setup(s => s.GetSystemInfo()).Returns(sysInfo); + + var processInfoMock = new Mock(); + processInfoMock.Setup(p => p.GetCurrentAppMemoryInfo()).Returns(appMemoryInfo); + + var jobHandleMock = new Mock(); + jobHandleMock.Setup(j => j.GetJobCpuLimitInfo()).Returns(cpuLimit); + jobHandleMock.Setup(j => j.GetBasicAccountingInfo()).Returns(accountingInfo); + jobHandleMock.Setup(j => j.GetExtendedLimitInfo()).Returns(limitInfo); + + var source = new WindowsContainerSnapshotProvider( + memoryInfoMock.Object, + systemInfoMock.Object, + processInfoMock.Object, + () => jobHandleMock.Object); + var data = source.GetSnapshot(); + Assert.Equal(accountingInfo.TotalKernelTime, data.KernelTimeSinceStart.Ticks); + Assert.Equal(accountingInfo.TotalUserTime, data.UserTimeSinceStart.Ticks); + Assert.Equal(1.0, source.Resources.GuaranteedCpuUnits); + Assert.Equal(1.0, source.Resources.MaximumCpuUnits); + Assert.Equal(memStatus.TotalPhys, source.Resources.GuaranteedMemoryInBytes); + Assert.Equal(memStatus.TotalPhys, source.Resources.MaximumMemoryInBytes); + Assert.Equal(appMemoryInfo.TotalCommitUsage, data.MemoryUsageInBytes); + Assert.True(data.MemoryUsageInBytes > 0); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsCountersOptionsCustomValidatorTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsCountersOptionsCustomValidatorTest.cs new file mode 100644 index 0000000000..ab0805b575 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsCountersOptionsCustomValidatorTest.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Test; + +public sealed class WindowsCountersOptionsCustomValidatorTest +{ + [Fact] + public void Test_WindowsCountersOptionsCustomValidator_With_Fake_IPv6_Address() + { + var options = new WindowsCountersOptions + { + InstanceIpAddresses = new HashSet { "[::]" } + }; + var validator = new WindowsCountersOptionsCustomValidator(); + var result = validator.Validate("", options); + + Assert.True(result.Failed); + } + + [Fact] + public void Test_WindowsCountersOptionsCustomValidator_with_Fake_IPv4_Address() + { + var options = new WindowsCountersOptions + { + InstanceIpAddresses = new HashSet { "127.0.0.1" } + }; + var validator = new WindowsCountersOptionsCustomValidator(); + var result = validator.Validate("", options); + + Assert.True(result.Succeeded); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsCountersTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsCountersTest.cs new file mode 100644 index 0000000000..dc613ae438 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsCountersTest.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Extensions.Telemetry.Metering; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Test; + +[Collection("Tcp Connection Tests")] +public sealed class WindowsCountersTest +{ + [Fact] + public void WindowsCounters_Registers_Instruments() + { + TcpTableInfoTest.StartTimestamp = DateTimeOffset.UtcNow; + TcpTableInfoTest.NextTimestamp = TcpTableInfoTest.StartTimestamp.Add(TcpTableInfoTest.DefaultTimeSpan); + TcpTableInfo.SetGetTcpTableDelegate(TcpTableInfoTest.FakeGetTcpTableWithFakeInformation); + var options = new WindowsCountersOptions + { + InstanceIpAddresses = new HashSet { "127.0.0.1" }, + CachingInterval = TimeSpan.FromSeconds(5) + }; + using var meter = new Meter(); + using var windowsCounters = new WindowsCounters(Microsoft.Extensions.Options.Options.Create(options), meter); + using var listener = new System.Diagnostics.Metrics.MeterListener + { + InstrumentPublished = (instrument, listener) => + { + listener.EnableMeasurementEvents(instrument); + } + }; + + var samples = new List<(System.Diagnostics.Metrics.Instrument instrument, long value)>(); + listener.SetMeasurementEventCallback((instrument, value, _, _) => + { + samples.Add((instrument, value)); + }); + + listener.Start(); + listener.RecordObservableInstruments(); + samples.Count.Should().Be(12); + samples.First().instrument.Name.Should().Be("ipv4_tcp_connection_closed_count"); + samples.First().value.Should().Be(1); + samples.Skip(1).First().instrument.Name.Should().Be("ipv4_tcp_connection_listen_count"); + samples.Skip(1).First().value.Should().Be(1); + samples.Skip(2).First().instrument.Name.Should().Be("ipv4_tcp_connection_syn_sent_count"); + samples.Skip(2).First().value.Should().Be(1); + samples.Skip(3).First().instrument.Name.Should().Be("ipv4_tcp_connection_syn_received_count"); + samples.Skip(3).First().value.Should().Be(1); + samples.Skip(4).First().instrument.Name.Should().Be("ipv4_tcp_connection_established_count"); + samples.Skip(4).First().value.Should().Be(1); + samples.Skip(5).First().instrument.Name.Should().Be("ipv4_tcp_connection_fin_wait_1_count"); + samples.Skip(5).First().value.Should().Be(1); + samples.Skip(6).First().instrument.Name.Should().Be("ipv4_tcp_connection_fin_wait_2_count"); + samples.Skip(6).First().value.Should().Be(1); + samples.Skip(7).First().instrument.Name.Should().Be("ipv4_tcp_connection_close_wait_count"); + samples.Skip(7).First().value.Should().Be(1); + samples.Skip(8).First().instrument.Name.Should().Be("ipv4_tcp_connection_closing_count"); + samples.Skip(8).First().value.Should().Be(1); + samples.Skip(9).First().instrument.Name.Should().Be("ipv4_tcp_connection_last_ack_count"); + samples.Skip(9).First().value.Should().Be(1); + samples.Skip(10).First().instrument.Name.Should().Be("ipv4_tcp_connection_time_wait_count"); + samples.Skip(10).First().value.Should().Be(1); + samples.Skip(11).First().instrument.Name.Should().Be("ipv4_tcp_connection_delete_tcb_count"); + samples.Skip(11).First().value.Should().Be(1); + } + + [Fact] + public void WindowsCounters_Got_Unsuccessful() + { + TcpTableInfo.SetGetTcpTableDelegate(TcpTableInfoTest.FakeGetTcpTableWithUnsuccessfulStatusAllTheTime); + var options = new WindowsCountersOptions + { + InstanceIpAddresses = new HashSet { "127.0.0.1" }, + CachingInterval = TimeSpan.FromSeconds(5) + }; + using var meter = new Meter(); + using var windowsCounters = new WindowsCounters(Microsoft.Extensions.Options.Options.Create(options), meter); + using var listener = new System.Diagnostics.Metrics.MeterListener + { + InstrumentPublished = (instrument, listener) => + { + listener.EnableMeasurementEvents(instrument); + } + }; + + var samples = new List<(System.Diagnostics.Metrics.Instrument instrument, long value)>(); + listener.SetMeasurementEventCallback((instrument, value, _, _) => + { + samples.Add((instrument, value)); + }); + + listener.Start(); + Assert.Throws(() => + { + listener.RecordObservableInstruments(); + }); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsSnapshotProviderTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsSnapshotProviderTest.cs new file mode 100644 index 0000000000..48d0021b83 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsSnapshotProviderTest.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.TestUtilities; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Test; + +public sealed class WindowsSnapshotProviderTest +{ + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Windows specific.")] + public void BasicConstructor() + { + var loggerMock = new Mock>(); + var provider = new WindowsSnapshotProvider(loggerMock.Object); + var memoryStatus = new MemoryInfo().GetMemoryStatus(); + + Assert.Equal(Environment.ProcessorCount, provider.Resources.GuaranteedCpuUnits); + Assert.Equal(Environment.ProcessorCount, provider.Resources.MaximumCpuUnits); + Assert.Equal(memoryStatus.TotalPhys, provider.Resources.GuaranteedMemoryInBytes); + Assert.Equal(memoryStatus.TotalPhys, provider.Resources.MaximumMemoryInBytes); + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Windows specific.")] + public void GetSnapshot_DoesNotThrowExceptions() + { + var loggerMock = new Mock>(); + var provider = new WindowsSnapshotProvider(loggerMock.Object); + + var exception = Record.Exception(() => provider.GetSnapshot()); + Assert.Null(exception); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsUtilizationExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsUtilizationExtensionsTest.cs new file mode 100644 index 0000000000..b535408aa4 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/Windows/WindowsUtilizationExtensionsTest.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Internal; +using Microsoft.TestUtilities; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Windows.Test; + +[Collection("Tcp Connection Tests")] +public sealed class WindowsUtilizationExtensionsTest +{ + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Windows specific.")] + public void AddWindowsProvider_Adds_WindowsResourceUtilizationProvider_To_ServiceCollection() + { + var builderMock = new Mock(); + var services = new ServiceCollection() + .AddLogging(); + builderMock.Setup(builder => builder.Services).Returns(services); + + builderMock.Object + .AddWindowsProvider() + .AddWindowsPerfCounterPublisher(); + + var descriptor = services.Single(d => d.ServiceType == typeof(ISnapshotProvider)); + Assert.NotNull(descriptor); + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Windows specific.")] + public void AddWindowsPerfCounterPublisher_Adds_WindowsPerfCounterPublisher_To_ServiceCollection() + { + var builderMock = new Mock(MockBehavior.Loose); + var services = new ServiceCollection() + .AddLogging(); + builderMock.Setup(builder => builder.Services).Returns(services); + + builderMock.Object + .AddWindowsProvider() + .AddWindowsPerfCounterPublisher(); + + builderMock.Verify(b => b.AddPublisher()); + } + + [Fact] + public void AddWindowsCounters_Adds_WindowsCounters_To_ServiceCollection() + { + var builderMock = new Mock(MockBehavior.Loose); + var services = new ServiceCollection() + .AddLogging(); + builderMock.Setup(builder => builder.Services).Returns(services); + + builderMock.Object + .AddWindowsCounters(); + + var provider = services.BuildServiceProvider(); + Assert.NotNull(provider.GetRequiredService()); + } + + [Fact] + public void AddWindowsCounters_Adds_WindowsCounters_To_ServiceCollection_With_ConfigurationSection() + { + var builderMock = new Mock(MockBehavior.Loose); + var configurationMock = new Mock(MockBehavior.Loose); + var services = new ServiceCollection() + .AddLogging(); + builderMock.Setup(builder => builder.Services).Returns(services); + + builderMock.Object + .AddWindowsCounters(configurationMock.Object); + + var provider = services.BuildServiceProvider(); + Assert.NotNull(provider.GetRequiredService()); + } + + [Fact] + public void AddWindowsCounters_Adds_WindowsCounters_To_ServiceCollection_With_Action() + { + var builderMock = new Mock(MockBehavior.Loose); + var actionMock = new Mock>(MockBehavior.Loose); + var services = new ServiceCollection() + .AddLogging(); + builderMock.Setup(builder => builder.Services).Returns(services); + + builderMock.Object + .AddWindowsCounters(actionMock.Object); + + var provider = services.BuildServiceProvider(); + Assert.NotNull(provider.GetRequiredService()); + } + + [Fact] + public void VerifyHostBuilderNullCheck() + { + Assert.Throws(() => WindowsUtilizationExtensions.AddWindowsProvider(null!)); + Assert.Throws(() => WindowsUtilizationExtensions.AddWindowsPerfCounterPublisher(null!)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/FileWithRChars b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/FileWithRChars new file mode 100644 index 0000000000..f3d6d0bcd6 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/FileWithRChars @@ -0,0 +1 @@ +RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpu.cfs_period_us b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpu.cfs_period_us new file mode 100644 index 0000000000..5a73e7926b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpu.cfs_period_us @@ -0,0 +1 @@ +200000 \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpu.cfs_quota_us b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpu.cfs_quota_us new file mode 100644 index 0000000000..5a73e7926b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpu.cfs_quota_us @@ -0,0 +1 @@ +200000 \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpuacct.stat b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpuacct.stat new file mode 100644 index 0000000000..c56ddfb28e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpuacct.stat @@ -0,0 +1,2 @@ +user 1399428 +system 1124053 diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpuset.cpus b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpuset.cpus new file mode 100644 index 0000000000..8b0fab869c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/cpuset.cpus @@ -0,0 +1 @@ +0-1 diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/meminfo b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/meminfo new file mode 100644 index 0000000000..06e6756bf3 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/meminfo @@ -0,0 +1,50 @@ +MemTotal: 16233760 kB +MemFree: 8786820 kB +MemAvailable: 13587740 kB +Buffers: 329144 kB +Cached: 4659780 kB +SwapCached: 0 kB +Active: 1461828 kB +Inactive: 5405356 kB +Active(anon): 6060 kB +Inactive(anon): 1877548 kB +Active(file): 1455768 kB +Inactive(file): 3527808 kB +Unevictable: 0 kB +Mlocked: 0 kB +SwapTotal: 4194304 kB +SwapFree: 4194304 kB +Dirty: 128 kB +Writeback: 0 kB +AnonPages: 1666304 kB +Mapped: 661644 kB +Shmem: 10660 kB +KReclaimable: 131820 kB +Slab: 243220 kB +SReclaimable: 131820 kB +SUnreclaim: 111400 kB +KernelStack: 24352 kB +PageTables: 13828 kB +NFS_Unstable: 0 kB +Bounce: 0 kB +WritebackTmp: 0 kB +CommitLimit: 12311184 kB +Committed_AS: 6783148 kB +VmallocTotal: 34359738367 kB +VmallocUsed: 46624 kB +VmallocChunk: 0 kB +Percpu: 16512 kB +AnonHugePages: 1175552 kB +ShmemHugePages: 0 kB +ShmemPmdMapped: 0 kB +FileHugePages: 0 kB +FilePmdMapped: 0 kB +HugePages_Total: 0 +HugePages_Free: 0 +HugePages_Rsvd: 0 +HugePages_Surp: 0 +Hugepagesize: 2048 kB +Hugetlb: 0 kB +DirectMap4k: 91136 kB +DirectMap2M: 11292672 kB +DirectMap1G: 13631488 kB diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/memory.limit_in_bytes b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/memory.limit_in_bytes new file mode 100644 index 0000000000..f288803365 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/memory.limit_in_bytes @@ -0,0 +1 @@ +3082706944 diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/memory.usage_in_bytes b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/memory.usage_in_bytes new file mode 100644 index 0000000000..3164dd8291 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/memory.usage_in_bytes @@ -0,0 +1 @@ +30827 diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/status b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/status new file mode 100644 index 0000000000..915bebc313 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/status @@ -0,0 +1,56 @@ +Name: cat +Umask: 0022 +State: R (running) +Tgid: 310 +Ngid: 0 +Pid: 310 +PPid: 170 +TracerPid: 0 +Uid: 0 0 0 0 +Gid: 0 0 0 0 +FDSize: 256 +Groups: 0 1000 +NStgid: 310 +NSpid: 310 +NSpgid: 310 +NSsid: 170 +VmPeak: 3344 kB +VmSize: 3344 kB +VmLck: 0 kB +VmPin: 0 kB +VmHWM: 1016 kB +VmRSS: 1016 kB +RssAnon: 92 kB +RssFile: 924 kB +RssShmem: 0 kB +VmData: 360 kB +VmStk: 132 kB +VmExe: 16 kB +VmLib: 1792 kB +VmPTE: 44 kB +VmSwap: 0 kB +HugetlbPages: 0 kB +CoreDumping: 0 +THP_enabled: 1 +Threads: 1 +SigQ: 1/63393 +SigPnd: 0000000000000000 +ShdPnd: 0000000000000000 +SigBlk: 0000000000000000 +SigIgn: 0000000200000000 +SigCgt: 0000000000000000 +CapInh: 0000000000000000 +CapPrm: 000001ffffffffff +CapEff: 000001ffffffffff +CapBnd: 000001ffffffffff +CapAmb: 0000000000000000 +NoNewPrivs: 0 +Seccomp: 0 +Seccomp_filters: 0 +Speculation_Store_Bypass: thread vulnerable +Cpus_allowed: fffff +Cpus_allowed_list: 0-19 +Mems_allowed: 1 +Mems_allowed_list: 0 +voluntary_ctxt_switches: 0 +nonvoluntary_ctxt_switches: 0 diff --git a/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/test.cpuacct.stat b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/test.cpuacct.stat new file mode 100644 index 0000000000..c56ddfb28e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Tests/fixtures/test.cpuacct.stat @@ -0,0 +1,2 @@ +user 1399428 +system 1124053 diff --git a/test/Libraries/Microsoft.Extensions.EnumStrings.Tests/EnumStringsAttributeTests.cs b/test/Libraries/Microsoft.Extensions.EnumStrings.Tests/EnumStringsAttributeTests.cs new file mode 100644 index 0000000000..14121ae8e9 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.EnumStrings.Tests/EnumStringsAttributeTests.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.EnumStrings.Tests; + +public static class EnumStringsAttributeTests +{ + private enum Color + { + Red, + Green, + Blue, + } + + [Fact] + public static void All() + { + var a = new EnumStringsAttribute(); + Assert.Null(a.ExtensionNamespace); + Assert.Null(a.ExtensionClassName); + Assert.Equal("ToInvariantString", a.ExtensionMethodName); + Assert.Equal("internal static", a.ExtensionClassModifiers); + Assert.Null(a.EnumType); + + a = new EnumStringsAttribute(typeof(Color)); + Assert.Null(a.ExtensionNamespace); + Assert.Null(a.ExtensionClassName); + Assert.Equal("ToInvariantString", a.ExtensionMethodName); + Assert.Equal("internal static", a.ExtensionClassModifiers); + Assert.Equal(typeof(Color), a.EnumType); + + a.ExtensionNamespace = "A"; + a.ExtensionClassName = "B"; + a.ExtensionMethodName = "C"; + a.ExtensionClassModifiers = "D"; + + Assert.Equal("A", a.ExtensionNamespace); + Assert.Equal("B", a.ExtensionClassName); + Assert.Equal("C", a.ExtensionMethodName); + Assert.Equal("D", a.ExtensionClassModifiers); + + } +} diff --git a/test/Libraries/Microsoft.Extensions.EnumStrings.Tests/Microsoft.Extensions.EnumStrings.Tests.csproj b/test/Libraries/Microsoft.Extensions.EnumStrings.Tests/Microsoft.Extensions.EnumStrings.Tests.csproj new file mode 100644 index 0000000000..8acd17f845 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.EnumStrings.Tests/Microsoft.Extensions.EnumStrings.Tests.csproj @@ -0,0 +1,10 @@ + + + Microsoft.Extensions.EnumStrings.Test + Unit tests for Microsoft.Extensions.EnumStrings. + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/FakeConfigurationSourceTest.cs b/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/FakeConfigurationSourceTest.cs new file mode 100644 index 0000000000..394a607680 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/FakeConfigurationSourceTest.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Hosting.Testing.Internal; +using Xunit; + +namespace Microsoft.Extensions.Hosting.Testing.Test; + +public class FakeConfigurationSourceTest +{ + [Fact] + public void Constructor_KeyValuePairsGiven_PopulatesInitialData() + { + var configSource = new FakeConfigurationSource( + new KeyValuePair("testKey", "testValue"), + new KeyValuePair("anotherTestKey", "anotherTestValue")); + + Assert.Collection( + configSource.InitialData!, + item => + { + Assert.Equal("testKey", item.Key); + Assert.Equal("testValue", item.Value); + }, + item => + { + Assert.Equal("anotherTestKey", item.Key); + Assert.Equal("anotherTestValue", item.Value); + }); + } + + [Fact] + public void Constructor_NoKeyValuePairGiven_HasEmptyInitialData() + { + var configSource = new FakeConfigurationSource(); + + Assert.Empty(configSource.InitialData!); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/FakeHostBuilderTest.cs b/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/FakeHostBuilderTest.cs new file mode 100644 index 0000000000..c6dfe56454 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/FakeHostBuilderTest.cs @@ -0,0 +1,204 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting.Testing.Internal; +using Microsoft.Extensions.Hosting.Testing.Test.TestResources; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Hosting.Testing.Test; + +public class FakeHostBuilderTest +{ + private static readonly FakeHostOptions _noFakesOptions = new() + { + FakeLogging = false, + FakeRedaction = false, + ValidateScopes = false, + ValidateOnBuild = false, + }; + + [Fact] + public void Constructor_AddsFakeHostOptions() + { + var hostBuilderServices = new FakeHostBuilder(new FakeHostOptions { }).Build().Services; + + var options = hostBuilderServices.GetRequiredService(); + Assert.NotNull(options); + } + + [Fact] + public void Constructor_AddsHostTerminatorService() + { + var hostBuilderServices = new FakeHostBuilder(new FakeHostOptions()).Build().Services; + Assert.Contains(hostBuilderServices.GetServices(), x => x is HostTerminatorService); + } + + [Fact] + public void Constructor_FakesLogging() + { + var hostBuilderServices = new FakeHostBuilder(new FakeHostOptions()).Build().Services; + + Assert.NotNull(hostBuilderServices.GetService()); + Assert.IsType(hostBuilderServices.GetService()); + } + + [Fact] + public void Constructor_FakeLoggingFalse_DoesNotFakeLogging() + { + var hostBuilderServices = new FakeHostBuilder(new FakeHostOptions { FakeLogging = false }).Build().Services; + + Assert.Null(hostBuilderServices.GetService()); + } + + [Fact] + public void ConfigureHostConfiguration_CallsWrappedInstance() + { + var configurationDelegate = (IConfigurationBuilder _) => { }; + var builderMock = new Mock(); + builderMock.Setup(x => x.ConfigureHostConfiguration(configurationDelegate)).Returns(builderMock.Object); + + var builder = new FakeHostBuilder(builderMock.Object, _noFakesOptions); + var returnedBuilder = builder.ConfigureHostConfiguration(configurationDelegate); + + Assert.Equal(builderMock.Object, returnedBuilder); + } + + [Fact] + public void ConfigureAppConfiguration_CallsWrappedInstance() + { + var configurationDelegate = (HostBuilderContext _, IConfigurationBuilder _) => { }; + var builderMock = new Mock(); + builderMock.Setup(x => x.ConfigureAppConfiguration(configurationDelegate)).Returns(builderMock.Object); + + var builder = new FakeHostBuilder(builderMock.Object, _noFakesOptions); + var returnedBuilder = builder.ConfigureAppConfiguration(configurationDelegate); + + Assert.Equal(builderMock.Object, returnedBuilder); + } + + [Fact] + public void Properties_UsesWrappedInstance() + { + IDictionary properties = new Dictionary(); + var builderMock = new Mock(); + builderMock.SetupGet(x => x.Properties) + .Returns(properties); + + var builder = new FakeHostBuilder(builderMock.Object, _noFakesOptions); + + Assert.Same(properties, builder.Properties); + } + + [Fact] + public void ConfigureContainer_CallsWrappedInstance() + { + var configurationDelegate = (HostBuilderContext _, object _) => { }; + var builderMock = new Mock(); + builderMock.Setup(x => x.ConfigureContainer(configurationDelegate)).Returns(builderMock.Object); + + var builder = new FakeHostBuilder(builderMock.Object, _noFakesOptions); + var returnedBuilder = builder.ConfigureContainer(configurationDelegate); + + Assert.Equal(builderMock.Object, returnedBuilder); + } + + [Fact] + public void UseServiceProviderFactory_CallsWrappedInstance() + { + var factory = new Mock>().Object; + var builderMock = new Mock(); + builderMock.Setup(x => x.UseServiceProviderFactory(factory)) + .Returns(builderMock.Object); + + var builder = new FakeHostBuilder(builderMock.Object, _noFakesOptions); + var returnedBuilder = builder.UseServiceProviderFactory(factory); + + Assert.Equal(builderMock.Object, returnedBuilder); + } + + [Fact] + public void Build_ValidatesScopes() + { + var hostBuilder = FakeHost.CreateBuilder() + .ConfigureServices((_, services) => + { + services.AddScoped() + .AddSingleton(); + }); + + var exception = Record.Exception(() => hostBuilder.Build()); + + Assert.IsType(exception); + Assert.Collection( + ((AggregateException)exception).InnerExceptions, + x => Assert.IsType(x)); + } + + [Fact] + public void Build_ValidateScopesFalse_DoesNotValidateScopes() + { + var hostBuilder = FakeHost.CreateBuilder(x => x.ValidateScopes = false) + .ConfigureServices((_, services) => + { + services.AddScoped() + .AddSingleton(); + }); + + var exception = Record.Exception(() => hostBuilder.Build()); + + Assert.Null(exception); + } + + [Fact] + public void Build_ValidatesDependenciesOnBuild() + { + var hostBuilder = FakeHost.CreateBuilder() + .ConfigureServices((_, services) => + { + services.AddSingleton(); + }); + + var exception = Record.Exception(() => hostBuilder.Build()); + + Assert.IsType(exception); + Assert.Collection( + ((AggregateException)exception).InnerExceptions, + x => Assert.IsType(x)); + } + + [Fact] + public void Build_ValidateOnBuildFalse_DoesNotValidateOnBuild() + { + var hostBuilder = FakeHost.CreateBuilder(x => x.ValidateOnBuild = false) + .ConfigureServices((_, services) => + { + services.AddSingleton(); + }); + + var exception = Record.Exception(() => hostBuilder.Build()); + + Assert.Null(exception); + } + + [Fact] + public void UseNewServiceProviderFactory_CallsWrappedInstance() + { + var factory = new Mock>().Object; + var functor = (HostBuilderContext _) => factory; + var builderMock = new Mock(); + builderMock.Setup(x => x.UseServiceProviderFactory(functor)) + .Returns(builderMock.Object); + + var builder = new FakeHostBuilder(builderMock.Object, _noFakesOptions); + var returnedBuilder = builder.UseServiceProviderFactory(functor); + + Assert.Equal(builderMock.Object, returnedBuilder); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/FakeHostTest.cs b/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/FakeHostTest.cs new file mode 100644 index 0000000000..0722dbf37b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/FakeHostTest.cs @@ -0,0 +1,191 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Microsoft.Extensions.Time.Testing; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Hosting.Testing.Test; + +public class FakeHostTest +{ + [Fact] + public async Task CreateBuilder_AddsFakeLogging() + { + using var host = await FakeHost.CreateBuilder().StartAsync(); + Assert.Contains(host.Services.GetServices(), x => x is FakeLoggerProvider); + } + + [Fact] + public async Task Host_ShutsDownAfterTimeout() + { + using var host = await FakeHost + .CreateBuilder(x => + { + x.FakeRedaction = false; + x.TimeToLive = TimeSpan.Zero; + }) + .StartAsync(); + + Assert.Throws(() => host.Services.GetService()); + } + + [Fact] + public async Task StartAsync_NoTokenProvided_UsesDefaultTimeout() + { + var hostMock = new Mock(MockBehavior.Strict); + hostMock + .Setup(x => x.StartAsync(It.Is(y => y != default))) + .Returns(Task.CompletedTask); + +#pragma warning disable CA2000 + var sut = new FakeHost(hostMock.Object, new FakeHostOptions { StartUpTimeout = TimeSpan.Zero }); +#pragma warning restore CA2000 + await sut.StartAsync(); + await Task.Delay(TimeSpan.FromMilliseconds(100)); + + hostMock.VerifyAll(); + } + + [Fact] + public async Task StartAsync_TokenProvided_Starts() + { + using var tokenSource = new CancellationTokenSource(); + var hostMock = new Mock(MockBehavior.Strict); + hostMock + .Setup(x => x.StartAsync(It.Is(y => y != tokenSource.Token))) + .Returns(Task.CompletedTask); + +#pragma warning disable CA2000 + var sut = new FakeHost(hostMock.Object, new FakeHostOptions { StartUpTimeout = TimeSpan.Zero }); +#pragma warning restore CA2000 + await sut.StartAsync(tokenSource.Token); + hostMock.VerifyAll(); + } + + [Fact] + public void StartAsync_TokenProvided_LinksTheToken() + { + var timeProvider = new FakeTimeProvider(); + var task = new Task(() => { }); + var cancellationTokenSource = timeProvider.CreateCancellationTokenSource(TimeSpan.FromMilliseconds(1)); + CancellationToken receivedToken = default; + var hostMock = new Mock(MockBehavior.Strict); + hostMock + .Setup(x => x.StartAsync(It.Is(y => y != cancellationTokenSource.Token))) + .Callback(x => receivedToken = x) + .Returns(task); + +#pragma warning disable CA2000 + var sut = new FakeHost(hostMock.Object, new FakeHostOptions { StartUpTimeout = TimeSpan.FromMilliseconds(-1) }); +#pragma warning restore CA2000 +#pragma warning disable R9A056 // Fire-and-forget async call inside a 'using' block + _ = sut.StartAsync(cancellationTokenSource.Token); +#pragma warning restore R9A056 // Fire-and-forget async call inside a 'using' block + + Assert.False(receivedToken.IsCancellationRequested); + cancellationTokenSource.Cancel(); + Assert.True(receivedToken.IsCancellationRequested); + + hostMock.VerifyAll(); + + cancellationTokenSource.Dispose(); + } + + [Fact] + public async Task StopAsync_NoTokenProvided_UsesDefaultTimeout() + { + var hostMock = new Mock(MockBehavior.Strict); + hostMock + .Setup(x => x.StopAsync(It.Is(y => y != default))) + .Returns(Task.CompletedTask); + +#pragma warning disable CA2000 + var sut = new FakeHost(hostMock.Object, new FakeHostOptions { ShutDownTimeout = TimeSpan.Zero }); +#pragma warning restore CA2000 + await sut.StopAsync(); + + hostMock.VerifyAll(); + } + + [Fact] + public async Task StopAsync_TokenProvided_Stops() + { + using var tokenSource = new CancellationTokenSource(); + var hostMock = new Mock(MockBehavior.Strict); + hostMock + .Setup(x => x.StopAsync(It.Is(y => y != tokenSource.Token))) + .Returns(Task.CompletedTask); + +#pragma warning disable CA2000 + var sut = new FakeHost(hostMock.Object, new FakeHostOptions { StartUpTimeout = TimeSpan.Zero }); +#pragma warning restore CA2000 + await sut.StopAsync(tokenSource.Token); + hostMock.VerifyAll(); + } + + [Fact] + public void StopAsync_TokenProvided_LinksTheToken() + { + var timeProvider = new FakeTimeProvider(); + var task = new Task(() => { }); + + var cancellationTokenSource = timeProvider.CreateCancellationTokenSource(TimeSpan.FromMilliseconds(1)); + CancellationToken receivedToken = default; + var hostMock = new Mock(MockBehavior.Strict); + hostMock + .Setup(x => x.StopAsync(It.Is(y => y != cancellationTokenSource.Token))) + .Callback(x => receivedToken = x) + .Returns(task); + +#pragma warning disable CA2000 + var sut = new FakeHost(hostMock.Object, new FakeHostOptions { StartUpTimeout = TimeSpan.FromMilliseconds(-1) }); +#pragma warning restore CA2000 +#pragma warning disable R9A056 // Fire-and-forget async call inside a 'using' block + _ = sut.StopAsync(cancellationTokenSource.Token); +#pragma warning restore R9A056 // Fire-and-forget async call inside a 'using' block + + Assert.False(receivedToken.IsCancellationRequested); + cancellationTokenSource.Cancel(); + Assert.True(receivedToken.IsCancellationRequested); + + hostMock.VerifyAll(); + + cancellationTokenSource.Dispose(); + } + + [Fact] + public void Dispose_ShutsDownHost() + { + var hostMock = new Mock(MockBehavior.Strict); + hostMock.Setup(x => x.StopAsync(It.IsAny())).Returns(Task.CompletedTask); + hostMock.Setup(x => x.Dispose()); + + var sut = new FakeHost(hostMock.Object, new FakeHostOptions()) { TimeProvider = new FakeTimeProvider() }; + sut.Dispose(); + + hostMock.VerifyAll(); + } + + [Fact] + public void Dispose_RunsOnlyOnce() + { + var hostMock = new Mock(); + hostMock.Setup(x => x.StopAsync(It.IsAny())).Returns(Task.CompletedTask); + hostMock.Setup(x => x.Dispose()); + + var sut = new FakeHost(hostMock.Object, new FakeHostOptions()) { TimeProvider = new FakeTimeProvider() }; + sut.Dispose(); +#pragma warning disable S3966 + sut.Dispose(); +#pragma warning restore S3966 + + hostMock.Verify(x => x.Dispose(), Times.Once); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/HostTerminatorServiceTest.cs b/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/HostTerminatorServiceTest.cs new file mode 100644 index 0000000000..93a9de03f1 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/HostTerminatorServiceTest.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting.Testing.Internal; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Microsoft.Extensions.Time.Testing; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Hosting.Testing.Test; + +public class HostTerminatorServiceTest +{ + [Fact] + public async Task ExecuteAsync_ServiceCanceled_DoesNothing() + { + var logger = new FakeLogger(); + var hostMock = new Mock(MockBehavior.Strict); + var options = new FakeHostOptions(); + + using var sut = new HostTerminatorService(hostMock.Object, options, logger) { TimeProvider = new FakeTimeProvider() }; + await sut.StartAsync(CancellationToken.None); + await sut.StopAsync(CancellationToken.None); + + Assert.Empty(logger.Collector.GetSnapshot()); + } + + [Fact] + public async Task ExecuteAsync_TimeToLiveUp_LogsAndDisposesHost() + { + var timeProvider = new FakeTimeProvider(); + var logger = new FakeLogger(); + var hostMock = new Mock(MockBehavior.Strict); + hostMock.Setup(x => x.Dispose()); + hostMock.Setup(x => x.StopAsync(It.IsAny())).Returns(Task.CompletedTask); + var options = new FakeHostOptions(); + + using var sut = new HostTerminatorService(hostMock.Object, options, logger) { TimeProvider = timeProvider }; + var task = RunProtectedExecuteAsync(sut, CancellationToken.None); + timeProvider.Advance(options.TimeToLive); + + await task; + + Assert.Equal( + "FakeHostOptions.TimeToLive set to 00:00:30 is up, disposing the host.", + logger.LatestRecord.Message); + hostMock.VerifyAll(); + } + + [Fact] + public async Task ExecuteAsync_DebuggerAttached_DoesNothing() + { + var logger = new FakeLogger(); + var hostMock = new Mock(MockBehavior.Strict); + var options = new FakeHostOptions(); + + using var sut = new HostTerminatorService(hostMock.Object, options, logger) { DebuggerAttached = true }; + await RunProtectedExecuteAsync(sut, CancellationToken.None); + + Assert.Equal( + "Debugger is attached. The host won't be automatically disposed.", + logger.LatestRecord.Message); + hostMock.VerifyAll(); + } + + private static Task RunProtectedExecuteAsync(HostTerminatorService instance, CancellationToken cancellationToken) + { + var methodInfo = typeof(HostTerminatorService) + .GetMethod("ExecuteAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; + return (Task)methodInfo.Invoke(instance, new object[] { cancellationToken })!; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/HostingFakesExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/HostingFakesExtensionsTest.cs new file mode 100644 index 0000000000..94371b6b25 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/HostingFakesExtensionsTest.cs @@ -0,0 +1,353 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting.Testing.Internal; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Hosting.Testing.Test; + +public class HostingFakesExtensionsTest +{ + [Fact] + public async Task StartAndStop_NullGiven_Throws() + { + var exception = await Record.ExceptionAsync(() => ((IHostedService)null!).StartAndStopAsync()); + Assert.IsType(exception); + } + + [Fact] + public async Task StartAndStop_ServiceGiven_StartsAndStopsTheService() + { + using var tokenSource = new CancellationTokenSource(); + + var serviceMock = new Mock(MockBehavior.Strict); + serviceMock.Setup(x => x.StartAsync(tokenSource.Token)).Returns(Task.CompletedTask); + serviceMock.Setup(x => x.StopAsync(tokenSource.Token)).Returns(Task.CompletedTask); + + await serviceMock.Object.StartAndStopAsync(tokenSource.Token); + + serviceMock.VerifyAll(); + } + + [Fact] + public async Task GetFakeLogCollector_FetchesGetFakeLogCollector() + { + using var host = await FakeHost.CreateBuilder().StartAsync(); + Assert.NotNull(host.GetFakeLogCollector()); + } + + [Fact] + public void GetFakeLogCollector_FakeCollectorMissing_ThrowsException() + { + using var host = new HostBuilder().Build(); + + var exception = Record.Exception(() => host.GetFakeLogCollector()); + + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("No fake log collector registered", exception.Message); + } + + [Fact] + public async Task GetFakeRedactionCollector_FetchesFakeRedactionCollector() + { + using var host = await FakeHost.CreateBuilder().StartAsync(); + + var collector = host.GetFakeRedactionCollector(); + + Assert.NotNull(collector); + } + + [Fact] + public void GetFakeRedactionCollector_FakeCollectorMissing_ThrowsException() + { + using var host = new HostBuilder().Build(); + + var exception = Record.Exception(() => host.GetFakeRedactionCollector()); + + Assert.NotNull(exception); + Assert.IsType(exception); + } + + [Fact] + public void Configure_delegateUsed_ConfiguresGivenBuilder() + { + var builderMock = new Mock(); + + var returnedBuilder = builderMock.Object.Configure(builder => builder.Build()); + + Assert.Equal(builderMock.Object, returnedBuilder); + builderMock.Verify(mock => mock.Build(), Times.Once); + } + + [Fact] + public void Configure_BuilderIsNull_Throws() + { + var exception = Record.Exception(() => ((IHostBuilder)null!).Configure(_ => { })); + Assert.IsType(exception); + } + + [Fact] + public void Configure_ConfigureDelegateIsNull_Throws() + { + var exception = Record.Exception(() => new HostBuilder().Configure(null!)); + Assert.IsType(exception); + } + + [Fact] + public void Configure_nullDelegate_Throws() + { + var builderMock = new Mock(); + + var exception = Record.Exception(() => builderMock.Object.Configure(null!)); + Assert.IsType(exception); + } + + [Fact] + public void ConfigureAppConfiguration_ReturnsBuilder() + { + var builderMock = new Mock(); + builderMock + .Setup(x => x.ConfigureAppConfiguration(It.IsAny>())) + .Returns(builderMock.Object); + + var returnedBuilder = builderMock.Object.ConfigureAppConfiguration("testKey", "testValue"); + + Assert.Equal(builderMock.Object, returnedBuilder); + } + + [Fact] + public void ConfigureAppConfiguration_KeyAndValueGiven_AddsToConfiguration() + { + var configBuilder = new ConfigurationBuilder(); + configBuilder.Sources.Add(new ChainedConfigurationSource()); + + var builderMock = CreateHostBuilderMock(appConfigBuilder: configBuilder); + + _ = builderMock.Object.ConfigureAppConfiguration("testKey", "testValue"); + + Assert.Collection( + configBuilder.Sources, + source => Assert.IsType(source), + source => + { + Assert.IsType(source); + Assert.Collection( + ((FakeConfigurationSource)source).InitialData!, + item => + { + Assert.Equal("testKey", item.Key); + Assert.Equal("testValue", item.Value); + }); + }); + } + + [Fact] + public void ConfigureAppConfiguration_MultipleKeyAndValueGiven_AddsToConfiguration() + { + var configBuilder = new ConfigurationBuilder(); + configBuilder.Sources.Add(new ChainedConfigurationSource()); + + var builderMock = CreateHostBuilderMock(appConfigBuilder: configBuilder); + + _ = builderMock.Object.ConfigureAppConfiguration(("testKey1", "testValue1"), ("testKey2", "testValue2")); + + Assert.Collection( + configBuilder.Sources, + source => Assert.IsType(source), + source => + { + Assert.IsType(source); + Assert.Collection( + ((FakeConfigurationSource)source).InitialData!, + item => + { + Assert.Equal("testKey1", item.Key); + Assert.Equal("testValue1", item.Value); + }, + item => + { + Assert.Equal("testKey2", item.Key); + Assert.Equal("testValue2", item.Value); + }); + }); + } + + [Fact] + public void ConfigureAppConfiguration_MultipleKeyAndValueGiven_AddsOnlyOneConfigurationSource() + { + var configBuilder = new ConfigurationBuilder(); + var builderMock = CreateHostBuilderMock(appConfigBuilder: configBuilder); + + _ = builderMock.Object.ConfigureAppConfiguration("testKey", "testValue"); + _ = builderMock.Object.ConfigureAppConfiguration("anotherTestKey", "anotherTestValue"); + + Assert.Collection( + configBuilder.Sources, + source => + { + Assert.IsType(source); + Assert.Collection( + ((FakeConfigurationSource)source).InitialData!, + item => + { + Assert.Equal("testKey", item.Key); + Assert.Equal("testValue", item.Value); + }, + item => + { + Assert.Equal("anotherTestKey", item.Key); + Assert.Equal("anotherTestValue", item.Value); + }); + }); + } + + [Fact] + public void ConfigureHostConfiguration_KeyAndValueGiven_AddsToConfiguration() + { + var configBuilder = new ConfigurationBuilder(); + configBuilder.Sources.Add(new ChainedConfigurationSource()); + + var builderMock = CreateHostBuilderMock(hostConfigBuilder: configBuilder); + + _ = builderMock.Object.ConfigureHostConfiguration("testKey", "testValue"); + + Assert.Collection( + configBuilder.Sources, + source => Assert.IsType(source), + source => + { + Assert.IsType(source); + Assert.Collection( + ((FakeConfigurationSource)source).InitialData!, + item => + { + Assert.Equal("testKey", item.Key); + Assert.Equal("testValue", item.Value); + }); + }); + } + + [Fact] + public void ConfigureHostConfiguration_MultipleEntriesGiven_AddsToConfiguration() + { + var configBuilder = new ConfigurationBuilder(); + configBuilder.Sources.Add(new ChainedConfigurationSource()); + + var builderMock = CreateHostBuilderMock(hostConfigBuilder: configBuilder); + + _ = builderMock.Object.ConfigureHostConfiguration(("testKey1", "testValue1"), ("testKey2", "testValue2")); + + Assert.Collection( + configBuilder.Sources, + source => Assert.IsType(source), + source => + { + Assert.IsType(source); + Assert.Collection( + ((FakeConfigurationSource)source).InitialData!, + item => + { + Assert.Equal("testKey1", item.Key); + Assert.Equal("testValue1", item.Value); + }, + item => + { + Assert.Equal("testKey2", item.Key); + Assert.Equal("testValue2", item.Value); + }); + }); + } + + [Fact] + public void ConfigureHostConfiguration_MultipleKeyAndValueGiven_AddsOnlyOneConfigurationSource() + { + var configBuilder = new ConfigurationBuilder(); + var builderMock = CreateHostBuilderMock(hostConfigBuilder: configBuilder); + + _ = builderMock.Object.ConfigureHostConfiguration("testKey", "testValue"); + _ = builderMock.Object.ConfigureHostConfiguration("anotherTestKey", "anotherTestValue"); + + Assert.Collection( + configBuilder.Sources, + source => + { + Assert.IsType(source); + Assert.Collection( + ((FakeConfigurationSource)source).InitialData!, + item => + { + Assert.Equal("testKey", item.Key); + Assert.Equal("testValue", item.Value); + }, + item => + { + Assert.Equal("anotherTestKey", item.Key); + Assert.Equal("anotherTestValue", item.Value); + }); + }); + } + + [Fact] + public void AddLoggingCallback_NullCallback_Throws() + { + Assert.Throws(() => FakeHost.CreateBuilder().AddFakeLoggingOutputSink(null!)); + } + + [Fact] + public void AddLoggingCallback_NullHostBuilder_Throws() + { + Assert.Throws(() => ((IHostBuilder)null!).AddFakeLoggingOutputSink(_ => { })); + } + + [Fact] + public async Task AddLoggingCallback_CallbackUsed_AddsCallback() + { + var message = Guid.NewGuid().ToString(); + var firstCallbackTarget = new List(); + var secondCallbackTarget = new List(); + using var host = await FakeHost.CreateBuilder() + .AddFakeLoggingOutputSink(firstCallbackTarget.Add) + .AddFakeLoggingOutputSink(secondCallbackTarget.Add) + .StartAsync(); + + var logger = host.Services.GetRequiredService(); + logger.LogWarning(message); + + Assert.Contains(firstCallbackTarget, record => record.Contains(message)); + Assert.Contains(secondCallbackTarget, record => record.Contains(message)); + } + + private static Mock CreateHostBuilderMock( + IConfigurationBuilder? appConfigBuilder = null, + IConfigurationBuilder? hostConfigBuilder = null) + { + var builderMock = new Mock(); + + if (appConfigBuilder is not null) + { + builderMock + .Setup(x => x.ConfigureAppConfiguration(It.IsAny>())) + .Returns(builderMock.Object) + .Callback>(configure => configure(null, appConfigBuilder)); + } + + if (hostConfigBuilder is not null) + { + builderMock + .Setup(x => x.ConfigureHostConfiguration(It.IsAny>())) + .Returns(builderMock.Object) + .Callback>(configure => configure(hostConfigBuilder)); + } + + return builderMock; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/Microsoft.Extensions.Hosting.Testing.Tests.csproj b/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/Microsoft.Extensions.Hosting.Testing.Tests.csproj new file mode 100644 index 0000000000..29cc6ceca2 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/Microsoft.Extensions.Hosting.Testing.Tests.csproj @@ -0,0 +1,12 @@ + + + Microsoft.Extensions.Hosting.Test + Unit tests for Microsoft.Extensions.Hosting.Testing. + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/TestResources/DependentClass.cs b/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/TestResources/DependentClass.cs new file mode 100644 index 0000000000..d9fea899c2 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/TestResources/DependentClass.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Hosting.Testing.Test.TestResources; + +public class DependentClass +{ + public DependentClass(FakeHostTest _) + { + } +} diff --git a/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/TestResources/InnerClass.cs b/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/TestResources/InnerClass.cs new file mode 100644 index 0000000000..01b4061935 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/TestResources/InnerClass.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Hosting.Testing.Test.TestResources; + +public class InnerClass +{ +} diff --git a/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/TestResources/OuterClass.cs b/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/TestResources/OuterClass.cs new file mode 100644 index 0000000000..26e06ee360 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Hosting.Testing.Tests/TestResources/OuterClass.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Hosting.Testing.Test.TestResources; + +public class OuterClass +{ + public OuterClass(InnerClass _) + { + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/InterfaceAttributesTests.cs b/test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/InterfaceAttributesTests.cs new file mode 100644 index 0000000000..120c570938 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/InterfaceAttributesTests.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Http.AutoClient.Test; + +public class InterfaceAttributesTests +{ + [Fact] + public void StaticHeaderAttributeNameAndValue() + { + var a = new StaticHeaderAttribute("HeaderName", "value"); + Assert.Equal("HeaderName", a.Header); + Assert.Equal("value", a.Value); + } + + [Fact] + public void RestApiAttributeClientNameAndDependencyName() + { + var a = new AutoClientAttribute("MyClient"); + Assert.Equal("MyClient", a.HttpClientName); + + a = new AutoClientAttribute("MyClient1", "MyDependency"); + Assert.Equal("MyClient1", a.HttpClientName); + Assert.Equal("MyDependency", a.CustomDependencyName); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/MethodAttributesTests.cs b/test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/MethodAttributesTests.cs new file mode 100644 index 0000000000..e8c069d723 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/MethodAttributesTests.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Http.AutoClient.Test +{ + public class MethodAttributesTests + { + [Fact] + public void DeleteAttributePath() + { + var a = new DeleteAttribute("some-path"); + Assert.Equal("some-path", a.Path); + } + + [Fact] + public void GetAttributePath() + { + var a = new GetAttribute("some-path"); + Assert.Equal("some-path", a.Path); + } + + [Fact] + public void HeadAttributePath() + { + var a = new HeadAttribute("some-path"); + Assert.Equal("some-path", a.Path); + } + + [Fact] + public void OptionsAttributePath() + { + var a = new OptionsAttribute("some-path"); + Assert.Equal("some-path", a.Path); + } + + [Fact] + public void PatchAttributePath() + { + var a = new PatchAttribute("some-path"); + Assert.Equal("some-path", a.Path); + } + + [Fact] + public void PostAttributePath() + { + var a = new PostAttribute("some-path"); + Assert.Equal("some-path", a.Path); + } + + [Fact] + public void PutAttributePath() + { + var a = new PutAttribute("some-path"); + Assert.Equal("some-path", a.Path); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/Microsoft.Extensions.Http.AutoClient.Tests.csproj b/test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/Microsoft.Extensions.Http.AutoClient.Tests.csproj new file mode 100644 index 0000000000..d71eadd725 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/Microsoft.Extensions.Http.AutoClient.Tests.csproj @@ -0,0 +1,14 @@ + + + Microsoft.Extensions.Http.AutoClient.Test + Unit tests for Microsoft.Extensions.Http.AutoClient. + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/ParameterAttributesTests.cs b/test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/ParameterAttributesTests.cs new file mode 100644 index 0000000000..9a4279d3ac --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/ParameterAttributesTests.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Http.AutoClient.Test; + +public class ParameterAttributesTests +{ + [Fact] + public void BodyAttributeContentType() + { + var a = new BodyAttribute(); + Assert.Equal(BodyContentType.ApplicationJson, a.ContentType); + + a = new BodyAttribute(BodyContentType.TextPlain); + Assert.Equal(BodyContentType.TextPlain, a.ContentType); + } + + [Fact] + public void HeaderAttributeName() + { + var a = new HeaderAttribute("HeaderName"); + Assert.Equal("HeaderName", a.Header); + } + + [Fact] + public void QueryAttributeNameOrNull() + { + var a = new QueryAttribute(); + Assert.Null(a.Key); + + a = new QueryAttribute("QueryKey"); + Assert.Equal("QueryKey", a.Key); + } + + [Fact] + public void RequestNameAttributeValue() + { + var a = new RequestNameAttribute("RequestName"); + Assert.Equal("RequestName", a.Value); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/RestApiExceptionTests.cs b/test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/RestApiExceptionTests.cs new file mode 100644 index 0000000000..33fe291565 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.AutoClient.Tests/RestApiExceptionTests.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.Http.AutoClient.Test; + +public class RestApiExceptionTests +{ + [Fact] + public async Task RestApiExceptionConstructor() + { + var e = new AutoClientException("Message", "api/users/{userId}"); + Assert.Equal("Message", e.Message); + Assert.Null(e.StatusCode); + Assert.Null(e.HttpError); + + var response = new HttpResponseMessage(HttpStatusCode.BadRequest) + { + ReasonPhrase = "Reason", + Content = new StringContent("someContent") + }; + + var error = await AutoClientHttpError.CreateAsync(response, default); + + e = new AutoClientException("Message", "api/users/{userId}", error); + var contentHeaders = response.Content.Headers; + response.Dispose(); + Assert.Equal("Message", e.Message); + Assert.Equal(400, e.StatusCode); + Assert.Equal(400, e.HttpError!.StatusCode); + Assert.Equal("someContent", e.HttpError!.RawContent); + Assert.Equal(response.ReasonPhrase, e.HttpError!.ReasonPhrase); + Assert.Equal("api/users/{userId}", e.Path); + + foreach (var responseHeader in response.Headers) + { + Assert.Equal(responseHeader.Value, e.HttpError!.ResponseHeaders[responseHeader.Key]); + } + + foreach (var contentHeader in response.Content.Headers) + { + Assert.Equal(contentHeader.Value, e.HttpError!.ResponseHeaders[contentHeader.Key]); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/FallbackClientHandlerOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/FallbackClientHandlerOptionsTests.cs new file mode 100644 index 0000000000..0fd129a545 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/FallbackClientHandlerOptionsTests.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test; + +public class FallbackClientHandlerOptionsTests +{ + private readonly FallbackClientHandlerOptions _testObject; + + public FallbackClientHandlerOptionsTests() + { + _testObject = new FallbackClientHandlerOptions(); + } + + [Fact] + public void Constructor_ShouldInitialize() + { + var instance = new FallbackClientHandlerOptions(); + Assert.NotNull(instance); + } + + [Fact] + public void FallbackPolicyOptions_ShouldGetAndSet() + { + var testValue = new HttpFallbackPolicyOptions + { + ShouldHandleResultAsError = response => !response.IsSuccessStatusCode + }; + + _testObject.FallbackPolicyOptions = testValue; + Assert.Equal(testValue, _testObject.FallbackPolicyOptions); + } + + [Fact] + public void BaseFallbackUri_ShouldGetAndSet() + { + var testValue = new Uri("https://lalalala.com"); + + _testObject.BaseFallbackUri = testValue; + Assert.Equal(testValue, _testObject.BaseFallbackUri); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Helpers/ConfigurationStubFactory.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Helpers/ConfigurationStubFactory.cs new file mode 100644 index 0000000000..6ba9e9b5a5 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Helpers/ConfigurationStubFactory.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Extensions.Http.Resilience.Tests.Helpers; + +public sealed class ConfigurationStubFactory +{ + public static IConfiguration Create(Dictionary collection) + { + return new ConfigurationBuilder() + .AddInMemoryCollection(collection) + .Build(); + } + + public static IConfiguration CreateEmpty() + { + return new ConfigurationBuilder().Build(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Helpers/OptionsUtilities.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Helpers/OptionsUtilities.cs new file mode 100644 index 0000000000..dc7f7df03b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Helpers/OptionsUtilities.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; + +namespace Microsoft.Extensions.Http.Resilience.Test; + +internal static class OptionsUtilities +{ + public static void ValidateOptions(object options) + { + var context = new ValidationContext(options); + Validator.ValidateObject(options, context, true); + } + + public static bool EqualOptions(T options1, T options2) + { + if (options1 is null && options2 is null) + { + return true; + } + + if (options1 is null || options2 is null) + { + return false; + } + + var propertiesValuesByName1 = options1.GetPropertiesValuesByName(); + var propertiesValuesByName2 = options2.GetPropertiesValuesByName(); + + foreach (var propertyDefinition1 in propertiesValuesByName1) + { + var propertyName = propertyDefinition1.Key; + var propertyValue1 = propertyDefinition1.Value; + + if (!propertiesValuesByName2.TryGetValue(propertyName, out var propertyValue2) || + !Equals(propertyValue1, propertyValue2)) + { + return false; + } + } + + return true; + } + + private static IDictionary GetPropertiesValuesByName(this T options) + { + return options! + .GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .GroupBy(property => property.Name) + .ToDictionary( + propertyGroup => propertyGroup.Key, + propertyGroup => propertyGroup.Last().GetValue(options)!); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.BySelector.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.BySelector.cs new file mode 100644 index 0000000000..e27521c641 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.BySelector.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Resilience; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test; + +public sealed partial class HttpClientBuilderExtensionsTests +{ + [InlineData(true, "https://dummy:21/path", "https://dummy:21")] + [InlineData(true, "https://dummy", "https://dummy")] + [InlineData(false, "https://dummy:21/path", "https://dummy:21")] + [InlineData(false, "https://dummy", "https://dummy")] + [Theory] + public void SelectPipelineByAuthority_Ok(bool standardResilience, string url, string expectedPipelineKey) + { + _builder.Services.AddFakeRedaction(); + + var pipelineName = standardResilience ? + _builder.AddStandardResilienceHandler().SelectPipelineByAuthority(DataClassification.Unknown).PipelineName : + _builder.AddResilienceHandler("dummy").SelectPipelineByAuthority(DataClassification.Unknown).AddRetryPolicy("test").PipelineName; + + var provider = _builder.Services.BuildServiceProvider().GetPipelineKeyProvider(pipelineName)!; + + using var request = new HttpRequestMessage(HttpMethod.Head, url); + + var key = provider.GetPipelineKey(request); + + Assert.Equal(expectedPipelineKey, key); + Assert.Same(provider.GetPipelineKey(request), provider.GetPipelineKey(request)); + } + + [Fact] + public void SelectPipelineByAuthority_Ok_NullURL_Throws() + { + _builder.Services.AddFakeRedaction(); + var builder = _builder.AddResilienceHandler("dummy").SelectPipelineByAuthority(DataClassification.Unknown).AddRetryPolicy("test"); + var provider = PipelineKeyProviderHelper.GetPipelineKeyProvider(builder.Services.BuildServiceProvider(), builder.PipelineName)!; + + using var request = new HttpRequestMessage(); + + Assert.Throws(() => provider.GetPipelineKey(request)); + } + + [Fact] + public void SelectPipelineByAuthority_ErasingRedactor_InvalidOperationException() + { + _builder.Services.AddRedaction(); + var builder = _builder.AddResilienceHandler("dummy").SelectPipelineByAuthority(SimpleClassifications.PrivateData).AddRetryPolicy("test"); + var provider = PipelineKeyProviderHelper.GetPipelineKeyProvider(builder.Services.BuildServiceProvider(), builder.PipelineName)!; + + using var request = new HttpRequestMessage(HttpMethod.Get, "https://dummy"); + + Assert.Throws(() => provider.GetPipelineKey(request)); + } + + [InlineData(true, "https://dummy:21/path", "https://")] + [InlineData(true, "https://dummy", "https://")] + [InlineData(false, "https://dummy:21/path", "https://")] + [InlineData(false, "https://dummy", "https://")] + [Theory] + public void SelectPipelineBy_Ok(bool standardResilience, string url, string expectedPipelineKey) + { + _builder.Services.AddFakeRedaction(); + + string? pipelineName = null; + + if (standardResilience) + { + pipelineName = _builder + .AddResilienceHandler("dummy") + .SelectPipelineBy(_ => r => r.RequestUri!.GetLeftPart(UriPartial.Scheme)) + .AddRetryPolicy("test").PipelineName; + } + else + { + pipelineName = _builder + .AddStandardResilienceHandler() + .SelectPipelineBy(_ => r => r.RequestUri!.GetLeftPart(UriPartial.Scheme)).PipelineName; + } + + var provider = _builder.Services.BuildServiceProvider().GetPipelineKeyProvider(pipelineName)!; + + using var request = new HttpRequestMessage(HttpMethod.Head, url); + + var key = provider.GetPipelineKey(request); + + Assert.Equal(expectedPipelineKey, key); + Assert.NotSame(provider.GetPipelineKey(request), provider.GetPipelineKey(request)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.Fallback.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.Fallback.cs new file mode 100644 index 0000000000..f345d27f96 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.Fallback.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test; + +public sealed partial class HttpClientBuilderExtensionsTests +{ + private static readonly Uri _defaultFallbackUri = new("Http://dummy.uri"); + + [Fact] + public async Task BuildHostBuilder_WithConfigureFallbackOptions_ShouldNotThrow() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddHttpClient("client", client => client.BaseAddress = new Uri("http://localhost:8080/")) + .AddFallbackHandler(opts => opts.BaseFallbackUri = new Uri("http://localhost:9090"))) + .Build(); + + // Start host. + var startTask = host.RunAsync(); + + // When start logic is complete, stop it. + await host.StopAsync(); + + // Await, so the task becomes completed and assert. + await startTask; + Assert.True(startTask.IsCompleted); + } + + [InlineData(Args.Configure)] + [InlineData(Args.Section)] + [InlineData(Args.ConfigureAndSection)] + [Theory] + public void AddFallbackHandler_InvalidConfiguration_OptionsValidationException(Args args) + { + AddFallbackHandler(args, new ConfigurationBuilder().Build().GetSection(""), options => { }); + + Assert.Throws(() => CreateClient()); + } + + [InlineData(Args.Configure)] + [InlineData(Args.Section)] + [InlineData(Args.ConfigureAndSection)] + [Theory] + public void AddFallbackHandler_ValidConfiguration_OptionsValidationException(Args args) + { + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(new Dictionary + { + { "section:BaseFallbackUri", _defaultFallbackUri.ToString() } + }); + var section = builder.Build().GetSection("section"); + + var result = AddFallbackHandler(args, section, options => options.BaseFallbackUri = _defaultFallbackUri); + + Assert.NotNull(CreateClient()); + } + + [Fact] + public async Task AddFallbackHandler_EnsureWorksCorrectly() + { + var urls = new List(); + using var testHandler = new TestHandler + { + ResponseFactory = r => + { + urls.Add(r.RequestUri!); + + if (urls.Count == 1) + { + return new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError); + } + else + { + return new HttpResponseMessage(System.Net.HttpStatusCode.OK); + } + } + }; + + _builder.AddFallbackHandler(options => options.BaseFallbackUri = _defaultFallbackUri); + _builder.AddHttpMessageHandler(() => testHandler); + + var client = CreateClient(); + + var response = await client.GetAsync("https://dummy-host/path"); + + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.Equal(2, urls.Count); + Assert.Equal("https://dummy-host/path", urls[0].ToString()); + Assert.Equal($"{_defaultFallbackUri}path", urls[1].ToString()); + } + + private System.Net.Http.HttpClient CreateClient() => _builder.Services.BuildServiceProvider().GetRequiredService().CreateClient(BuilderName); + + private IHttpClientBuilder AddFallbackHandler(Args args, IConfigurationSection? section, Action? configure) + { + return args switch + { + Args.Section => _builder.AddFallbackHandler(section!), + Args.Configure => _builder.AddFallbackHandler(configure!), + Args.ConfigureAndSection => _builder.AddFallbackHandler(section!, configure!), + _ => throw new NotSupportedException(), + }; + } + + public enum Args + { + Section, + Configure, + ConfigureAndSection + } + + private class TestHandler : DelegatingHandler + { + public Func? ResponseFactory { get; set; } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(ResponseFactory!.Invoke(request)); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.Resilience.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.Resilience.cs new file mode 100644 index 0000000000..21e1adf191 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.Resilience.cs @@ -0,0 +1,250 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Resilience; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Extensions.Telemetry.Metering; +using Moq; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test; + +public sealed partial class HttpClientBuilderExtensionsTests +{ + private const string DefaultPolicyName = "dummy-policy-name"; + + [Fact] + public void AddResilienceHandler_ArgumentValidation() + { + var services = new ServiceCollection(); + IHttpClientBuilder? builder = services.AddHttpClient("client"); + + Assert.Throws(() => builder.AddResilienceHandler(null!)); + Assert.Throws(() => builder.AddResilienceHandler(string.Empty)); + + builder = null; + Assert.Throws(() => builder!.AddResilienceHandler("pipeline-name")); + } + + [Fact] + public void AddResilienceHandler_EnsureOtherPipelineDefaultsNotAffected() + { + var called = false; + var services = new ServiceCollection().RegisterMetering().AddLogging(); + + services.ConfigureAll>(options => + { + Assert.NotEqual(HttpClientResiliencePredicates.IsTransientHttpFailure, options.ShouldHandleResultAsError); + }); + + services + .AddHttpClient("client") + .AddResilienceHandler("test").AddRetryPolicy(DefaultPolicyName); + + services + .AddResiliencePipeline("test2") + .AddRetryPolicy( + DefaultPolicyName, + options => + { + Assert.NotEqual(HttpClientResiliencePredicates.IsTransientHttpFailure, options.ShouldHandleResultAsError); + called = true; + }); + + var provider = services.BuildServiceProvider(); + var pipelineProvider = provider.GetRequiredService(); + + pipelineProvider.GetPipeline("client-test"); + pipelineProvider.GetPipeline("test2"); + + Assert.True(called); + } + + [Fact] + public void AddResilienceHandler_EnsureCorrectServicesRegistered() + { + var services = new ServiceCollection(); + IHttpClientBuilder? builder = services.AddHttpClient("client"); + + builder.AddResilienceHandler("test"); + + // add twice intentionally + builder.AddResilienceHandler("test"); + + Assert.Contains(services, s => s.ServiceType == typeof(IPolicyFactory)); + } + + public enum PolicyType + { + Fallback, + Retry, + CircuitBreaker, + } + + [InlineData(PolicyType.Fallback)] + [InlineData(PolicyType.CircuitBreaker)] + [InlineData(PolicyType.Retry)] + [Theory] + public async Task AddResilienceHandler_IndividialPolicies_EnsureProperDelegatesRegistered(PolicyType policyType) + { + // arrange + var called = false; + var services = new ServiceCollection().AddLogging().RegisterMetering(); + var builder = services.AddHttpClient("client"); + + ConfigureAndAssertPolicies(policyType, builder.AddResilienceHandler("test-pipeline"), () => called = true); + + builder.AddHttpMessageHandler(() => new TestHandlerStub(HttpStatusCode.OK)); + services.ConfigureHttpFailureResultContext(); + + var provider = services.BuildServiceProvider(); + var client = provider.GetRequiredService().CreateClient("client"); + var pipelineProvider = provider.GetRequiredService(); + + // act + await client.GetAsync("https://dummy"); + + // assert + Assert.True(called); + + Assert.NotNull(pipelineProvider.GetPipeline("client-test-pipeline")); + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task AddResilienceHandler_EnsureProperPipelineInstanceRetrieved(bool bySelector) + { + // arrange + var resilienceProvider = new Mock(MockBehavior.Strict); + var services = new ServiceCollection().AddLogging().RegisterMetering().AddFakeRedaction(); + services.AddSingleton(resilienceProvider.Object); + var builder = services.AddHttpClient("client"); + var pipelineBuilder = builder.AddResilienceHandler("dummy"); + var expectedPipelineName = "client-dummy"; + if (bySelector) + { + pipelineBuilder.SelectPipelineByAuthority(DataClassification.Unknown); + } + + pipelineBuilder.AddRetryPolicy("test"); + builder.AddHttpMessageHandler(() => new TestHandlerStub(HttpStatusCode.OK)); + + var provider = services.BuildServiceProvider(); + if (bySelector) + { + resilienceProvider.Setup(v => v.GetPipeline(expectedPipelineName, "https://dummy1")).Returns(Policy.NoOpAsync()); + } + else + { + resilienceProvider.Setup(v => v.GetPipeline(expectedPipelineName)).Returns(Policy.NoOpAsync()); + } + + var client = provider.GetRequiredService().CreateClient("client"); + + // act + await client.GetAsync("https://dummy1"); + + // assert + resilienceProvider.VerifyAll(); + } + + [Fact] + public async Task AddResilienceHandlerBySelector_EnsurePolicyProviderCalled() + { + // arrange + var services = new ServiceCollection().AddLogging().RegisterMetering(); + var providerMock = new Mock(MockBehavior.Strict); + services.AddSingleton(providerMock.Object); + var pipelineName = string.Empty; + + pipelineName = "client-my-pipeline"; + var clientBuilder = services.AddHttpClient("client"); + clientBuilder + .AddResilienceHandler("my-pipeline") + .AddRetryPolicy(DefaultPolicyName); + clientBuilder.AddHttpMessageHandler(() => new TestHandlerStub(HttpStatusCode.OK)); + + providerMock + .Setup(v => v.GetPipeline(pipelineName)) + .Returns(Policy.NoOpAsync()) + .Verifiable(); + + var provider = services.BuildServiceProvider(); + var client = provider.GetRequiredService().CreateClient("client"); + var pipelineProvider = provider.GetRequiredService(); + + // act + await client.GetAsync("https://dummy1"); + + // assert + providerMock.VerifyAll(); + } + + [Fact] + public void AddResilienceHandler_AuthoritySelectorAndNotConfiguredRedaction_EnsureValidated() + { + // arrange + var clientBuilder = new ServiceCollection().AddLogging().RegisterMetering().AddRedaction() + .AddHttpClient("my-client") + .AddResilienceHandler("my-pipeline") + .SelectPipelineByAuthority(SimpleClassifications.PrivateData) + .AddRetryPolicy(DefaultPolicyName); + + var factory = clientBuilder.Services.BuildServiceProvider().GetRequiredService(); + + var error = Assert.Throws(() => factory.CreateClient("my-client")); + Assert.Equal("The redacted pipeline is an empty string and cannot be used for pipeline selection. Is redaction correctly configured?", error.Message); + } + + [Fact] + public void AddResilienceHandler_AuthorityByCustomSelector_NotValidated() + { + // arrange + var clientBuilder = new ServiceCollection().AddLogging().RegisterMetering().AddRedaction() + .AddHttpClient("my-client") + .AddResilienceHandler("my-pipeline") + .SelectPipelineBy(_ => _ => string.Empty) + .AddRetryPolicy(DefaultPolicyName); + + var factory = clientBuilder.Services.BuildServiceProvider().GetRequiredService(); + + Assert.NotNull(factory.CreateClient("my-client")); + } + + private static void ConfigureAndAssertPolicies(PolicyType policyType, IResiliencePipelineBuilder builder, Action onCalled) + { + var optionsName = $"{builder.PipelineName}-{policyType}-{DefaultPolicyName}"; + + if (policyType == PolicyType.Fallback) + { + builder.AddFallbackPolicy( + DefaultPolicyName, + args => Task.FromResult(new HttpResponseMessage()), + options => onCalled()); + } + else if (policyType == PolicyType.Retry) + { + builder.AddRetryPolicy(DefaultPolicyName, options => onCalled()); + } + else if (policyType == PolicyType.CircuitBreaker) + { + builder.AddCircuitBreakerPolicy(DefaultPolicyName, options => onCalled()); + } + else + { + throw new NotSupportedException(); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.Standard.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.Standard.cs new file mode 100644 index 0000000000..0aa8b2cd71 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.Standard.cs @@ -0,0 +1,195 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using FluentAssertions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.Tests.Helpers; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience; +using Microsoft.Extensions.Telemetry.Metering; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test; + +public sealed partial class HttpClientBuilderExtensionsTests +{ + private const string BuilderName = "Name"; + private readonly IHttpClientBuilder _builder; + + public HttpClientBuilderExtensionsTests() + { + _builder = new ServiceCollection().AddHttpClient(BuilderName); + _builder.Services.RegisterMetering(); + _builder.Services.AddLogging(); + } + + private static readonly IConfigurationSection _validConfigurationSection = + ConfigurationStubFactory.Create( + new Dictionary + { + { "StandardResilienceOptions:CircuitBreakerOptions:FailureThreshold", "0.1"}, + { "StandardResilienceOptions:AttemptTimeoutOptions:TimeoutInterval", "00:00:05"}, + { "StandardResilienceOptions:TotalRequestTimeoutOptions:TimeoutInterval", "00:00:20"}, + }) + .GetSection("StandardResilienceOptions"); + + private static readonly IConfigurationSection _invalidConfigurationSection = + ConfigurationStubFactory.Create( + new Dictionary + { + { "StandardResilienceOptions:CircuitBreakerOptionsTypo:FailureThreshold", "0.1"} + }) + .GetSection("StandardResilienceOptions"); + + private static readonly IConfigurationSection _emptyConfigurationSection = + ConfigurationStubFactory.CreateEmpty().GetSection(string.Empty); + + [Flags] + public enum MethodArgs + { + None = 0, + + ConfigureMethod = 1 << 0, + + ConfigureMethodWithServiceProvider = 1 << 1, + + Configuration = 1 << 2, + + Builder = 1 << 3, + } + + [InlineData(MethodArgs.None)] + [InlineData(MethodArgs.ConfigureMethod)] + [InlineData(MethodArgs.Configuration)] + [InlineData(MethodArgs.Configuration | MethodArgs.ConfigureMethod)] + [Theory] + public void AddStandardResilienceHandler_NullBuilder_Throws(MethodArgs mode) + { + IHttpClientBuilder builder = null!; + + Assert.Throws(() => AddStandardResilienceHandler(mode, builder, _validConfigurationSection, options => { })); + } + + [InlineData(MethodArgs.ConfigureMethod)] + [InlineData(MethodArgs.Configuration | MethodArgs.ConfigureMethod)] + [Theory] + public void AddStandardResilienceHandler_NullConfigureMethod_Throws(MethodArgs mode) + { + var builder = new ServiceCollection().AddHttpClient("test"); + + Assert.Throws(() => AddStandardResilienceHandler(mode, builder, _validConfigurationSection, null!)); + + } + + [InlineData(MethodArgs.Configuration)] + [InlineData(MethodArgs.Configuration | MethodArgs.ConfigureMethod)] + [Theory] + public void AddStandardResilienceHandler_NullConfiguration_Throws(MethodArgs mode) + { + var builder = new ServiceCollection().AddHttpClient("test"); + + Assert.Throws(() => AddStandardResilienceHandler(mode, builder, null!, options => { })); + } + + [InlineData(MethodArgs.Configuration)] + [InlineData(MethodArgs.Configuration | MethodArgs.ConfigureMethod)] + [Theory] + public void AddStandardResilienceHandler_NullConfigurationSectionContent_Throws(MethodArgs mode) + { + var builder = new ServiceCollection().AddHttpClient("test"); + + Assert.Throws(() => AddStandardResilienceHandler(mode, builder, _emptyConfigurationSection, options => { })); + } + + [InlineData(MethodArgs.Configuration)] + [InlineData(MethodArgs.Configuration | MethodArgs.ConfigureMethod | MethodArgs.Builder)] + [Theory] + public void AddStandardResilienceHandler_ConfigurationPropertyWithTypo_Throws(MethodArgs mode) + { + var builder = new ServiceCollection().AddLogging().RegisterMetering().AddHttpClient("test"); + + AddStandardResilienceHandler(mode, builder, _invalidConfigurationSection, options => { }); + + var provider = builder.Services.BuildServiceProvider().GetRequiredService(); +#if NET6_0_OR_GREATER + Assert.Throws(() => provider.GetPipeline($"test-standard")); +#else + var pipeline = provider.GetPipeline($"test-standard"); + Assert.NotNull(pipeline); +#endif + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public void AddStandardResilienceHandler_EnsureValidated(bool wholePipeline) + { + var builder = new ServiceCollection().AddLogging().RegisterMetering().AddHttpClient("test"); + + AddStandardResilienceHandler(MethodArgs.ConfigureMethod, builder, null!, options => + { + if (wholePipeline) + { + options.TotalRequestTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(2); + options.AttemptTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(1); + } + else + { + options.BulkheadOptions.MaxQueuedActions = -1; + } + }); + + var provider = builder.Services.BuildServiceProvider().GetRequiredService(); + + Assert.Throws(() => provider.GetPipeline($"test-standard")); + } + + [InlineData(MethodArgs.None)] + [InlineData(MethodArgs.ConfigureMethod)] + [InlineData(MethodArgs.Configuration)] + [InlineData(MethodArgs.Configuration | MethodArgs.ConfigureMethod)] + [InlineData(MethodArgs.Builder | MethodArgs.ConfigureMethodWithServiceProvider)] + [InlineData(MethodArgs.ConfigureMethod | MethodArgs.Builder)] + [InlineData(MethodArgs.Configuration | MethodArgs.Builder)] + [InlineData(MethodArgs.Configuration | MethodArgs.ConfigureMethod | MethodArgs.Builder)] + [Theory] + public void AddStandardResilienceHandler_EnsureConfigured(MethodArgs mode) + { + var builder = new ServiceCollection().AddLogging().RegisterMetering().AddHttpClient("test"); + + AddStandardResilienceHandler(mode, builder, _validConfigurationSection, options => { }); + + var provider = builder.Services.BuildServiceProvider().GetRequiredService(); + + var pipeline = provider.GetPipeline($"test-standard"); + Assert.NotNull(pipeline); + } + + private static void AddStandardResilienceHandler( + MethodArgs mode, + IHttpClientBuilder builder, + IConfigurationSection configuration, + Action configureMethod) + { + _ = mode switch + { + MethodArgs.None => builder.AddStandardResilienceHandler(), + MethodArgs.Configuration | MethodArgs.Builder => builder.AddStandardResilienceHandler().Configure(configuration), + MethodArgs.ConfigureMethod | MethodArgs.Builder => builder.AddStandardResilienceHandler().Configure(configureMethod), + MethodArgs.ConfigureMethodWithServiceProvider | MethodArgs.Builder => builder.AddStandardResilienceHandler().Configure((options, serviceProvider) => + { + serviceProvider.Should().NotBeNull(); + configureMethod(options); + }), + MethodArgs.Configuration | MethodArgs.ConfigureMethod | MethodArgs.Builder => builder.AddStandardResilienceHandler().Configure(configuration).Configure(configureMethod), + MethodArgs.Configuration | MethodArgs.ConfigureMethod => builder.AddStandardResilienceHandler().Configure(configuration).Configure(configureMethod), + MethodArgs.Configuration => builder.AddStandardResilienceHandler(configuration), + MethodArgs.ConfigureMethod => builder.AddStandardResilienceHandler(configureMethod), + _ => throw new NotSupportedException() + }; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpStandardResilienceOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpStandardResilienceOptionsTests.cs new file mode 100644 index 0000000000..ea56e26a41 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpStandardResilienceOptionsTests.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Polly; + +public class HttpStandardResilienceOptionsTests +{ + private readonly HttpStandardResilienceOptions _defaultInstance; + + public HttpStandardResilienceOptionsTests() + { + _defaultInstance = new HttpStandardResilienceOptions(); + } + + [Fact] + public void TimeoutSettings_Ok() + { + Assert.True(_defaultInstance.AttemptTimeoutOptions.TimeoutInterval < _defaultInstance.TotalRequestTimeoutOptions.TimeoutInterval); + } + + [Fact] + public void PropertiesNotNull() + { + Assert.NotNull(_defaultInstance.RetryOptions); + Assert.NotNull(_defaultInstance.AttemptTimeoutOptions); + Assert.NotNull(_defaultInstance.TotalRequestTimeoutOptions); + Assert.NotNull(_defaultInstance.CircuitBreakerOptions); + Assert.NotNull(_defaultInstance.BulkheadOptions); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/ContextExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/ContextExtensionsTest.cs new file mode 100644 index 0000000000..668db2302a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/ContextExtensionsTest.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Net.Http; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Telemetry; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Internals; + +public class ContextExtensionsTest +{ + [Fact] + public void ArgumentValidation_Ok() + { + Assert.Throws(() => Resilience.Internal.ContextExtensions.SetRequestMetadata(null!, new HttpRequestMessage())); + Assert.Throws(() => Resilience.Internal.ContextExtensions.SetRequestMetadata(new Context(), null!)); + } + + [InlineData("A", null, "A")] + [InlineData("A", "B", "B")] + [InlineData(null, "B", "B")] + [InlineData(null, null, null)] + [Theory] + public void SetRequestMetadata_EnsureCorrectBehavior(string? requestMetadata, string? contextMetadata, string? expectedMetadata) + { + var context = new Context(); + using var request = new HttpRequestMessage(); + + if (requestMetadata != null) + { + request.SetRequestMetadata(new RequestMetadata { DependencyName = requestMetadata }); + } + + if (contextMetadata != null) + { + context[TelemetryConstants.RequestMetadataKey] = new RequestMetadata { DependencyName = contextMetadata }; + } + + context.SetRequestMetadata(request); + + context.TryGetValue(TelemetryConstants.RequestMetadataKey, out var val); + + Assert.Equal(expectedMetadata, (val as RequestMetadata)?.DependencyName); + } + + [Fact] + public void RequestMessageProviderAndSetter_EnsureCorrectBehavior() + { + using var message = new HttpRequestMessage(); + var context = new Context(); + var setter = Resilience.Internal.ContextExtensions.CreateRequestMessageSetter("my-pipeline"); + var provider = Resilience.Internal.ContextExtensions.CreateRequestMessageProvider("my-pipeline"); + var providerOther = Resilience.Internal.ContextExtensions.CreateRequestMessageProvider("my-pipeline-other"); + + setter(context, message); + + Assert.Equal(message, provider(context)); + Assert.NotEqual(message, providerOther(context)); + Assert.Null(providerOther(context)); + } + + [Fact] + public void InvokerProviderAndSetter_EnsureCorrectBehavior() + { + using var handler = new TestHandlerStub(HttpStatusCode.OK); + using var invoker = new HttpMessageInvoker(handler); + var context = new Context(); + var setter = Resilience.Internal.ContextExtensions.CreateMessageInvokerSetter("my-pipeline"); + var provider = Resilience.Internal.ContextExtensions.CreateMessageInvokerProvider("my-pipeline"); + var providerOther = Resilience.Internal.ContextExtensions.CreateMessageInvokerProvider("my-pipeline-other"); + + setter(context, new Lazy(() => invoker)); + + Assert.Equal(invoker, provider(context)); + Assert.NotEqual(invoker, providerOther(context)); + Assert.Null(providerOther(context)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/DefaultRequestClonerTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/DefaultRequestClonerTests.cs new file mode 100644 index 0000000000..6455428444 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/DefaultRequestClonerTests.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Net.Http; +using System.Text; +using FluentAssertions; +using Microsoft.Extensions.Http.Resilience.Internal; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Internals; + +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS0618 // Type or member is obsolete + +public class DefaultRequestClonerTests +{ + private readonly DefaultRequestCloner _cloneHandler; + + public DefaultRequestClonerTests() + { + _cloneHandler = new DefaultRequestCloner(); + } + + [Fact] + public void CreateSnapshot_NullRequest_ShouldThrow() + { + Assert.Throws(() => _cloneHandler.CreateSnapshot(null!)); + } + + [Fact] + public void CreateSnapshot_StreamContent_ShouldThrow() + { + var initialRequest = new HttpRequestMessage + { + RequestUri = new Uri("https://dummy-uri.com?query=param"), + Method = HttpMethod.Get, + Content = new StreamContent(new MemoryStream()) + }; + + var exception = Assert.Throws(() => _cloneHandler.CreateSnapshot(initialRequest)); + Assert.Equal("StreamContent content cannot by cloned using the DefaultRequestCloner.", exception.Message); + initialRequest.Dispose(); + } + + [Fact] + public void CreateSnapshot_CreatesClone() + { + using var request = CreateRequest(); + var snapshot = _cloneHandler.CreateSnapshot(request); + var cloned = snapshot.Create(); + AssertClonedMessage(request, cloned); + } + + [Fact] + public void CreateSnapshot_OriginalMessageChanged_SnapshotReturnsOriginalData() + { + using var request = CreateRequest(); + var snapshot = _cloneHandler.CreateSnapshot(request); + + request.Properties["some-new-prop"] = "ABC"; + var cloned = snapshot.Create(); + cloned.Properties.Should().NotContainKey("some-new-prop"); + } + + private static HttpRequestMessage CreateRequest() + { + var initialRequest = new HttpRequestMessage + { + RequestUri = new Uri("https://dummy-uri.com?query=param"), + Method = HttpMethod.Get, + Version = new Version(1, 1), + Content = new StringContent("{\"name\":\"John Doe\",\"age\":33}", Encoding.UTF8, "application/json") + }; + + initialRequest.Headers.Add("Authorization", "Bearer token"); + initialRequest.Properties.Add("A", "A"); + initialRequest.Properties.Add("B", "B"); + +#if NET5_0_OR_GREATER + initialRequest.Options.Set(new HttpRequestOptionsKey("C"), "C"); + initialRequest.Options.Set(new HttpRequestOptionsKey("D"), "D"); +#endif + return initialRequest; + } + + private static void AssertClonedMessage(HttpRequestMessage initialRequest, HttpRequestMessage cloned) + { + Assert.NotNull(cloned); + Assert.Equal(initialRequest.Method, cloned.Method); + Assert.Equal(initialRequest.RequestUri, cloned.RequestUri); + Assert.Equal(initialRequest.Content, cloned.Content); + Assert.Equal(initialRequest.Version, cloned.Version); + + Assert.NotNull(cloned.Headers.Authorization); + + cloned.Properties["A"].Should().Be("A"); + cloned.Properties["B"].Should().Be("B"); + +#if NET5_0_OR_GREATER + initialRequest.Options.TryGetValue(new HttpRequestOptionsKey("C"), out var val).Should().BeTrue(); + val.Should().Be("C"); + + initialRequest.Options.TryGetValue(new HttpRequestOptionsKey("D"), out val).Should().BeTrue(); + val.Should().Be("D"); +#endif + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/FallbackClientHandlerOptionsValidatorTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/FallbackClientHandlerOptionsValidatorTests.cs new file mode 100644 index 0000000000..86a6b94500 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/FallbackClientHandlerOptionsValidatorTests.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Http.Resilience.Internal.Validators; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Internals; + +public class FallbackClientHandlerOptionsValidatorTests +{ + private readonly FallbackClientHandlerOptionsValidator _validator; + + public FallbackClientHandlerOptionsValidatorTests() + { + _validator = new FallbackClientHandlerOptionsValidator(); + } + + [Theory] + [InlineData("https://fallback-uri.com/somepath")] + [InlineData("https://fallback-uri.com/somepath/someotherpath")] + [InlineData("https://fallback-uri.com?a")] + [InlineData("https://fallback-uri.com/somepath?query=value")] + [InlineData("https://fallback-uri.com?a=1&b=2&c=3")] + public void Validate_InvalidUri_ShouldReturnFailResult(string input) + { + var uri = new Uri(input); + var options = new FallbackClientHandlerOptions { BaseFallbackUri = uri }; + var result = _validator.Validate(string.Empty, options); + + Assert.True(result.Failed); + Assert.Equal("Property BaseFallbackUri: must be a base uri, hence it may contain only the schema, host and port.", result.FailureMessage); + } + + [Fact] + public void Validate_NullUri_ShouldReturnFailResult() + { + var options = new FallbackClientHandlerOptions(); + var result = _validator.Validate(string.Empty, options); + + Assert.True(result.Failed); + Assert.Contains("Property BaseFallbackUri: must be configured", result.FailureMessage); + } + + [Fact] + public void Validate_NullProperties_ShouldReturnFailedResult() + { + var options = new FallbackClientHandlerOptions + { + FallbackPolicyOptions = null! + }; + + var result = _validator.Validate(string.Empty, options); + Assert.True(result.Failed); + Assert.Contains(nameof(FallbackClientHandlerOptions.FallbackPolicyOptions), result.FailureMessage); + + options = new FallbackClientHandlerOptions + { + FallbackPolicyOptions = null! + }; + + result = _validator.Validate(string.Empty, options); + Assert.True(result.Failed); + Assert.Contains(nameof(FallbackClientHandlerOptions.FallbackPolicyOptions), result.FailureMessage); + + options = new FallbackClientHandlerOptions + { + FallbackPolicyOptions = null! + }; + + result = _validator.Validate(string.Empty, options); + Assert.True(result.Failed); + } + + [Theory] + [InlineData("https://fallback-uri.com")] + [InlineData("https://fallback-uri.com:99")] + [InlineData("http://test")] + public void Validate_ValidUri_ShouldReturnSuccessResult(string input) + { + var uri = new Uri(input); + var options = new FallbackClientHandlerOptions { BaseFallbackUri = uri }; + var result = _validator.Validate(string.Empty, options); + + Assert.True(result.Succeeded); + } + + [Fact] + public void Validate_RelativeValidUri_ShouldReturnFailure() + { + var uri = new Uri("/", UriKind.Relative); + var options = new FallbackClientHandlerOptions { BaseFallbackUri = uri }; + + var result = _validator.Validate(string.Empty, options); + + Assert.True(result.Failed); + Assert.Equal("Property BaseFallbackUri: must be an absolute uri.", result.FailureMessage); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/FallbackTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/FallbackTests.cs new file mode 100644 index 0000000000..169b15544e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/FallbackTests.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Internals; + +public sealed class FallbackTests : IDisposable +{ + private const string ClientName = "fallback"; + private static readonly Uri _fallbackBaseUri = new("https://www.example-fallback.com"); + private readonly IHttpClientBuilder _clientBuilder; + private readonly Mock _requestCloneHandlerMock; + private readonly List _requests = new(); + private bool _fail; + + public FallbackTests() + { + _requestCloneHandlerMock = new Mock(MockBehavior.Strict); + + var services = new ServiceCollection(); + services.AddFakeLogging(); + services.AddSingleton(_requestCloneHandlerMock.Object); + + _clientBuilder = services + .AddHttpClient(ClientName) + .AddFallbackHandler(options => options.BaseFallbackUri = _fallbackBaseUri); + } + + public void Dispose() + { + _requestCloneHandlerMock.VerifyAll(); + } + + [Fact] + public async Task SendAsync_SuccessfullExecution_ShouldReturnResponseWithoutFallback() + { + var client = CreateClientWithHandler(); + using var request = new HttpRequestMessage(HttpMethod.Get, "https://www.dummy-request.com/abc"); + var response = await client.SendAsync(request, default); + + Assert.Single(_requests); + Assert.Equal("https://www.dummy-request.com/abc", _requests[0].ToString()); + } + + [Fact] + public async Task SendAsync_FailedExecution_ShouldReturnResponseFromFallback() + { + var client = CreateClientWithHandler(); + + using var request = new HttpRequestMessage(HttpMethod.Get, "https://www.dummy-request.com/abc?x=x"); + _fail = true; + _requestCloneHandlerMock.Setup(mock => mock.CreateSnapshot(request)).Returns(Mock.Of(v => v.Create() == request)); + var response = await client.SendAsync(request, default); + + Assert.Equal(2, _requests.Count); + Assert.Equal("https://www.dummy-request.com/abc?x=x", _requests[0].ToString()); + Assert.Equal("https://www.example-fallback.com/abc?x=x", _requests[1].ToString()); + + } + + private System.Net.Http.HttpClient CreateClientWithHandler() + { + _clientBuilder.AddHttpMessageHandler(() => new TestHandlerStub(InnerHandlerFunction)); + + return _clientBuilder.Services.BuildServiceProvider().GetRequiredService().CreateClient(ClientName); + } + + private Task InnerHandlerFunction(HttpRequestMessage request, CancellationToken cancellationToken) + { + _requests.Add(request.RequestUri!); + + if (_fail) + { + _fail = false; + throw new HttpRequestException("Something went wrong"); + } + + return Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.OK }); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/HttpRequestMessageExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/HttpRequestMessageExtensionsTests.cs new file mode 100644 index 0000000000..2440b52bc1 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/HttpRequestMessageExtensionsTests.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using Microsoft.Extensions.Http.Resilience.Internal; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Internals; + +#pragma warning disable CA2000 // Test class + +public class HttpRequestMessageExtensionsTests +{ + [Theory] + [InlineData("https://initial.uri", "https://fallback-uri.com")] + [InlineData("https://initial.uri/somepath", "https://fallback-uri.com/somepath")] + [InlineData("https://initial.uri/somepath/someotherpath", "https://fallback-uri.com/somepath/someotherpath")] + [InlineData("https://initial.uri:2030/somepath", "https://fallback-uri.com/somepath")] + [InlineData("https://initial.uri:2030/somepath?query=value", "https://fallback-uri.com/somepath?query=value")] + [InlineData("https://initial.uri?a=1&b=2&c=3", "https://fallback-uri.com?a=1&b=2&c=3")] + [InlineData("https://initial.uri?", "https://fallback-uri.com")] + public void ReplaceHost_ValidArguments_ShouldReplaceUri(string initialUriString, string expectedUriString) + { + var request = new HttpRequestMessage(HttpMethod.Get, new Uri(initialUriString)); + var fallbackUri = new Uri("https://fallback-uri.com"); + + request = request.ReplaceHost(fallbackUri); + var expectedUri = new Uri(expectedUriString); + Assert.Equal(expectedUri, request.RequestUri); + } + + [Fact] + public void ReplaceHost_NullUri_ShouldThrow() + { + var request = new HttpRequestMessage(HttpMethod.Get, new Uri("https://initial.uri")); + Assert.Throws(() => + request.ReplaceHost(null!)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/HttpResiliencePipelineBuilderTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/HttpResiliencePipelineBuilderTest.cs new file mode 100644 index 0000000000..00f1ac2050 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/HttpResiliencePipelineBuilderTest.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Resilience; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Internals; +public class HttpResiliencePipelineBuilderTest +{ + [Fact] + public void Ctor_Ok() + { + var services = new ServiceCollection(); + var builder = services.AddResiliencePipeline("test"); + + var httpBuilder = new HttpResiliencePipelineBuilder(builder); + + Assert.Equal(services, httpBuilder.Services); + Assert.Equal("test", httpBuilder.PipelineName); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/PipelineNameHelperTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/PipelineNameHelperTest.cs new file mode 100644 index 0000000000..fb930606de --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/PipelineNameHelperTest.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Http.Resilience.Internal; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Internals; +public class PipelineNameHelperTest +{ + [Fact] + public void GetPipelineName_Ok() + { + Assert.Equal("client-pipeline", PipelineNameHelper.GetPipelineName("client", "pipeline")); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/ResilienceHandlerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/ResilienceHandlerTest.cs new file mode 100644 index 0000000000..fa51b67b75 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/ResilienceHandlerTest.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Telemetry; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Internals; + +public class ResilienceHandlerTest +{ + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task SendAsync_EnsureRequestMetadataFlows(bool executionContextSet) + { + using var handler = new ResilienceHandler("dummy", _ => Policy.NoOpAsync()); + using var invoker = new HttpMessageInvoker(handler); + using var request = new HttpRequestMessage(); + + if (executionContextSet) + { + request.SetPolicyExecutionContext(new Context()); + } + + request.SetRequestMetadata(new RequestMetadata()); + + handler.InnerHandler = new TestHandlerStub(HttpStatusCode.OK); + + await invoker.SendAsync(request, default); + + if (executionContextSet) + { + Assert.NotNull(request.GetPolicyExecutionContext()![TelemetryConstants.RequestMetadataKey]); + } + else + { + Assert.Null(request.GetPolicyExecutionContext()); + } + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task SendAsync_EnsureExecutionContext(bool executionContextSet) + { + using var handler = new ResilienceHandler("dummy", _ => Policy.NoOpAsync()); + using var invoker = new HttpMessageInvoker(handler); + using var request = new HttpRequestMessage(); + + if (executionContextSet) + { + request.SetPolicyExecutionContext(new Context()); + } + + handler.InnerHandler = new TestHandlerStub(HttpStatusCode.OK); + + await invoker.SendAsync(request, default); + + if (executionContextSet) + { + Assert.NotNull(request.GetPolicyExecutionContext()); + } + else + { + Assert.Null(request.GetPolicyExecutionContext()); + } + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task SendAsync_EnsureInvoker(bool executionContextSet) + { + using var handler = new ResilienceHandler("dummy", _ => Policy.NoOpAsync()); + using var invoker = new HttpMessageInvoker(handler); + using var request = new HttpRequestMessage(); + + if (executionContextSet) + { + request.SetPolicyExecutionContext(new Context()); + } + + handler.InnerHandler = new TestHandlerStub((r, _) => + { + var invokerProvider = Resilience.Internal.ContextExtensions.CreateMessageInvokerProvider("dummy"); + var requestProvider = Resilience.Internal.ContextExtensions.CreateRequestMessageProvider("dummy"); + + Assert.NotNull(invokerProvider(r.GetPolicyExecutionContext()!)); + Assert.Equal(request, requestProvider(r.GetPolicyExecutionContext()!)); + + return Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.Created }); + }); + + var response = await invoker.SendAsync(request, default); + + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/Validators/HttpStandardResilienceOptionsCustomValidatorTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/Validators/HttpStandardResilienceOptionsCustomValidatorTests.cs new file mode 100644 index 0000000000..68be9f4f5c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/Validators/HttpStandardResilienceOptionsCustomValidatorTests.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +#if NET6_0_OR_GREATER +using System.Linq; +#endif +using Microsoft.Extensions.Http.Resilience.Internal.Validators; +using Microsoft.Extensions.Resilience.Options; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Internals.Validators; +public class HttpStandardResilienceOptionsCustomValidatorTests +{ + [Fact] + public void Validate_InvalidOptions_EnsureValidationErrors() + { + HttpStandardResilienceOptions options = new(); + options.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromSeconds(1); + options.TotalRequestTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(1); + + var validationResult = new HttpStandardResilienceOptionsCustomValidator().Validate(string.Empty, options); + + Assert.True(validationResult.Failed); + +#if NET6_0_OR_GREATER + Assert.Equal(3, validationResult.Failures.Count()); +#endif + } + + [Fact] + public void Validate_ValidOptions_NoValidationErrors() + { + HttpStandardResilienceOptions options = new(); + + var validationResult = new HttpStandardResilienceOptionsCustomValidator().Validate(string.Empty, options); + + Assert.True(validationResult.Succeeded); + } + + public static IEnumerable GetOptions_ValidOptions_EnsureNoErrors_Data + { + get + { + var options = new HttpStandardResilienceOptions(); + options.AttemptTimeoutOptions.TimeoutInterval = options.TotalRequestTimeoutOptions.TimeoutInterval; + options.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromMilliseconds(options.AttemptTimeoutOptions.TimeoutInterval.TotalMilliseconds * 2); + yield return new object[] { options }; + + options = new HttpStandardResilienceOptions(); + options.AttemptTimeoutOptions.TimeoutInterval = options.TotalRequestTimeoutOptions.TimeoutInterval; + options.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromMilliseconds(options.AttemptTimeoutOptions.TimeoutInterval.TotalMilliseconds * 2) + TimeSpan.FromMilliseconds(10); + yield return new object[] { options }; + + options = new HttpStandardResilienceOptions(); + options.RetryOptions.RetryCount = 1; + options.RetryOptions.BackoffType = BackoffType.Linear; + options.RetryOptions.BaseDelay = options.TotalRequestTimeoutOptions.TimeoutInterval; + yield return new object[] { options }; + } + } + + [MemberData(nameof(GetOptions_ValidOptions_EnsureNoErrors_Data))] + [Theory] + public void Validate_ValidOptions_EnsureNoErrors(HttpStandardResilienceOptions options) + { + var validationResult = new HttpStandardResilienceOptionsCustomValidator().Validate(string.Empty, options); + + Assert.False(validationResult.Failed); + } + + public static IEnumerable GetOptions_InvalidOptions_EnsureErrors_Data + { + get + { + var options = new HttpStandardResilienceOptions(); + options.TotalRequestTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(2); + options.AttemptTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(3); + yield return new object[] { options }; + + options = new HttpStandardResilienceOptions(); + options.TotalRequestTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(2); + yield return new object[] { options }; + + options = new HttpStandardResilienceOptions(); + options.RetryOptions.BaseDelay = TimeSpan.FromDays(1); + yield return new object[] { options }; + + options = new HttpStandardResilienceOptions(); + options.AttemptTimeoutOptions.TimeoutInterval = options.TotalRequestTimeoutOptions.TimeoutInterval; + options.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromMilliseconds(options.AttemptTimeoutOptions.TimeoutInterval.TotalMilliseconds / 2); + yield return new object[] { options }; + } + } + + [MemberData(nameof(GetOptions_InvalidOptions_EnsureErrors_Data))] + [Theory] + public void Validate_InvalidOptions_EnsureErrors(HttpStandardResilienceOptions options) + { + var validationResult = new HttpStandardResilienceOptionsCustomValidator().Validate(string.Empty, options); + + Assert.True(validationResult.Failed); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/Validators/ValidationHelperTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/Validators/ValidationHelperTests.cs new file mode 100644 index 0000000000..1512170971 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/Validators/ValidationHelperTests.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using Microsoft.Extensions.Http.Resilience.Internal.Validators; +using Microsoft.Extensions.Resilience.Options; +using Polly.Contrib.WaitAndRetry; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Internal.Test; + +public class ValidationHelperTests +{ + [Fact] + public void GetExponentialWithJitterDeterministicDelay_ShouldReturnRetryPolicyUpperboundDelaySum() + { + var retryPolicyOptions = new RetryPolicyOptions + { + RetryCount = 3, + BaseDelay = TimeSpan.FromSeconds(2), + BackoffType = BackoffType.ExponentialWithJitter + }; + var upperbound = ValidationHelper.GetExponentialWithJitterDeterministicDelay(retryPolicyOptions); + var jitteredDelays = Backoff.DecorrelatedJitterBackoffV2(retryPolicyOptions.BaseDelay, retryPolicyOptions.RetryCount); + + var expected = TimeSpan.FromTicks(114_061_988); + Assert.True(upperbound >= jitteredDelays.Aggregate((accumulated, current) => accumulated + current)); + Assert.Equal(expected.TotalMilliseconds, upperbound.TotalMilliseconds); + } + + [Fact] + public void GetExponentialWithJitterDeterministicDelay_MaxDelayTest() + { + var options = new RetryPolicyOptions + { + RetryCount = 99, + BaseDelay = TimeSpan.FromDays(1), + BackoffType = BackoffType.ExponentialWithJitter + }; + + var upper = ValidationHelper.GetRetryPolicyDelaySum(options); + + Assert.Equal(TimeSpan.MaxValue.Ticks - 1000, upper.Ticks); + } + + [Fact] + public void GetRetryPolicyDelaySum_Ok() + { + var options = new RetryPolicyOptions + { + RetryCount = 2, + BaseDelay = TimeSpan.FromSeconds(2), + BackoffType = BackoffType.Linear + }; + + Assert.Equal(TimeSpan.FromSeconds(6), options.GetRetryPolicyDelaySum()); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/TestHandlerStub.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/TestHandlerStub.cs new file mode 100644 index 0000000000..7dc799eb6a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/TestHandlerStub.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Http.Resilience.Test; + +public class TestHandlerStub : DelegatingHandler +{ + private readonly Func> _handlerFunc; + + public TestHandlerStub(HttpStatusCode responseStatus) +#pragma warning disable CA2000 // Dispose objects before losing scope + : this(new HttpResponseMessage(responseStatus)) +#pragma warning restore CA2000 // Dispose objects before losing scope + { + } + + public TestHandlerStub(HttpResponseMessage responseMessage) + : this((_, _) => Task.FromResult(responseMessage)) + { + } + + public TestHandlerStub(Func> handlerFunc) + { + _handlerFunc = handlerFunc; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return _handlerFunc(request, cancellationToken); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/HttpClientBuilderExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/HttpClientBuilderExtensionsTest.cs new file mode 100644 index 0000000000..3264e2efba --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/HttpClientBuilderExtensionsTest.cs @@ -0,0 +1,276 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Resilience.FaultInjection; +using Microsoft.Extensions.Telemetry.Metering; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Test; + +public class HttpClientBuilderExtensionsTest +{ + private readonly IConfiguration _configurationWithPolicyOptions; + + public HttpClientBuilderExtensionsTest() + { + var builder = new ConfigurationBuilder().AddJsonFile("configs/appsettings.json"); + _configurationWithPolicyOptions = builder.Build(); + } + + [Fact] + public async Task HttpClientBuilder_AddFaultInjectionPolicyHandler_WithOptionsName_NoPreviousContext_ShouldWork() + { + var httpClient = SetupHttpClientWithFaultInjection("TestGroup1"); + + using var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:12345"); + var response = await httpClient.SendAsync(request); + + Assert.False(response.IsSuccessStatusCode); + } + + [Fact] + public async Task HttpClientBuilder_AddFaultInjectionPolicyHandler_WithOptionsName_WithPreviousContext_ShouldSucceed() + { + var httpClient = SetupHttpClientWithFaultInjection("TestGroup1"); + + var context = new Context(); + using var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:12345"); + request.SetPolicyExecutionContext(context); + var response = await httpClient.SendAsync(request); + + Assert.False(response.IsSuccessStatusCode); + } + + [Fact] + public void AddFaultInjectionPolicyHandler_OptionsGroupNameNull_ShouldThrow() + { + var services = new ServiceCollection(); + + Action action = builder => { }; + services + .AddLogging() + .RegisterMetering() + .AddHttpClientFaultInjection(action); + + var builder = services.AddHttpClient(); + Assert.Throws(() => builder.AddFaultInjectionPolicyHandler(null!)); + } + + [Fact] + public void AddFaultInjectionPolicyHandler_HttpClientBuilderNull_ShouldThrow() + { + IHttpClientBuilder? builder = null!; + Assert.Throws(() => builder.AddFaultInjectionPolicyHandler("TestGroup1")); + } + + [Fact] + public async Task AddWeightedFaultInjectionPolicyHandlers_WithWeightAssignmentsConfigAction() + { + var services = new ServiceCollection(); + var httpClientIdentifier = "HttpClientClass"; + var chaosPolicyOptionsGroup1 = new ChaosPolicyOptionsGroup + { + HttpResponseInjectionPolicyOptions = new HttpResponseInjectionPolicyOptions + { + Enabled = true, + FaultInjectionRate = 1.0, + StatusCode = HttpStatusCode.GatewayTimeout + } + }; + var chaosPolicyOptionsGroup2 = new ChaosPolicyOptionsGroup + { + HttpResponseInjectionPolicyOptions = new HttpResponseInjectionPolicyOptions + { + Enabled = true, + FaultInjectionRate = 1.0, + StatusCode = HttpStatusCode.ServiceUnavailable + } + }; + + Action action = builder => + builder + .Configure(option => + { + option.ChaosPolicyOptionsGroups = new Dictionary + { + { "TestA", chaosPolicyOptionsGroup1 }, + { "TestB", chaosPolicyOptionsGroup2 }, + }; + }); + + services + .AddLogging() + .RegisterMetering() + .AddHttpClientFaultInjection(action); + services + .AddHttpClient() + .AddWeightedFaultInjectionPolicyHandlers(weightAssignmentsOptions => + { + weightAssignmentsOptions.WeightAssignments.Add("TestA", 50); + weightAssignmentsOptions.WeightAssignments.Add("TestB", 50); + }) + .AddHttpMessageHandler(() => new TestMessageHandler()); + + var clientFactory = services + .BuildServiceProvider() + .GetRequiredService(); + var httpClient = clientFactory.CreateClient(httpClientIdentifier); + + for (int i = 0; i < 100; i++) + { + using var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:12345"); + var response = await httpClient.SendAsync(request); + + Assert.False(response.IsSuccessStatusCode); + Assert.True(response.StatusCode == HttpStatusCode.GatewayTimeout || response.StatusCode == HttpStatusCode.ServiceUnavailable); + } + } + + [Fact] + public async Task AddWeightedFaultInjectionPolicyHandlers_WithWeightAssignmentsConfigSection() + { + var services = new ServiceCollection(); + var httpClientIdentifier = "HttpClientClass"; + var chaosPolicyOptionsGroup1 = new ChaosPolicyOptionsGroup + { + HttpResponseInjectionPolicyOptions = new HttpResponseInjectionPolicyOptions + { + Enabled = true, + FaultInjectionRate = 1.0, + StatusCode = HttpStatusCode.GatewayTimeout + } + }; + var chaosPolicyOptionsGroup2 = new ChaosPolicyOptionsGroup + { + HttpResponseInjectionPolicyOptions = new HttpResponseInjectionPolicyOptions + { + Enabled = true, + FaultInjectionRate = 1.0, + StatusCode = HttpStatusCode.ServiceUnavailable + } + }; + + Action action = builder => + builder + .Configure(option => + { + option.ChaosPolicyOptionsGroups = new Dictionary + { + { "TestA", chaosPolicyOptionsGroup1 }, + { "TestB", chaosPolicyOptionsGroup2 }, + }; + }); + + services + .AddLogging() + .RegisterMetering() + .AddHttpClientFaultInjection(action); + services + .AddHttpClient() + .AddWeightedFaultInjectionPolicyHandlers(_configurationWithPolicyOptions.GetSection("FaultPolicyWeightAssignments")) + .AddHttpMessageHandler(() => new TestMessageHandler()); + + var clientFactory = services + .BuildServiceProvider() + .GetRequiredService(); + var httpClient = clientFactory.CreateClient(httpClientIdentifier); + + for (int i = 0; i < 100; i++) + { + var context = new Context + { + ["TestField"] = "Test123" + }; + + using var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:12345"); + request.SetPolicyExecutionContext(context); + var response = await httpClient.SendAsync(request); + + Assert.Equal("Test123", request.GetPolicyExecutionContext()!["TestField"]); + Assert.False(response.IsSuccessStatusCode); + Assert.True(response.StatusCode == HttpStatusCode.GatewayTimeout || response.StatusCode == HttpStatusCode.ServiceUnavailable); + } + } + + [Fact] + public void AddWeightedFaultInjectionPolicyHandlers_WeightAssignmentOptionsNull_ShouldThrow() + { + var services = new ServiceCollection(); + + Action action = builder => { }; + services + .AddLogging() + .RegisterMetering() + .AddHttpClientFaultInjection(action); + + var builder = services.AddHttpClient(); + Assert.Throws(() => builder.AddWeightedFaultInjectionPolicyHandlers((Action)null!)); + Assert.Throws(() => builder.AddWeightedFaultInjectionPolicyHandlers((IConfigurationSection)null!)); + } + + [Fact] + public void AddWeightedFaultInjectionPolicyHandlers_HttpClientBuilderNull_ShouldThrow() + { + IHttpClientBuilder? builder = null!; + Assert.Throws(() => builder.AddWeightedFaultInjectionPolicyHandlers(options => { })); + Assert.Throws(() => builder.AddWeightedFaultInjectionPolicyHandlers(_configurationWithPolicyOptions.GetSection("FaultPolicyWeightAssignments"))); + } + + private class HttpClientClass + { + } + + private class TestMessageHandler : DelegatingHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent("OK") }); + } + } + + private static System.Net.Http.HttpClient SetupHttpClientWithFaultInjection(string chaosPolicyOptionsGroupName) + { + var services = new ServiceCollection(); + var httpClientIdentifier = "HttpClientClass"; + var chaosPolicyOptionsGroup1 = new ChaosPolicyOptionsGroup + { + HttpResponseInjectionPolicyOptions = new HttpResponseInjectionPolicyOptions + { + Enabled = true, + FaultInjectionRate = 1.0 + } + }; + + Action action = builder => + builder + .Configure(option => + { + option.ChaosPolicyOptionsGroups = new Dictionary + { + { chaosPolicyOptionsGroupName, chaosPolicyOptionsGroup1 } + }; + }); + services + .AddLogging() + .RegisterMetering() + .AddHttpClientFaultInjection(action); + services + .AddHttpClient() + .AddFaultInjectionPolicyHandler(chaosPolicyOptionsGroupName) + .AddHttpMessageHandler(() => new TestMessageHandler()); + + var clientFactory = services + .BuildServiceProvider() + .GetRequiredService(); + return clientFactory.CreateClient(httpClientIdentifier); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/HttpClientFaultInjectionExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/HttpClientFaultInjectionExtensionsTest.cs new file mode 100644 index 0000000000..0f9c443ea9 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/HttpClientFaultInjectionExtensionsTest.cs @@ -0,0 +1,238 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.FaultInjection; +using Microsoft.Extensions.Telemetry.Metering; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Test; + +public class HttpClientFaultInjectionExtensionsTest +{ + private readonly IConfiguration _configurationWithPolicyOptions; + + public HttpClientFaultInjectionExtensionsTest() + { + var builder = new ConfigurationBuilder().AddJsonFile("configs/appsettings.json"); + _configurationWithPolicyOptions = builder.Build(); + } + + [Fact] + public void AddHttpClientFaultInjection_ShouldRegisterRequiredServices() + { + var services = new ServiceCollection(); + services + .AddLogging() + .RegisterMetering() + .AddHttpClientFaultInjection(); + + using var serviceProvider = services.BuildServiceProvider(); + + var chaosPolicyConfigProvider = serviceProvider.GetService(); + Assert.IsAssignableFrom(chaosPolicyConfigProvider); + + var policyFactory = serviceProvider.GetService(); + Assert.IsAssignableFrom(policyFactory); + + var httpPolicyFactory = serviceProvider.GetService(); + Assert.IsAssignableFrom(httpPolicyFactory); + } + + [Fact] + public void AddHttpClientFaultInjection_NullServices_ShouldThrow() + { + ServiceCollection? services = null!; + Assert.Throws(() => services.AddHttpClientFaultInjection()); + } + + [Fact] + public void AddHttpClientFaultInjection_WithConfigurationSection_ShouldRegisterRequiredServices() + { + var services = new ServiceCollection(); + services + .AddLogging() + .RegisterMetering() + .AddHttpClientFaultInjection(_configurationWithPolicyOptions.GetSection("ChaosPolicyConfigurations")); + + using var serviceProvider = services.BuildServiceProvider(); + + var chaosPolicyConfigProvider = serviceProvider.GetService(); + Assert.IsAssignableFrom(chaosPolicyConfigProvider); + + var policyFactory = serviceProvider.GetService(); + Assert.IsAssignableFrom(policyFactory); + + var httpPolicyFactory = serviceProvider.GetService(); + Assert.IsAssignableFrom(httpPolicyFactory); + } + + [Fact] + public void AddHttpClientFaultInjection_WithConfigurationSection_NullServices_ShouldThrow() + { + ServiceCollection? services = null!; + + Assert.Throws( + () => services.AddHttpClientFaultInjection(_configurationWithPolicyOptions.GetSection("ChaosPolicyConfigurations"))); + } + + [Fact] + public void AddHttpClientFaultInjection_NullConfigurationSection_ShouldThrow() + { + var services = new ServiceCollection(); + IConfigurationSection? configurationSection = null!; + + Assert.Throws( + () => services.AddHttpClientFaultInjection(configurationSection)); + } + + [Fact] + public void AddHttpClientFaultInjection_WithAction_ShouldRegisterRequiredServicesAndHttpMessageHandlers() + { + var services = new ServiceCollection(); + + Action action = + builder => + builder.Configure(_configurationWithPolicyOptions.GetSection("ChaosPolicyConfigurations")); + services + .AddLogging() + .RegisterMetering() + .AddHttpClientFaultInjection(action); + + using var serviceProvider = services.BuildServiceProvider(); + + var chaosPolicyConfigProvider = serviceProvider.GetService(); + Assert.IsAssignableFrom(chaosPolicyConfigProvider); + + var policyFactory = serviceProvider.GetService(); + Assert.IsAssignableFrom(policyFactory); + + var httpPolicyFactory = serviceProvider.GetService(); + Assert.IsAssignableFrom(httpPolicyFactory); + + var httpClientFactoryOptions = serviceProvider.GetRequiredService>().Value; + Assert.True(httpClientFactoryOptions.HttpMessageHandlerBuilderActions.Count > 0); + } + + [Fact] + public void AddHttpClientFaultInjection_NullAction_ShouldThrow() + { + var services = new ServiceCollection(); + + Action? action = null!; + Assert.Throws(() => services.AddHttpClientFaultInjection(action)); + } + + [Fact] + public void AddHttpClientFaultInjection_WithAction_NullServices_ShouldThrow() + { + ServiceCollection? services = null!; + + Action action = builder => { }; + Assert.Throws( + () => services.AddHttpClientFaultInjection(action)); + } + + [Fact] + public async Task AddHttpClientFaultInjection_FaultInjectionShouldWork() + { + var chaosPolicyOptionsGroup = new ChaosPolicyOptionsGroup + { + HttpResponseInjectionPolicyOptions = new HttpResponseInjectionPolicyOptions + { + Enabled = true, + FaultInjectionRate = 1.0 + } + }; + + Action action = + builder => + builder.Configure(option => + { + option.ChaosPolicyOptionsGroups = new Dictionary + { + { "HttpClientClass", chaosPolicyOptionsGroup } + }; + }); + var httpClient = SetupHttpClientWithFaultInjection(action); + + using var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:12345"); + var response = await httpClient.SendAsync(request); + + Assert.False(response.IsSuccessStatusCode); + Assert.Equal(response.RequestMessage, request); + } + + [Fact] + public async Task AddHttpClientFaultInjection_FaultInjectionWithHttpContent() + { + var chaosPolicyOptionsGroup = new ChaosPolicyOptionsGroup + { + HttpResponseInjectionPolicyOptions = new HttpResponseInjectionPolicyOptions + { + Enabled = true, + FaultInjectionRate = 1.0, + StatusCode = HttpStatusCode.OK, + HttpContentKey = "TestPayload" + } + }; + using var testContent = new StringContent("Test Content"); + Action action = + builder => + builder.Configure(option => + { + option.ChaosPolicyOptionsGroups = new Dictionary + { + { "HttpClientClass", chaosPolicyOptionsGroup } + }; + }) + .AddHttpContent("TestPayload", testContent); + var httpClient = SetupHttpClientWithFaultInjection(action); + + using var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:12345"); + var response = await httpClient.SendAsync(request); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(testContent, response.Content); + Assert.Equal(response.RequestMessage, request); + } + + private class HttpClientClass + { + } + + private class TestMessageHandler : DelegatingHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent("OK") }); + } + } + + private static System.Net.Http.HttpClient SetupHttpClientWithFaultInjection(Action configure) + { + var services = new ServiceCollection(); + var httpClientIdentifier = "HttpClientClass"; + + services + .AddLogging() + .RegisterMetering() + .AddHttpClientFaultInjection(configure); + services + .AddHttpClient() + .AddHttpMessageHandler(() => new TestMessageHandler()); + + var clientFactory = services + .BuildServiceProvider() + .GetRequiredService(); + return clientFactory.CreateClient(httpClientIdentifier); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/HttpFaultInjectionOptionsBuilderTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/HttpFaultInjectionOptionsBuilderTest.cs new file mode 100644 index 0000000000..1c55137a81 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/HttpFaultInjectionOptionsBuilderTest.cs @@ -0,0 +1,175 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.FaultInjection.Internal; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.FaultInjection; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Test; + +public class HttpFaultInjectionOptionsBuilderTest +{ + [Fact] + public void CanConstruct() + { + var services = new ServiceCollection(); + var faultInjectionOptionsBuilder = new HttpFaultInjectionOptionsBuilder(services); + + Assert.NotNull(faultInjectionOptionsBuilder); + } + + [Fact] + public void Constructor_NullServices_ShouldThrow() + { + Assert.Throws(() => new HttpFaultInjectionOptionsBuilder(null!)); + } + + [Fact] + public void Configure_WithConfigurationSection_ShouldConfigureChaosPolicyConfigProviderOptions() + { + var services = new ServiceCollection(); + var faultInjectionOptionsBuilder = new HttpFaultInjectionOptionsBuilder(services); + + var builder = new ConfigurationBuilder().AddJsonFile("configs/appsettings.json"); + var configuration = builder.Build(); + + faultInjectionOptionsBuilder.Configure(configuration.GetSection("ChaosPolicyConfigurations")); + + using var provider = services.BuildServiceProvider(); + var result = provider.GetRequiredService>().Value; + Assert.IsAssignableFrom(result); + Assert.NotNull(result.ChaosPolicyOptionsGroups?["OptionsGroupTest"]); + } + + [Fact] + public void Configure_WithConfigurationSection_NullConfigurationSection_ShouldThrow() + { + var services = new ServiceCollection(); + var faultInjectionOptionsBuilder = new HttpFaultInjectionOptionsBuilder(services); + + Assert.Throws( + () => faultInjectionOptionsBuilder.Configure((IConfigurationSection)null!)); + } + + [Fact] + public void Configure_WithAction_ShouldConfigureChaosPolicyConfigProviderOptions() + { + var services = new ServiceCollection(); + var faultInjectionOptionsBuilder = new HttpFaultInjectionOptionsBuilder(services); + var testChaosPolicyOptionsGroups = new Dictionary(); + + faultInjectionOptionsBuilder.Configure(options => + { + options.ChaosPolicyOptionsGroups = testChaosPolicyOptionsGroups; + }); + + using var provider = services.BuildServiceProvider(); + var result = provider.GetRequiredService>().Value; + Assert.IsAssignableFrom(result); + Assert.Equal(testChaosPolicyOptionsGroups, result.ChaosPolicyOptionsGroups); + } + + [Fact] + public void Configure_WithAction_NullAction_ShouldThrow() + { + var services = new ServiceCollection(); + var faultInjectionOptionsBuilder = new HttpFaultInjectionOptionsBuilder(services); + + Assert.Throws( + () => faultInjectionOptionsBuilder.Configure((Action)null!)); + } + + [Fact] + public void AddHttpContent_ShouldAddInstanceToHttpContentOptionsRegistry() + { + var testKey = "TestKey"; + using var testHttpContent = new StringContent("Test Content"); + var services = new ServiceCollection(); + var faultInjectionOptionsBuilder = new HttpFaultInjectionOptionsBuilder(services); + faultInjectionOptionsBuilder.AddHttpContent(testKey, testHttpContent); + + using var provider = services.BuildServiceProvider(); + var httpContentOptions = provider.GetRequiredService>().Get(testKey); + + Assert.Equal(httpContentOptions.HttpContent, testHttpContent); + } + + [Fact] + public void AddHttpContent_NullExceptionInstance_ShouldThrow() + { + var testKey = "TestKey"; + HttpContent? testContent = null!; + + var services = new ServiceCollection(); + var faultInjectionOptionsBuilder = new HttpFaultInjectionOptionsBuilder(services); + + Assert.Throws(() => + faultInjectionOptionsBuilder.AddHttpContent(testKey, testContent)); + } + + [Fact] + public void AddHttpContent_KeyNullOrWhiteSpace_ShouldThrow() + { + string? testKey = null!; + using var testHttpContent = new StringContent("Test Content"); + var services = new ServiceCollection(); + var faultInjectionOptionsBuilder = new HttpFaultInjectionOptionsBuilder(services); + + Assert.Throws(() => + faultInjectionOptionsBuilder.AddHttpContent(testKey, testHttpContent)); + + testKey = ""; + Assert.Throws(() => + faultInjectionOptionsBuilder.AddHttpContent(testKey, testHttpContent)); + } + + [Fact] + public void AddExceptionForFaultInjection_ShouldAddInstanceToExceptionRegistryOptions() + { + var testExceptionKey = "TestExceptionKey"; + var testExceptionInstance = new InjectedFaultException(); + var services = new ServiceCollection(); + var faultInjectionOptionsBuilder = new HttpFaultInjectionOptionsBuilder(services); + faultInjectionOptionsBuilder.AddException(testExceptionKey, testExceptionInstance); + + using var provider = services.BuildServiceProvider(); + var faultInjectionExceptionOptions = provider.GetRequiredService>().Get(testExceptionKey); + + Assert.Equal(faultInjectionExceptionOptions.Exception, testExceptionInstance); + } + + [Fact] + public void AddExceptionForFaultInjection_NullExceptionInstance_ShouldThrow() + { + var testExceptionKey = "TestExceptionKey"; + Exception? testExceptionInstance = null!; + + var services = new ServiceCollection(); + var faultInjectionOptionsBuilder = new HttpFaultInjectionOptionsBuilder(services); + + Assert.Throws(() => + faultInjectionOptionsBuilder.AddException(testExceptionKey, testExceptionInstance)); + } + + [Fact] + public void AddExceptionForFaultInjection_KeyNullOrWhiteSpace_ShouldThrow() + { + string? testExceptionKey = null!; + var testExceptionInstance = new InjectedFaultException(); + var services = new ServiceCollection(); + var faultInjectionOptionsBuilder = new HttpFaultInjectionOptionsBuilder(services); + + Assert.Throws(() => + faultInjectionOptionsBuilder.AddException(testExceptionKey, testExceptionInstance)); + + testExceptionKey = ""; + Assert.Throws(() => + faultInjectionOptionsBuilder.AddException(testExceptionKey, testExceptionInstance)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/FaultInjectionTelemetryHandlerTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/FaultInjectionTelemetryHandlerTests.cs new file mode 100644 index 0000000000..55880f41da --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/FaultInjectionTelemetryHandlerTests.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Resilience.FaultInjection; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metering; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Internal.Test; + +public class FaultInjectionTelemetryHandlerTests +{ + private const string MetricName = @"R9\Resilience\FaultInjection\HttpClient\InjectedFaults"; + + [Fact] + public void LogAndMeter_WithHttpContentKey() + { + var logger = Mock.Of>(); + + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + var counter = meter.CreateCounter(MetricName); + var metricCounter = new HttpClientFaultInjectionMetricCounter(counter); + + const string GroupName = "TestClient"; + const string FaultType = "Type"; + const string InjectedValue = "Value"; + const string HttpContentKey = "HttpContentKey"; + + FaultInjectionTelemetryHandler.LogAndMeter(logger, metricCounter, GroupName, FaultType, InjectedValue, HttpContentKey); + + var latest = metricCollector.GetCounterValues(MetricName)!.LatestWritten; + + Assert.NotNull(latest); + Assert.Equal(1, latest.Value); + Assert.Equal(GroupName, latest.GetDimension(FaultInjectionEventMeterDimensions.FaultInjectionGroupName)); + Assert.Equal(FaultType, latest.GetDimension(FaultInjectionEventMeterDimensions.FaultType)); + Assert.Equal(InjectedValue, latest.GetDimension(FaultInjectionEventMeterDimensions.InjectedValue)); + Assert.Equal(HttpContentKey, latest.GetDimension(FaultInjectionEventMeterDimensions.HttpContentKey)); + } + + [Fact] + public void LogAndMeter_WithoutHttpContentKey() + { + var logger = Mock.Of>(); + + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + var counter = meter.CreateCounter(MetricName); + var metricCounter = new HttpClientFaultInjectionMetricCounter(counter); + + const string GroupName = "TestClient"; + const string FaultType = "Type"; + const string InjectedValue = "Value"; + const string HttpContentKey = "N/A"; + + FaultInjectionTelemetryHandler.LogAndMeter(logger, metricCounter, GroupName, FaultType, InjectedValue, httpContentKey: null); + + var latest = metricCollector.GetCounterValues(MetricName)!.LatestWritten; + + Assert.NotNull(latest); + Assert.Equal(1, latest.Value); + Assert.Equal(GroupName, latest.GetDimension(FaultInjectionEventMeterDimensions.FaultInjectionGroupName)); + Assert.Equal(FaultType, latest.GetDimension(FaultInjectionEventMeterDimensions.FaultType)); + Assert.Equal(InjectedValue, latest.GetDimension(FaultInjectionEventMeterDimensions.InjectedValue)); + Assert.Equal(HttpContentKey, latest.GetDimension(FaultInjectionEventMeterDimensions.HttpContentKey)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/FaultInjectionWeightAssignmentContextMessageHandlerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/FaultInjectionWeightAssignmentContextMessageHandlerTest.cs new file mode 100644 index 0000000000..5cb94934e0 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/FaultInjectionWeightAssignmentContextMessageHandlerTest.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.FaultInjection; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Internal.Test; + +public class FaultInjectionWeightAssignmentContextMessageHandlerTest +{ + [Fact] + public async Task SendAsync_WithContext() + { + var services = new ServiceCollection(); + var httpClientIdentifier = "HttpClientClass"; + + services + .AddHttpClient() + .AddHttpMessageHandler(services => + { + var weightAssignmentsOptions = services.GetRequiredService>(); + return ActivatorUtilities.CreateInstance(services, httpClientIdentifier, weightAssignmentsOptions); + }) + .AddHttpMessageHandler(() => new TestMessageHandler()); + + var clientFactory = services + .BuildServiceProvider() + .GetRequiredService(); + var httpClient = clientFactory.CreateClient(httpClientIdentifier); + + using var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:12345"); + var context = new Context(); + request.SetPolicyExecutionContext(context); + var response = await httpClient.SendAsync(request); + + Assert.True(response.IsSuccessStatusCode); + } + + [Fact] + public async Task SendAsync_WithoutContext() + { + var services = new ServiceCollection(); + var httpClientIdentifier = "HttpClientClass"; + + services + .AddHttpClient() + .AddHttpMessageHandler(services => + { + var weightAssignmentsOptions = services.GetRequiredService>(); + return ActivatorUtilities.CreateInstance(services, httpClientIdentifier, weightAssignmentsOptions); + }) + .AddHttpMessageHandler(() => new TestMessageHandler()); + + var clientFactory = services + .BuildServiceProvider() + .GetRequiredService(); + var httpClient = clientFactory.CreateClient(httpClientIdentifier); + + using var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:12345"); + var response = await httpClient.SendAsync(request); + + Assert.True(response.IsSuccessStatusCode); + } + + private class HttpClientClass + { + } + + private class TestMessageHandler : DelegatingHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent("OK") }); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/HttpClientChaosPolicyFactoryTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/HttpClientChaosPolicyFactoryTest.cs new file mode 100644 index 0000000000..1a13b92131 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/HttpClientChaosPolicyFactoryTest.cs @@ -0,0 +1,195 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.FaultInjection.Internal; +using Microsoft.Extensions.Resilience.FaultInjection; +using Microsoft.Extensions.Telemetry.Metering; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Test; + +public class HttpClientChaosPolicyFactoryTest +{ + private readonly string _testOptionsGroupName = "TestGroupName"; + private readonly ChaosPolicyOptionsGroup _testChaosPolicyOptionsGroup; + private readonly IHttpClientChaosPolicyFactory _testPolicyFactory; + + public HttpClientChaosPolicyFactoryTest() + { + _testChaosPolicyOptionsGroup = new ChaosPolicyOptionsGroup + { + HttpResponseInjectionPolicyOptions = new HttpResponseInjectionPolicyOptions + { + Enabled = true, + FaultInjectionRate = 0.4 + } + }; + + var services = new ServiceCollection(); + Action action = + builder => builder.Configure( + options => + { + options.ChaosPolicyOptionsGroups.Add(_testOptionsGroupName, _testChaosPolicyOptionsGroup); + }); + services + .AddLogging() + .RegisterMetering() + .AddHttpClientFaultInjection(action); + + using var provider = services.BuildServiceProvider(); + _testPolicyFactory = provider.GetRequiredService(); + } + + [Fact] + public void CreateInjectHttpResponsePolicy_WithDelegateFunctions_ShouldReturnInstance() + { + var policy = _testPolicyFactory.CreateHttpResponsePolicy(); + Assert.NotNull(policy); + } + + [Fact] + public async Task GetEnabledAsync_ShouldReturnEnabled() + { + var context = new Context(); + context.WithFaultInjection(_testOptionsGroupName); + + var result = await ((HttpClientChaosPolicyFactory)_testPolicyFactory).GetEnabledAsync(context, CancellationToken.None); + Assert.Equal(_testChaosPolicyOptionsGroup!.HttpResponseInjectionPolicyOptions!.Enabled, result); + } + + [Fact] + public async Task GetEnabledAsync_ContextNoOptionsGroupName_ShouldReturnFalse() + { + var services = new ServiceCollection(); + services + .AddLogging() + .RegisterMetering() + .AddHttpClientFaultInjection(); + + using var provider = services.BuildServiceProvider(); + var testPolicyFactory = provider.GetRequiredService(); + + var context = new Context(); + + var result = await ((HttpClientChaosPolicyFactory)testPolicyFactory).GetEnabledAsync(context, CancellationToken.None); + Assert.False(result); + } + + [Fact] + public async Task GetEnabledAsync_NoOptionsGroupFound_ShouldReturnFalse() + { + var context = new Context(); + context.WithFaultInjection("RandomName"); + + var result = await ((HttpClientChaosPolicyFactory)_testPolicyFactory).GetEnabledAsync(context, CancellationToken.None); + Assert.False(result); + } + + [Fact] + public async Task GetEnabledAsync_WhenNoHttpResponseInjectionPolicyFoundInOptionsGroup_ShouldReturnFalse() + { + var testGroupName = "TestGroup"; + var tesOptionsGroupNoPolicyOptions = new ChaosPolicyOptionsGroup(); + var services = new ServiceCollection(); + Action action = + builder => builder.Configure( + options => + { + options.ChaosPolicyOptionsGroups.Add(testGroupName, tesOptionsGroupNoPolicyOptions); + }); + services + .AddLogging() + .RegisterMetering() + .AddHttpClientFaultInjection(action); + + using var provider = services.BuildServiceProvider(); + var testPolicyFactory = provider.GetRequiredService(); + + var context = new Context(); + context.WithFaultInjection(testGroupName); + + var result = await ((HttpClientChaosPolicyFactory)testPolicyFactory).GetEnabledAsync(context, CancellationToken.None); + Assert.False(result); + } + + [Fact] + public async Task GetInjectionRateAsync_ShouldReturnInjectionRate() + { + var context = new Context(); + context.WithFaultInjection(_testOptionsGroupName); + + var result = await ((HttpClientChaosPolicyFactory)_testPolicyFactory).GetInjectionRateAsync(context, CancellationToken.None); + Assert.Equal(_testChaosPolicyOptionsGroup!.HttpResponseInjectionPolicyOptions!.FaultInjectionRate, result); + } + + [Fact] + public async Task GetInjectionRateAsync_ContextNoOptionsGroupName_ShouldReturnZero() + { + var services = new ServiceCollection(); + services + .AddLogging() + .RegisterMetering() + .AddHttpClientFaultInjection(); + + using var provider = services.BuildServiceProvider(); + var testPolicyFactory = provider.GetRequiredService(); + + var context = new Context(); + + var result = await ((HttpClientChaosPolicyFactory)testPolicyFactory).GetInjectionRateAsync(context, CancellationToken.None); + Assert.Equal(0.0, result); + } + + [Fact] + public async Task GetInjectionRateAsync_NoOptionsGroupFound_ShouldReturnZero() + { + var context = new Context(); + context.WithFaultInjection("RandomName"); + + var result = await ((HttpClientChaosPolicyFactory)_testPolicyFactory).GetInjectionRateAsync(context, CancellationToken.None); + Assert.Equal(0.0, result); + } + + [Fact] + public async Task GetInjectionRateAsync_WhenNoHttpResponseInjectionPolicyFoundInOptionsGroup_ShouldReturnZero() + { + var testGroupName = "TestGroup"; + var tesOptionsGroupNoPolicyOptions = new ChaosPolicyOptionsGroup(); + var services = new ServiceCollection(); + Action action = + builder => builder.Configure( + options => + { + options.ChaosPolicyOptionsGroups.Add(testGroupName, tesOptionsGroupNoPolicyOptions); + }); + services + .AddLogging() + .RegisterMetering() + .AddHttpClientFaultInjection(action); + + using var provider = services.BuildServiceProvider(); + var testPolicyFactory = provider.GetRequiredService(); + + var context = new Context(); + context.WithFaultInjection(testGroupName); + + var result = await ((HttpClientChaosPolicyFactory)testPolicyFactory).GetInjectionRateAsync(context, CancellationToken.None); + Assert.Equal(0.0, result); + } + + [Fact] + public async Task GetHttpResponseMessageAsync_ShouldReturnHttpResponseMessage() + { + var context = new Context(); + context.WithFaultInjection(_testOptionsGroupName); + + var result = await ((HttpClientChaosPolicyFactory)_testPolicyFactory).GetHttpResponseMessageAsync(context, CancellationToken.None); + Assert.Equal(_testChaosPolicyOptionsGroup!.HttpResponseInjectionPolicyOptions!.StatusCode, result.StatusCode); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/HttpContentOptionsRegistryTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/HttpContentOptionsRegistryTest.cs new file mode 100644 index 0000000000..f1f82fbdb4 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/HttpContentOptionsRegistryTest.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.FaultInjection.Internal; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Test; + +public class HttpContentOptionsRegistryTest +{ + [Fact] + public void GetHttpContent_NullKey_ReturnNullResult() + { + var services = new ServiceCollection(); + services.AddHttpClientFaultInjection(); + + using var provider = services.BuildServiceProvider(); + var registry = provider.GetRequiredService(); + + var result = registry.GetHttpContent(null!); + Assert.Null(result); + } + + [Fact] + public void GetHttpContent_RegisteredKey_ShouldReturnInstance() + { + var testKey = "TestKey"; + using var testHttpContent = new StringContent("Test Content"); + var services = new ServiceCollection(); + services.AddHttpClientFaultInjection(); + + var faultInjectionOptionsBuilder = new HttpFaultInjectionOptionsBuilder(services); + faultInjectionOptionsBuilder.AddHttpContent(testKey, testHttpContent); + + using var provider = services.BuildServiceProvider(); + var registry = provider.GetRequiredService(); + + var result = registry.GetHttpContent(testKey); + Assert.Equal(testHttpContent, result); + } + + [Fact] + public void GetException_UnregisteredKey_ShouldReturnNull() + { + var services = new ServiceCollection(); + services.AddHttpClientFaultInjection(); + + using var provider = services.BuildServiceProvider(); + var registry = provider.GetRequiredService(); + + var result = registry.GetHttpContent("testingtesting"); + Assert.Null(result); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/PolicyContextExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/PolicyContextExtensionsTest.cs new file mode 100644 index 0000000000..41149026df --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/PolicyContextExtensionsTest.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Test; + +public class PolicyContextExtensionsTest +{ + [Fact] + public void WithCallingRequestMessage_ShouldSetRequestMessageInContext() + { + var context = new Context(); + var method = HttpMethod.Get; + var testUri = new Uri("https://localhost:12345"); + using var testHttpRequest = new HttpRequestMessage(method, testUri); + + context.WithCallingRequestMessage(testHttpRequest); + var result = context.GetCallingRequestMessage(); + Assert.Equal(testHttpRequest, result); + } + + [Fact] + public void WithCallingRequestMessage_NullContext_ShouldThrow() + { + var method = HttpMethod.Get; + var testUri = new Uri("https://localhost:12345"); + using var testHttpRequest = new HttpRequestMessage(method, testUri); + + Context? context = null!; + Assert.Throws(() => context.WithCallingRequestMessage(testHttpRequest)); + } + + [Fact] + public void WithCallingRequestMessage_NullRequest_ShouldThrow() + { + var context = new Context(); + Assert.Throws(() => context.WithCallingRequestMessage(null!)); + } + + [Fact] + public void GetCallingRequestMessage_RequestMessageNotSet_ShouldReturnNull() + { + var context = new Context(); + + var result = context.GetCallingRequestMessage(); + Assert.Null(result); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HedgingTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HedgingTests.cs new file mode 100644 index 0000000000..93590e6054 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HedgingTests.cs @@ -0,0 +1,291 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Resilience.Internal.Routing; +using Microsoft.Extensions.Telemetry.Metering; +using Moq; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Hedging; + +#pragma warning disable CA1063 // Implement IDisposable Correctly +#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize + +public abstract class HedgingTests : IDisposable +{ + public const string ClientId = "clientId"; + + public const int DefaultHedgingAttempts = 3; + + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly Mock _requestCloneHandlerMock; + private readonly Mock _requestRoutingStrategyMock; + private readonly Mock _requestRoutingStrategyFactoryMock; + private readonly IServiceCollection _services; + private readonly List _requests = new(); + private readonly Queue _responses = new(); + private readonly Func _createDefaultBuilder; + private bool _failure; + + protected HedgingTests(Func createDefaultBuilder) + { + _cancellationTokenSource = new CancellationTokenSource(); + _requestCloneHandlerMock = new Mock(MockBehavior.Strict); + _requestRoutingStrategyMock = new Mock(MockBehavior.Strict); + _requestRoutingStrategyFactoryMock = new Mock(MockBehavior.Strict); + + _services = new ServiceCollection().RegisterMetering().AddLogging(); + _services.AddSingleton(_requestCloneHandlerMock.Object); + _services.AddSingleton(NullRedactorProvider.Instance); + + var httpClient = _services.AddHttpClient(ClientId); + + Builder = createDefaultBuilder(httpClient, _requestRoutingStrategyFactoryMock.Object); + _ = httpClient.AddHttpMessageHandler(() => new TestHandlerStub(InnerHandlerFunction)); + _createDefaultBuilder = createDefaultBuilder; + } + + public TBuilder Builder { get; private set; } + + public void Dispose() + { + _requestCloneHandlerMock.VerifyAll(); + _requestRoutingStrategyMock.VerifyAll(); + _requestRoutingStrategyFactoryMock.VerifyAll(); + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); + } + + [Fact] + public void AddHedging_EnsureRequestCloner() + { + var services = new ServiceCollection(); + + _createDefaultBuilder(services.AddHttpClient("dummy"), _requestRoutingStrategyFactoryMock.Object); + + Assert.NotNull(services.BuildServiceProvider().GetRequiredService()); + } + + [Fact] + public async Task SendAsync_EnsureContextFlows() + { + using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); + var context = new Context { ["custom-data"] = "my-data" }; + request.SetPolicyExecutionContext(context); + var calls = 0; + + SetupRouting(); + SetupRoutes(3, "https://enpoint-{0}:80"); + _services.RemoveAll(); + _services.AddRequestCloner(); + ConfigureHedgingOptions(options => + { + options.OnHedgingAsync = args => + { + Assert.Equal("my-data", (string)args.Context["custom-data"]); + calls++; + return Task.CompletedTask; + }; + }); + + AddResponse(HttpStatusCode.InternalServerError); + AddResponse(HttpStatusCode.InternalServerError); + AddResponse(HttpStatusCode.InternalServerError); + + using var client = CreateClientWithHandler(); + + await client.SendAsync(request, _cancellationTokenSource.Token); + + Assert.Equal(3, calls); + } + + [Fact] + public async Task SendAsync_NoErrors_ShouldReturnSingleResponse() + { + SetupRouting(); + SetupRoutes(1, "https://enpoint-{0}:80/"); + using var client = CreateClientWithHandler(); + using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); + SetupCloner(request, false); + + AddResponse(HttpStatusCode.OK); + + var response = await client.SendAsync(request, _cancellationTokenSource.Token); + Assert.Empty(_responses); + + Assert.Single(_requests); + Assert.Equal("https://enpoint-1:80/some-path?query", _requests[0]); + } + + [Fact] + public async Task SendAsync_NoRoutes_Throws() + { + using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); + + SetupRouting(false); + SetupRoutes(0); + + _failure = true; + + using var client = CreateClientWithHandler(); + + var exception = await Assert.ThrowsAsync(async () => await client.SendAsync(request, _cancellationTokenSource.Token)); + Assert.Equal("The routing strategy did not provide any route URL on the first attempt.", exception.Message); + } + + [Fact] + public async Task SendAsync_NoRoutesLeftAndNoResult_ShouldThrow() + { + using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); + + SetupRouting(); + SetupCloner(request, true); + SetupRoutes(2); + + _failure = true; + + using var client = CreateClientWithHandler(); + + var exception = await Assert.ThrowsAsync(async () => await client.SendAsync(request, _cancellationTokenSource.Token)); + Assert.Equal("Something went wrong!", exception.Message); + + Assert.Equal(2, _requests.Count); + Assert.Equal(2, _requests.Distinct().Count()); + } + + [Fact] + public async Task SendAsync_NoRoutesLeftAndSomeResultPresent_ShouldReturn() + { + using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); + + SetupRouting(); + SetupCloner(request, true); + SetupRoutes(4); + + AddResponse(HttpStatusCode.InternalServerError); + AddResponse(HttpStatusCode.InternalServerError); + AddResponse(HttpStatusCode.ServiceUnavailable); + + using var client = CreateClientWithHandler(); + + var result = await client.SendAsync(request, _cancellationTokenSource.Token); + Assert.Equal(DefaultHedgingAttempts, _requests.Count); + Assert.Equal(HttpStatusCode.ServiceUnavailable, result.StatusCode); + } + + [Fact] + public async Task SendAsync_NoRoutesLeft_EnsureLessThanMaxHedgedAttempts() + { + using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); + + SetupRouting(); + SetupCloner(request, true); + SetupRoutes(2); + + AddResponse(HttpStatusCode.InternalServerError); + AddResponse(HttpStatusCode.InternalServerError); + AddResponse(HttpStatusCode.InternalServerError); + AddResponse(HttpStatusCode.InternalServerError); + + using var client = CreateClientWithHandler(); + + var result = await client.SendAsync(request, _cancellationTokenSource.Token); + Assert.Equal(2, _requests.Count); + + _requestCloneHandlerMock.Verify(o => o.CreateSnapshot(It.IsAny()), Times.Exactly(1)); + } + + [Fact] + public async Task SendAsync_FailedExecution_ShouldReturnResponseFromHedging() + { + using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); + + SetupRouting(); + SetupCloner(request, true); + SetupRoutes(3, "https://enpoint-{0}:80"); + + AddResponse(HttpStatusCode.InternalServerError); + AddResponse(HttpStatusCode.ServiceUnavailable); + AddResponse(HttpStatusCode.OK); + + using var client = CreateClientWithHandler(); + + var result = await client.SendAsync(request, _cancellationTokenSource.Token); + Assert.Equal(3, _requests.Count); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal("https://enpoint-1:80/some-path?query", _requests[0]); + Assert.Equal("https://enpoint-2:80/some-path?query", _requests[1]); + Assert.Equal("https://enpoint-3:80/some-path?query", _requests[2]); + } + + protected void AddResponse(HttpStatusCode statusCode) + { +#pragma warning disable CA2000 // Dispose objects before losing scope + _responses.Enqueue(new HttpResponseMessage(statusCode)); +#pragma warning restore CA2000 // Dispose objects before losing scope + } + + protected abstract void ConfigureHedgingOptions(Action configure); + + protected System.Net.Http.HttpClient CreateClientWithHandler() => _services.BuildServiceProvider().GetRequiredService().CreateClient(ClientId); + + protected void SetupCloner(HttpRequestMessage request, bool createCalled) + { + var snapshot = createCalled ? + Mock.Of(v => v.Create() == request) : + Mock.Of(); + + _requestCloneHandlerMock + .Setup(mock => mock.CreateSnapshot(It.IsAny())) + .Returns(snapshot); + } + + private Task InnerHandlerFunction(HttpRequestMessage request, CancellationToken cancellationToken) + { + _requests.Add(request.RequestUri!.ToString()); + + if (_failure) + { + throw new HttpRequestException("Something went wrong!"); + } + + return Task.FromResult(_responses.Dequeue()); + } + + private void SetupRoutes(int totalAttempts, string pattern = "https://dummy-{0}") + { + int attemptCount = 0; + + Uri? outUri = null; + _requestRoutingStrategyMock + .Setup(mock => mock.TryGetNextRoute(out outUri)) + .Callback((out Uri? uri) => + { + attemptCount++; + uri = new Uri(string.Format(CultureInfo.InvariantCulture, pattern, attemptCount)); + }) + .Returns(() => attemptCount <= totalAttempts); + } + + private void SetupRouting(bool mustReturn = true) + { + _requestRoutingStrategyFactoryMock.Setup(s => s.CreateRoutingStrategy()).Returns(() => _requestRoutingStrategyMock.Object); + if (mustReturn) + { + _requestRoutingStrategyFactoryMock.Setup(s => s.ReturnRoutingStrategy(_requestRoutingStrategyMock.Object)).Verifiable(); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/ConfigurationStubFactory.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/ConfigurationStubFactory.cs new file mode 100644 index 0000000000..26f9a50a60 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/ConfigurationStubFactory.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Extensions.Http.Resilience.Test.Hedgings.Helpers; + +public sealed class ConfigurationStubFactory +{ + public static IConfiguration Create(Dictionary collection) + { + return new ConfigurationBuilder() + .AddInMemoryCollection(collection) + .Build(); + } + + public static IConfiguration CreateEmpty() + { + return new ConfigurationBuilder().Build(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/OptionsUtilities.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/OptionsUtilities.cs new file mode 100644 index 0000000000..b48ec57501 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/OptionsUtilities.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; + +namespace Microsoft.Extensions.Http.Resilience.Test.Hedging; + +internal static class OptionsUtilities +{ + public static void ValidateOptions(object options) + { + var context = new ValidationContext(options); + Validator.ValidateObject(options, context, true); + } + + public static bool EqualOptions(T options1, T options2) + { + if (options1 is null && options2 is null) + { + return true; + } + + if (options1 is null || options2 is null) + { + return false; + } + + var propertiesValuesByName1 = options1.GetPropertiesValuesByName(); + var propertiesValuesByName2 = options2.GetPropertiesValuesByName(); + + foreach (var propertyDefinition1 in propertiesValuesByName1) + { + var propertyName = propertyDefinition1.Key; + var propertyValue1 = propertyDefinition1.Value; + + if (!propertiesValuesByName2.TryGetValue(propertyName, out var propertyValue2) || + !Equals(propertyValue1, propertyValue2)) + { + return false; + } + } + + return true; + } + + private static IDictionary GetPropertiesValuesByName(this T options) + { + return options! + .GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .GroupBy(property => property.Name) + .ToDictionary( + propertyGroup => propertyGroup.Key, + propertyGroup => propertyGroup.Last().GetValue(options)!); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/TestHandlerStub.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/TestHandlerStub.cs new file mode 100644 index 0000000000..53ab6157c8 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/TestHandlerStub.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Http.Resilience.Test.Hedging; + +public class TestHandlerStub : DelegatingHandler +{ + private readonly Func> _handlerFunc; + + public TestHandlerStub(Func> handlerFunc) + { + _handlerFunc = handlerFunc; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return _handlerFunc(request, cancellationToken); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpClientHedgingResiliencePredicatesTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpClientHedgingResiliencePredicatesTests.cs new file mode 100644 index 0000000000..45035a4d93 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpClientHedgingResiliencePredicatesTests.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using Polly.CircuitBreaker; +using Polly.Timeout; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Hedging; + +public class HttpClientHedgingResiliencePredicatesTests +{ + [Fact] + public void IsTransientException_Ok() + { + Assert.True(HttpClientHedgingResiliencePredicates.IsTransientHttpException(new TimeoutRejectedException())); + Assert.True(HttpClientHedgingResiliencePredicates.IsTransientHttpException(new BrokenCircuitException())); + Assert.True(HttpClientHedgingResiliencePredicates.IsTransientHttpException(new HttpRequestException())); + Assert.False(HttpClientHedgingResiliencePredicates.IsTransientHttpException(new InvalidOperationException())); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpHedgingPolicyOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpHedgingPolicyOptionsTests.cs new file mode 100644 index 0000000000..62b9dca196 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpHedgingPolicyOptionsTests.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using Microsoft.Extensions.Resilience.Options; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Hedging; + +public class HttpHedgingPolicyOptionsTests +{ +#pragma warning disable S2330 + public static readonly IEnumerable HandledExceptionsClassified = new[] + { + new object[] { new InvalidCastException(), false }, + new object[] { new HttpRequestException(), true } + }; + + [Theory] + [InlineData(HttpStatusCode.OK, false)] + [InlineData(HttpStatusCode.BadRequest, false)] + [InlineData(HttpStatusCode.RequestEntityTooLarge, false)] + [InlineData(HttpStatusCode.InternalServerError, true)] + [InlineData(HttpStatusCode.HttpVersionNotSupported, true)] + [InlineData(HttpStatusCode.RequestTimeout, true)] + public void ShouldHandleResultAsError_DefaultInstance_ShouldClassify( + HttpStatusCode statusCode, + bool expected) + { + using var httpReq = new HttpResponseMessage { StatusCode = statusCode }; + var actual = new HttpHedgingPolicyOptions().ShouldHandleResultAsError(httpReq); + + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(nameof(HandledExceptionsClassified))] + public void ShouldHandleException_DefaultInstance_ShouldClassify( + Exception exception, + bool expected) + { + var actual = new HttpHedgingPolicyOptions().ShouldHandleException(exception); + + Assert.Equal(expected, actual); + } + + [Fact] + public void OnHedging_CallsRecordMetric() + { + var options = new HttpHedgingPolicyOptions(); + var expectedError = "Something went wrong"; + var delegateResult = new DelegateResult(new InvalidOperationException(expectedError)); + + Assert.NotNull(options.OnHedgingAsync( + new HedgingTaskArguments( + delegateResult, + new Context(), + 0, + CancellationToken.None))); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/DefaultRoutingStrategyFactoryTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/DefaultRoutingStrategyFactoryTests.cs new file mode 100644 index 0000000000..96fd4d0b78 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/DefaultRoutingStrategyFactoryTests.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.Internal.Routing; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Hedging.Internals; + +public sealed class DefaultRoutingStrategyFactoryTests : IDisposable +{ + private const string ClientName = "clientName"; + private readonly Uri _routeUri = new("https://bing.com"); + private readonly Mock _mockService = new(); + public void Dispose() + { + _mockService.VerifyAll(); + } + + [Fact] + public void CreateRoutingStrategy_ShouldCreateRoutingStrategyAndPassTheName() + { + var serviceCollection = new ServiceCollection(); + _mockService.Setup(s => s.Route).Returns(_routeUri); + + serviceCollection.AddSingleton(_mockService.Object); + using var provider = serviceCollection.BuildServiceProvider(); + var factory = new DefaultRoutingStrategyFactory(ClientName, provider); + + var routingStrategy = factory.CreateRoutingStrategy(); + Uri? resultRouteUri = null; + routingStrategy?.TryGetNextRoute(out resultRouteUri); + + var mockRoutingStrategy = routingStrategy as MockRoutingStrategy; + + Assert.NotNull(mockRoutingStrategy); + + Assert.Equal(ClientName, mockRoutingStrategy?.Name); + Assert.Equal(_routeUri, resultRouteUri!); + } + + [Fact] + public void CreateRoutingStrategy_WhenServiceIsNotInjectedShouldThrow() + { + var serviceCollection = new ServiceCollection(); + using var provider = serviceCollection.BuildServiceProvider(); + + var factory = new DefaultRoutingStrategyFactory(ClientName, provider); + + Assert.Throws(() => + { + factory.CreateRoutingStrategy(); + }); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/HedgingContextExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/HedgingContextExtensionsTests.cs new file mode 100644 index 0000000000..e2ae5f6238 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/HedgingContextExtensionsTests.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Http.Resilience.Internal; +using Moq; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Hedging.Internals; + +public class HedgingContextExtensionsTests +{ + [Fact] + public void GetSet_RoutingStrategy_Ok() + { + var setter = HedgingContextExtensions.CreateRoutingStrategySetter("my-pipeline"); + var getter = HedgingContextExtensions.CreateRoutingStrategyProvider("my-pipeline"); + var getterInvalid = HedgingContextExtensions.CreateRoutingStrategyProvider("my-other-pipeline"); + + var context = new Context(); + var strategy = Mock.Of(); + + setter(context, strategy); + + Assert.Equal(strategy, getter(context)); + Assert.Null(getterInvalid(context)); + } + + [Fact] + public void GetSet_HttpRequestMessageSnapshot_Ok() + { + var setter = HedgingContextExtensions.CreateRequestMessageSnapshotSetter("my-pipeline"); + var getter = HedgingContextExtensions.CreateRequestMessageSnapshotProvider("my-pipeline"); + var getterInvalid = HedgingContextExtensions.CreateRequestMessageSnapshotProvider("my-other-pipeline"); + + var context = new Context(); + var snapshot = Mock.Of(); + + setter(context, snapshot); + + Assert.Equal(snapshot, getter(context)); + Assert.Null(getterInvalid(context)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/IStubRoutingService.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/IStubRoutingService.cs new file mode 100644 index 0000000000..d6e7ab85ef --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/IStubRoutingService.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Http.Resilience.Test.Hedging.Internals; + +public interface IStubRoutingService +{ + Uri Route { get; } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/MockRoutingStrategy.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/MockRoutingStrategy.cs new file mode 100644 index 0000000000..ce934adbd3 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/MockRoutingStrategy.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Http.Resilience.Test.Hedging.Internals; + +// Can't use NotNullWhenAttribute since it's defined in two reference assemblies with InternalVisibleTo +#pragma warning disable CS8767 + +internal class MockRoutingStrategy : IRequestRoutingStrategy +{ + private readonly IStubRoutingService _mockService; + + public MockRoutingStrategy(IStubRoutingService mockService, string name) + { + _mockService = mockService; + Name = name; + } + + public string Name { get; private set; } + + public bool TryGetNextRoute(out Uri? nextRoute) + { + nextRoute = _mockService.Route; + return true; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/RandomizerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/RandomizerTest.cs new file mode 100644 index 0000000000..25511c30b2 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/RandomizerTest.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.Resilience.Internal; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Hedging.Internals; + +public class RandomizerTest +{ + [Fact] + public void ParallelCalls_ShouldNotResultInException() + { + var randomizer = new Randomizer(); + var actions = Enumerable.Range(0, 1000).Select(_ => + { +#pragma warning disable S3257 // Declarations and initializations should be as concise as possible + return new Action(() => + { + randomizer.NextInt(10000); + randomizer.NextDouble(10000); + }); + }).ToArray(); +#pragma warning restore S3257 // Declarations and initializations should be as concise as possible + + Parallel.Invoke(actions); + } + + [Fact] + public void NextDouble_Ok() + { + var randomizer = new Randomizer(); + var ok = false; + + for (int i = 0; i < 10; i++) + { + if (randomizer.NextDouble(100000) > 1) + { + ok = true; + break; + } + } + + Assert.True(ok); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/RequestMessageSnapshotPolicyTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/RequestMessageSnapshotPolicyTests.cs new file mode 100644 index 0000000000..aeefc7bbfa --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/RequestMessageSnapshotPolicyTests.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Http.Resilience.Internal; +using Moq; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Hedging.Internals; + +public class RequestMessageSnapshotPolicyTests +{ + [Fact] + public async Task SendAsync_EnsureSnapshotAttached() + { + var snapshot = new Mock(MockBehavior.Strict); + snapshot.Setup(s => s.Dispose()); + var cloner = new Mock(MockBehavior.Strict); + cloner.Setup(c => c.CreateSnapshot(It.IsAny())).Returns(snapshot.Object); + var policy = new RequestMessageSnapshotPolicy("dummy", cloner.Object); + var context = new Context + { + ["Resilience.ContextExtensions.Request-dummy"] = new HttpRequestMessage() + }; + + await policy.ExecuteAsync(_ => Task.FromResult(new HttpResponseMessage()), context); + + HedgingContextExtensions.CreateRequestMessageSnapshotProvider("dummy")(context).Should().Be(snapshot.Object); + cloner.VerifyAll(); + snapshot.VerifyAll(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/RoutingHelperTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/RoutingHelperTest.cs new file mode 100644 index 0000000000..e4ee320ae2 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/RoutingHelperTest.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Resilience.Internal.Routing; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Hedging.Internals; + +public class RoutingHelperTest +{ + [InlineData(1.5d, 2)] + [InlineData(0d, 1)] + [Theory] + public void SelectEndpoint_Ok(double nextResult, int expectedEndpoint) + { + var randomizer = new Mock(MockBehavior.Strict); + randomizer.Setup(v => v.NextDouble(10)).Returns(nextResult); + + var result = RoutingHelper.SelectByWeight(new List { 1, 2, 3, 4 }, v => v, randomizer.Object); + + Assert.Equal(expectedEndpoint, result); + } + + [Fact] + public void SelectEndpoint_Invalid() + { + var randomizer = new Mock(MockBehavior.Strict); + randomizer.Setup(v => v.NextDouble(10)).Returns(10000); + + Assert.Throws(() => RoutingHelper.SelectByWeight(new List { 1, 2, 3, 4 }, v => v, randomizer.Object)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/Validators/HttpStandardHedgingResilienceOptionsCustomValidatorTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/Validators/HttpStandardHedgingResilienceOptionsCustomValidatorTests.cs new file mode 100644 index 0000000000..a7ac029d1e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/Validators/HttpStandardHedgingResilienceOptionsCustomValidatorTests.cs @@ -0,0 +1,130 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +#if NET6_0_OR_GREATER +using System.Linq; +#endif +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.Internal.Validators; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Internals.Validators; +public class HttpStandardHedgingResilienceOptionsCustomValidatorTests +{ + [Fact] + public void Validate_InvalidOptions_EnsureValidationErrors() + { + HttpStandardHedgingResilienceOptions options = new(); + options.EndpointOptions.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromSeconds(1); + options.TotalRequestTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(1); + + var validationResult = CreateValidator("dummy").Validate("dummy", options); + + Assert.True(validationResult.Failed); + +#if NET6_0_OR_GREATER + Assert.Equal(3, validationResult.Failures.Count()); +#endif + } + + [Fact] + public void Validate_ValidOptions_NoValidationErrors() + { + HttpStandardHedgingResilienceOptions options = new(); + + var validationResult = CreateValidator("dummy").Validate("dummy", options); + + Assert.True(validationResult.Succeeded); + } + + [Fact] + public void Validate_ValidOptionsWithoutRouting_ValidationErrors() + { + HttpStandardHedgingResilienceOptions options = new(); + + var validationResult = CreateValidator("dummy").Validate("other", options); + + Assert.True(validationResult.Failed); + Assert.Equal("The hedging routing is not configured for 'other' HTTP client.", validationResult.FailureMessage); + + } + + public static IEnumerable GetOptions_ValidOptions_EnsureNoErrors_Data + { + get + { + var options = new HttpStandardHedgingResilienceOptions(); + options.EndpointOptions.TimeoutOptions.TimeoutInterval = options.TotalRequestTimeoutOptions.TimeoutInterval; + options.EndpointOptions.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromMilliseconds(options.EndpointOptions.TimeoutOptions.TimeoutInterval.TotalMilliseconds * 2); + yield return new object[] { options }; + + options = new HttpStandardHedgingResilienceOptions(); + options.EndpointOptions.TimeoutOptions.TimeoutInterval = options.TotalRequestTimeoutOptions.TimeoutInterval; + options.EndpointOptions.CircuitBreakerOptions.SamplingDuration = + TimeSpan.FromMilliseconds(options.EndpointOptions.TimeoutOptions.TimeoutInterval.TotalMilliseconds * 2) + TimeSpan.FromMilliseconds(10); + yield return new object[] { options }; + + options = new HttpStandardHedgingResilienceOptions(); + options.HedgingOptions.MaxHedgedAttempts = 1; + options.HedgingOptions.HedgingDelay = options.TotalRequestTimeoutOptions.TimeoutInterval; + yield return new object[] { options }; + + options = new HttpStandardHedgingResilienceOptions(); + options.HedgingOptions.HedgingDelay = TimeSpan.FromDays(1); + options.HedgingOptions.HedgingDelayGenerator = _ => TimeSpan.FromDays(1); + yield return new object[] { options }; + } + } + + [MemberData(nameof(GetOptions_ValidOptions_EnsureNoErrors_Data))] + [Theory] + public void Validate_ValidOptions_EnsureNoErrors(HttpStandardHedgingResilienceOptions options) + { + var validationResult = CreateValidator("dummy").Validate("dummy", options); + + Assert.False(validationResult.Failed); + } + + public static IEnumerable GetOptions_InvalidOptions_EnsureErrors_Data + { + get + { + var options = new HttpStandardHedgingResilienceOptions(); + options.TotalRequestTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(2); + options.EndpointOptions.TimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(3); + yield return new object[] { options }; + + options = new HttpStandardHedgingResilienceOptions(); + options.TotalRequestTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(2); + yield return new object[] { options }; + + options = new HttpStandardHedgingResilienceOptions(); + options.HedgingOptions.HedgingDelay = TimeSpan.FromDays(1); + yield return new object[] { options }; + + options = new HttpStandardHedgingResilienceOptions(); + options.EndpointOptions.TimeoutOptions.TimeoutInterval = options.TotalRequestTimeoutOptions.TimeoutInterval; + options.EndpointOptions.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromMilliseconds(options.EndpointOptions.TimeoutOptions.TimeoutInterval.TotalMilliseconds / 2); + yield return new object[] { options }; + } + } + + [MemberData(nameof(GetOptions_InvalidOptions_EnsureErrors_Data))] + [Theory] + public void Validate_InvalidOptions_EnsureErrors(HttpStandardHedgingResilienceOptions options) + { + var validationResult = CreateValidator("dummy").Validate("dummy", options); + + Assert.True(validationResult.Failed); + } + + private static HttpStandardHedgingResilienceOptionsCustomValidator CreateValidator(string name) + { + var mock = Mock.Of>(v => v.GetService(name) == Mock.Of()); + + return new HttpStandardHedgingResilienceOptionsCustomValidator(mock); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Routing/OrderedRoutingStrategyTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Routing/OrderedRoutingStrategyTest.cs new file mode 100644 index 0000000000..b529ab091d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Routing/OrderedRoutingStrategyTest.cs @@ -0,0 +1,117 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Resilience.Internal.Routing; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Hedging.Routing; + +public class OrderedRoutingStrategyTest : RoutingStrategyTest +{ + [Fact] + public void GetRoutes_EnsureExpectedOutput() + { + Randomizer.Setup(r => r.NextDouble(10)).Returns(1); + Randomizer.Setup(r => r.NextDouble(20)).Returns(2); + Randomizer.Setup(r => r.NextDouble(30)).Returns(3); + + StrategyResultHelper("https://a/", "https://b/", "https://c/"); + + Randomizer.VerifyAll(); + } + + [Fact] + public void Reload_Ok() + { + SetupRandomizer(1.0); + + ReloadHelper( + (b, c) => b.ConfigureOrderedGroups(c.GetSection("section")), + new() + { + { "section:groups:0:endpoints:0:uri", "https://a/" }, + }, + new() + { + { "section:groups:0:endpoints:0:uri", "https://b/" }, + }, + new[] { "https://a/" }, + new[] { "https://b/" }); + } + + protected override void Configure(IRoutingStrategyBuilder routingBuilder) + { + routingBuilder.ConfigureOrderedGroups(GetSection(new Dictionary + { + { "groups:0:endpoints:0:uri", "https://a/" }, + { "groups:0:endpoints:0:weight", "10" } + })); + + routingBuilder.ConfigureOrderedGroups(options => + { + var groups = new List(options.Groups) + { + CreateGroup(new WeightedEndpoint { Uri = new Uri("https://b/"), Weight = 20 }), + }; + options.Groups = groups; + }); + + routingBuilder.ConfigureOrderedGroups((options, serviceProvider) => + { + serviceProvider.Should().NotBeNull(); + options.Groups.Add(CreateGroup(new WeightedEndpoint { Uri = new Uri("https://c/"), Weight = 30 })); + }); + } + + protected override IEnumerable ConfigureMinRoutes(IRoutingStrategyBuilder routingBuilder) + { + routingBuilder.ConfigureOrderedGroups(options => options.Groups.Add(CreateGroup("https://dummy-route/"))); + + yield return "https://dummy-route/"; + } + + protected override IRequestRoutingStrategy CreateEmptyStrategy() => new OrderedGroupsRoutingStrategy(Mock.Of()); + + protected override IEnumerable> ConfigureInvalidRoutes() + { + yield return builder => builder.ConfigureOrderedGroups(options => { }); + + yield return builder => builder.ConfigureOrderedGroups(options => + { + var group = CreateGroup("https://dummy"); + group.Endpoints.Single().Weight = 0; + options.Groups.Add(group); + }); + + yield return builder => builder.ConfigureOrderedGroups(options => + { + var group = CreateGroup("https://dummy"); + group.Endpoints.Single().Weight = 99999; + options.Groups.Add(group); + }); + + yield return builder => builder.ConfigureOrderedGroups(options => + { + options.Groups.Add(null!); + }); + } + + private static EndpointGroup CreateGroup(params string[] endpoints) + { + return CreateGroup(endpoints.Select(v => new WeightedEndpoint { Uri = new Uri(v) }).ToArray()); + } + + private static EndpointGroup CreateGroup(params WeightedEndpoint[] endpoint) + { + return new EndpointGroup + { + Endpoints = endpoint.ToList() + }; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Routing/RoutingStrategyTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Routing/RoutingStrategyTest.cs new file mode 100644 index 0000000000..fc3b13c807 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Routing/RoutingStrategyTest.cs @@ -0,0 +1,199 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Resilience.Internal.Routing; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Hedging.Routing; + +public abstract class RoutingStrategyTest +{ + public const string RoutingName = "dummy-routing"; + + protected RoutingStrategyTest() + { + Builder = new RoutingStrategyBuilder(RoutingName, new ServiceCollection()); + Builder.Services.TryAddSingleton(Randomizer.Object); + } + + public IRoutingStrategyBuilder Builder { get; set; } + + internal Mock Randomizer { get; } = new Mock(MockBehavior.Strict); + + public virtual bool CompareOrder => true; + + [Fact] + public void Validate_Ok() + { + Configure(Builder); + + Assert.Throws(() => CreateStrategy("unknown")); + } + + [Fact] + public void CreateStrategy_EnsurePooled() + { + SetupRandomizer(60d); + SetupRandomizer(60); + Configure(Builder); + + var factory = CreateRoutingFactory(); + var strategies = new HashSet(); + + for (int i = 0; i < 10; i++) + { + var strategy = factory.CreateRoutingStrategy(); + strategies.Add(strategy); + ((IPooledRequestRoutingStrategyFactory)factory).ReturnRoutingStrategy(strategy); + } + + // assert that some strategies were pooled + Assert.True(strategies.Count < 5); + } + + [Fact] + public virtual void MinRoutes_Ok() + { + SetupRandomizer(0); + SetupRandomizer(0d); + + var routes = ConfigureMinRoutes(Builder).ToArray(); + + var urls = CollectUrls(CreateStrategy()).ToArray(); + Assert.Equal(routes.Length, urls.Length); + if (CompareOrder) + { + urls.Should().Equal(routes); + } + else + { + urls.Should().BeEquivalentTo(routes); + } + } + + [Fact] + public void InvalidRoutes_ValidationException() + { + foreach (var action in ConfigureInvalidRoutes()) + { + Builder = new RoutingStrategyBuilder(RoutingName, new ServiceCollection()); + Builder.Services.TryAddSingleton(Randomizer.Object); + action(Builder); + + Assert.Throws(() => CreateStrategy()); + } + } + + [Fact] + public void TryGetNextRoute_NotInitialized_Throws() + { + Assert.Throws(() => CreateEmptyStrategy().TryGetNextRoute(out _)); + } + + [Fact] + public void TryGetNextRoute_AfterReset_Throws() + { + SetupRandomizer(0); + + Builder = new RoutingStrategyBuilder(RoutingName, new ServiceCollection()); + Builder.Services.TryAddSingleton(Randomizer.Object); + Configure(Builder); + + var strategy = CreateStrategy(); + + _ = ((IResettable)strategy).TryReset(); + + Assert.Throws(() => strategy.TryGetNextRoute(out _)); + } + + protected void ReloadHelper( + Action configure, + Dictionary config1, + Dictionary config2, + string[] urls1, + string[] urls2) + { + var provider = new ReloadableConfiguration(); + provider.Reload(config1); + + var builder = new ConfigurationBuilder(); + builder.Add(provider); + configure(Builder, builder.Build()); + + CollectUrls(CreateStrategy()).Should().Equal(urls1); + + // empty data -> failure + Assert.Throws(() => provider.Reload(new Dictionary())); + + provider.Reload(config2); + CollectUrls(CreateStrategy()).Should().Equal(urls2); + } + + internal void StrategyResultHelper(params string[] expectedUrls) + { + Builder = new RoutingStrategyBuilder(RoutingName, new ServiceCollection()); + Builder.Services.TryAddSingleton(Randomizer.Object); + Configure(Builder); + + var factory = CreateRoutingFactory(); + + CollectUrls(factory.CreateRoutingStrategy()).Should().Equal(expectedUrls); + + // intentionally, we check that the output on subsequent calls is the same + CollectUrls(factory.CreateRoutingStrategy()).Should().Equal(expectedUrls); + } + + protected IRequestRoutingStrategy CreateStrategy(string? name = null) => CreateRoutingFactory(name).CreateRoutingStrategy(); + + protected IRequestRoutingStrategyFactory CreateRoutingFactory(string? name = null) => Builder.Services.BuildServiceProvider().GetRoutingFactory(name ?? Builder.Name); + + private static IEnumerable CollectUrls(IRequestRoutingStrategy strategy) + { + while (strategy.TryGetNextRoute(out var route)) + { + yield return route.ToString(); + } + } + + protected static IConfigurationSection GetSection(IDictionary values) + { + return new ConfigurationBuilder().AddInMemoryCollection(values.Select(pair => new KeyValuePair("section:" + pair.Key, pair.Value))).Build().GetSection("section"); + } + + protected abstract void Configure(IRoutingStrategyBuilder routingBuilder); + + protected abstract IEnumerable ConfigureMinRoutes(IRoutingStrategyBuilder routingBuilder); + + protected abstract IEnumerable> ConfigureInvalidRoutes(); + + protected abstract IRequestRoutingStrategy CreateEmptyStrategy(); + + protected void SetupRandomizer(double result) => Randomizer.Setup(r => r.NextDouble(It.IsAny())).Returns(result); + + protected void SetupRandomizer(int result) => Randomizer.Setup(r => r.NextInt(It.IsAny())).Returns(result); + + private class ReloadableConfiguration : ConfigurationProvider, IConfigurationSource + { + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return this; + } + + public void Reload(Dictionary data) + { + Data = new Dictionary(data, StringComparer.OrdinalIgnoreCase); + OnReload(); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Routing/WeightedRoutingStrategyTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Routing/WeightedRoutingStrategyTest.cs new file mode 100644 index 0000000000..15bd1c0fcb --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Routing/WeightedRoutingStrategyTest.cs @@ -0,0 +1,177 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Resilience.Internal.Routing; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Hedging.Routing; + +public class WeightedRoutingStrategyTest : RoutingStrategyTest +{ + private WeightedGroupSelectionMode _selectionMode; + + [Fact] + public void GetRoutes_InitialAttempt_EnsureExpectedOutput() + { + _selectionMode = WeightedGroupSelectionMode.InitialAttempt; + + SetupRandomizer(0d); + StrategyResultHelper("https://a/", "https://b/", "https://c/"); + + SetupRandomizer(21d); + StrategyResultHelper("https://b/", "https://a/", "https://c/"); + + SetupRandomizer(51d); + StrategyResultHelper("https://c/", "https://a/", "https://b/"); + } + + [Fact] + public void GetRoutes_EveryAttempt_EnsureExpectedOutput() + { + _selectionMode = WeightedGroupSelectionMode.EveryAttempt; + + SetupRandomizer(0d); + StrategyResultHelper("https://a/", "https://b/", "https://c/"); + } + + [Fact] + public void Reload_Ok() + { + SetupRandomizer(1.0); + + ReloadHelper( + (b, c) => b.ConfigureWeightedGroups(c.GetSection("section")), + new() + { + { "section:groups:0:endpoints:0:uri", "https://a/" }, + { "section:groups:0:weight", "10" } + }, + new() + { + { "section:groups:0:endpoints:0:uri", "https://b/" }, + { "section:groups:0:weight", "10" } + }, + new[] { "https://a/" }, + new[] { "https://b/" }); + } + + protected override void Configure(IRoutingStrategyBuilder routingBuilder) + { + routingBuilder.ConfigureWeightedGroups(GetSection(new Dictionary + { + { "groups:0:endpoints:0:uri", "https://a/" }, + { "groups:0:weight", "10" } + })); + + routingBuilder.ConfigureWeightedGroups(options => + { + options.SelectionMode = _selectionMode; + + var group = CreateGroup("https://b/"); + group.Weight = 20; + + var groups = new List(options.Groups) + { + group + }; + options.Groups = groups; + }); + + routingBuilder.ConfigureWeightedGroups((options, serviceProvider) => + { + serviceProvider.Should().NotBeNull(); + var group = CreateGroup("https://c/"); + group.Weight = 30; + options.Groups.Add(group); + }); + } + + protected override IEnumerable ConfigureMinRoutes(IRoutingStrategyBuilder routingBuilder) + { + routingBuilder.ConfigureWeightedGroups(options => options.Groups.Add(CreateGroup("https://dummy-route/"))); + yield return "https://dummy-route/"; + } + + protected override IEnumerable> ConfigureInvalidRoutes() + { + yield return builder => builder.ConfigureWeightedGroups(options => { }); + + yield return builder => builder.ConfigureWeightedGroups(options => + { + var group = CreateGroup("https://dummy"); + group.Weight = 0; + + var groups = new List(options.Groups) + { + group + }; + options.Groups = groups; + }); + + yield return builder => builder.ConfigureWeightedGroups(options => + { + var group = CreateGroup("https://dummy"); + group.Weight = 99999; + + var groups = new List(options.Groups) + { + group + }; + options.Groups = groups; + }); + + yield return builder => builder.ConfigureWeightedGroups(options => + { + var group = CreateGroup("https://dummy"); + group.Endpoints.Single().Weight = 0; + + var groups = new List(options.Groups) + { + group + }; + options.Groups = groups; + }); + + yield return builder => builder.ConfigureWeightedGroups(options => + { + var group = CreateGroup("https://dummy"); + group.Endpoints.Single().Weight = 99999; + + var groups = new List(options.Groups) + { + group + }; + options.Groups = groups; + }); + + yield return builder => builder.ConfigureWeightedGroups(options => + { + var groups = new List(options.Groups) + { + null! + }; + options.Groups = groups; + }); + } + + protected override IRequestRoutingStrategy CreateEmptyStrategy() => new WeightedGroupsRoutingStrategy(Mock.Of()); + + private static WeightedEndpointGroup CreateGroup(params string[] endpoints) + { + return CreateGroup(endpoints.Select(v => new WeightedEndpoint { Uri = new Uri(v) }).ToArray()); + } + + private static WeightedEndpointGroup CreateGroup(params WeightedEndpoint[] endpoint) + { + return new WeightedEndpointGroup + { + Endpoints = endpoint.ToList() + }; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/StandardHedgingTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/StandardHedgingTests.cs new file mode 100644 index 0000000000..63d3e29f56 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/StandardHedgingTests.cs @@ -0,0 +1,211 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Resilience.Internal.Routing; +using Microsoft.Extensions.Http.Resilience.Test.Hedgings.Helpers; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience; +using Microsoft.Extensions.Resilience.Internal; +using Moq; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Hedging; + +public sealed class StandardHedgingTests : HedgingTests +{ + public StandardHedgingTests() + : base(ConfigureDefaultBuilder) + { + } + + private static IStandardHedgingHandlerBuilder ConfigureDefaultBuilder(IHttpClientBuilder builder, IRequestRoutingStrategyFactory factory) + { + return builder + .AddStandardHedgingHandler(routing => routing.ConfigureRoutingStrategy(_ => factory)) + .Configure(options => + { + options.HedgingOptions.MaxHedgedAttempts = DefaultHedgingAttempts; + options.HedgingOptions.HedgingDelay = TimeSpan.FromMilliseconds(5); + }); + } + + [Fact] + public void EnsureValidated_BasicValidation() + { + Builder.Configure(options => options.HedgingOptions.MaxHedgedAttempts = -1); + + Assert.Throws(() => CreateClientWithHandler()); + } + + [Fact] + public void EnsureValidated_AdvancedValidation() + { + Builder.Configure(options => options.TotalRequestTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(1)); + + Assert.Throws(() => CreateClientWithHandler()); + } + + [Fact] + public void Configure_Callback_Ok() + { + Builder.Configure(o => o.HedgingOptions.MaxHedgedAttempts = 8); + + var options = Builder.Services.BuildServiceProvider().GetRequiredService>().Get(Builder.Name); + + Assert.Equal(8, options.HedgingOptions.MaxHedgedAttempts); + } + + [Fact] + public void Configure_CallbackWithServiceProvider_Ok() + { + Builder.Configure((o, serviceProvider) => + { + serviceProvider.GetRequiredService().Should().NotBeNull(); + o.HedgingOptions.MaxHedgedAttempts = 8; + }); + + var options = Builder.Services.BuildServiceProvider().GetRequiredService>().Get(Builder.Name); + + Assert.Equal(8, options.HedgingOptions.MaxHedgedAttempts); + } + + [Fact] + public void RoutingStrategyBuilder_EnsureExpectedName() + { + Assert.Equal(ClientId, Builder.RoutingStrategyBuilder.Name); + } + + [Fact] + public void Configure_ValidConfigurationSection_ShouldInitialize() + { + var section = ConfigurationStubFactory.Create(new Dictionary + { + { "dummy:HedgingOptions:MaxHedgedAttempts", "8" } + }).GetSection("dummy"); + + Builder.Configure(section); + + var options = Builder.Services.BuildServiceProvider().GetRequiredService>().Get(Builder.Name); + + Assert.Equal(8, options.HedgingOptions.MaxHedgedAttempts); + } + +#if NET6_0_OR_GREATER + [Fact] + public void Configure_InvalidConfigurationSection_ShouldThrow() + { + var section = ConfigurationStubFactory.Create(new Dictionary + { + { "dummy:HedgingOptionsTypo:MaxHedgedAttempts", "8" }, + { "dummy:TotalRequestTimeoutOptions:TimeoutInterval", "00:00:20" }, + }).GetSection("dummy"); + + Builder.Configure(section); + + Assert.Throws(() => + Builder.Services.BuildServiceProvider() + .GetRequiredService>() + .Get(Builder.Name)); + } +#endif + + [Fact] + public void Configure_EmptyConfigurationSectionContent_ShouldThrow() + { + var section = ConfigurationStubFactory.Create(new Dictionary + { + { "dummy", "" } + }).GetSection("dummy"); + + Assert.Throws(() => + Builder.Configure(section)); + } + + [Fact] + public void Configure_EmptyConfigurationSection_ShouldThrow() + { + var section = ConfigurationStubFactory.CreateEmpty().GetSection(string.Empty); + + Assert.Throws(() => + Builder.Configure(section)); + } + + [Fact] + public async Task VerifyPipeline() + { + var noPolicy = Policy.NoOpAsync(); + var builder = new Mock>(MockBehavior.Strict); + Builder.Services.RemoveAll>(); + Builder.Services.AddSingleton(builder.Object); + + var serviceProvider = Builder.Services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>().Get(Builder.Name); + + // primary handler + builder.SetupSequence(o => o.Initialize(It.Is(v => v.PipelineName == "clientId-standard-hedging"))); + builder.SetupSequence(o => o.AddTimeoutPolicy("StandardHedging-TotalRequestTimeout", options.TotalRequestTimeoutOptions)).Returns(builder.Object); + builder.SetupSequence(o => o.AddPolicy(It.Is>(p => p is RoutingPolicy))).Returns(builder.Object); + builder.SetupSequence(o => o.AddPolicy(It.Is>(p => p is RequestMessageSnapshotPolicy))).Returns(builder.Object); + builder.SetupSequence(o => o.AddHedgingPolicy("StandardHedging-Hedging", It.IsAny>(), options.HedgingOptions)).Returns(builder.Object); + builder.Setup(o => o.Build()).Returns(noPolicy); + + // inner handler + builder.SetupSequence(o => o.Initialize(It.Is(v => v.PipelineName == "clientId-standard-hedging-endpoint"))); + builder.SetupSequence(o => o.AddBulkheadPolicy("StandardHedging-Bulkhead", options.EndpointOptions.BulkheadOptions)).Returns(builder.Object); + builder.SetupSequence(o => o.AddCircuitBreakerPolicy("StandardHedging-CircuitBreaker", options.EndpointOptions.CircuitBreakerOptions)).Returns(builder.Object); + builder.SetupSequence(o => o.AddTimeoutPolicy("StandardHedging-AttemptTimeout", options.EndpointOptions.TimeoutOptions)).Returns(builder.Object); + builder.Setup(o => o.Build()).Returns(noPolicy); + + using var client = serviceProvider.GetRequiredService().CreateClient(ClientId); + AddResponse(HttpStatusCode.OK); + using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); + await client.SendAsync(request, CancellationToken.None); + + builder.VerifyAll(); + } + + [InlineData(null)] + [InlineData("custom-key")] + [Theory] + public async Task VerifyPipelineSelection(string? customKey) + { + var noPolicy = Policy.NoOpAsync(); + var provider = new Mock(MockBehavior.Strict); + Builder.Services.RemoveAll(); + Builder.Services.AddSingleton(provider.Object); + if (customKey == null) + { + Builder.SelectPipelineByAuthority(SimpleClassifications.PublicData); + } + else + { + Builder.SelectPipelineBy(_ => _ => customKey); + } + + customKey ??= "https://key:80"; + provider.Setup(v => v.GetPipeline("clientId-standard-hedging")).Returns(noPolicy); + provider.Setup(v => v.GetPipeline("clientId-standard-hedging-endpoint", customKey)).Returns(noPolicy); + + using var client = CreateClientWithHandler(); + using var request = new HttpRequestMessage(HttpMethod.Get, "https://key:80/discarded"); + AddResponse(HttpStatusCode.OK); + + var response = await client.SendAsync(request, CancellationToken.None); + + provider.VerifyAll(); + } + + protected override void ConfigureHedgingOptions(Action configure) => Builder.Configure(options => configure(options.HedgingOptions)); +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Microsoft.Extensions.Http.Resilience.Tests.csproj b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Microsoft.Extensions.Http.Resilience.Tests.csproj new file mode 100644 index 0000000000..364daae8cb --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Microsoft.Extensions.Http.Resilience.Tests.csproj @@ -0,0 +1,38 @@ + + + Microsoft.Extensions.Http.Resilience.Test + Unit tests for Microsoft.Extensions.Http.Resilience. + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpCircuitBreakerPolicyOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpCircuitBreakerPolicyOptionsTests.cs new file mode 100644 index 0000000000..955c71d8dd --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpCircuitBreakerPolicyOptionsTests.cs @@ -0,0 +1,147 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Options; +using Polly; +using Polly.Timeout; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Polly; + +public class HttpCircuitBreakerPolicyOptionsTests +{ +#pragma warning disable S2330 + public static readonly IEnumerable HandledExceptionsClassified = new[] + { + new object[] { new InvalidCastException(), false }, + new object[] { new HttpRequestException(), true }, + new object[] { new TaskCanceledException(), false }, + new object[] { new TimeoutRejectedException(), true }, + }; + + private readonly HttpCircuitBreakerPolicyOptions _testObject; + + public HttpCircuitBreakerPolicyOptionsTests() + { + _testObject = new HttpCircuitBreakerPolicyOptions(); + } + + [Fact] + public void Constructor_ShouldInitialize() + { + var instance = new HttpCircuitBreakerPolicyOptions(); + Assert.NotNull(instance); + } + + [Fact] + public void ShouldHandleResultAsError_ShouldGetAndSet() + { + Predicate testValue = response => !response.IsSuccessStatusCode; + _testObject.ShouldHandleResultAsError = testValue; + + Assert.Equal(testValue, _testObject.ShouldHandleResultAsError); + } + + [Theory] + [InlineData(HttpStatusCode.OK, false)] + [InlineData(HttpStatusCode.BadRequest, false)] + [InlineData(HttpStatusCode.RequestEntityTooLarge, false)] + [InlineData(HttpStatusCode.InternalServerError, true)] + [InlineData(HttpStatusCode.HttpVersionNotSupported, true)] + [InlineData(HttpStatusCode.RequestTimeout, true)] + public void ShouldHandleResultAsError_DefaultValue_ShouldClassify(HttpStatusCode statusCode, bool expectedCondition) + { + var response = new HttpResponseMessage { StatusCode = statusCode }; + var isTransientFailure = _testObject.ShouldHandleResultAsError(response); + Assert.Equal(expectedCondition, isTransientFailure); + response.Dispose(); + } + + [Fact] + public void ShouldHandleException_ShouldGetAndSet() + { + Predicate testValue = ex => ex is ArgumentNullException; + _testObject.ShouldHandleException = testValue; + + Assert.Equal(testValue, _testObject.ShouldHandleException); + } + + [Theory] + [MemberData(nameof(HandledExceptionsClassified))] + public void ShouldHandleException_DefaultValue_ShouldClassify(Exception exception, bool expectedToHandle) + { + var shouldHandle = _testObject.ShouldHandleException(exception); + Assert.Equal(expectedToHandle, shouldHandle); + } + + [Fact] + public void OnCircuitBreak_ShouldGetAndSet() + { + Action> testValue = _ => { }; + _testObject.OnCircuitBreak = testValue; + + Assert.Equal(testValue, _testObject.OnCircuitBreak); + } + + [Fact] + public void OnCircuitReset_ShouldGetAndSet() + { + Action testValue = _ => { }; + _testObject.OnCircuitReset = testValue; + + Assert.Equal(testValue, _testObject.OnCircuitReset); + } + + [Theory] + [InlineData(HttpStatusCode.OK, false)] + [InlineData(HttpStatusCode.BadRequest, false)] + [InlineData(HttpStatusCode.RequestEntityTooLarge, false)] + [InlineData(HttpStatusCode.InternalServerError, true)] + [InlineData(HttpStatusCode.HttpVersionNotSupported, true)] + [InlineData(HttpStatusCode.RequestTimeout, true)] + public void ShouldHandleResultAsError_DefaultInstance_ShouldClassify(HttpStatusCode statusCode, bool expectedCondition) + { + var response = new HttpResponseMessage { StatusCode = statusCode }; + var isTransientFailure = new HttpCircuitBreakerPolicyOptions().ShouldHandleResultAsError(response); + Assert.Equal(expectedCondition, isTransientFailure); + response.Dispose(); + } + + [Theory] + [MemberData(nameof(HandledExceptionsClassified))] + public void ShouldHandleException_DefaultInstance_ShouldClassify(Exception exception, bool expectedToHandle) + { + var shouldHandle = new HttpCircuitBreakerPolicyOptions().ShouldHandleException(exception); + Assert.Equal(expectedToHandle, shouldHandle); + } + + [Fact] + public void OnCircuitBreak_NoOp() + { + var options = new HttpCircuitBreakerPolicyOptions(); + var context = new Context(); + var expectedError = "Something went wrong"; + var delegateResult = new DelegateResult(new InvalidOperationException(expectedError)); + var args = new BreakActionArguments( + delegateResult, + context, + TimeSpan.FromSeconds(2), + CancellationToken.None); + Assert.Null(Record.Exception(() => options.OnCircuitBreak(args))); + } + + [Fact] + public void OnCircuitReset_NoOp() + { + var options = new HttpCircuitBreakerPolicyOptions { OnCircuitReset = (_) => { } }; + var context = new Context(); + + Assert.Null(Record.Exception(() => options.OnCircuitReset(new ResetActionArguments(context, CancellationToken.None)))); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpClientResiliencePredicatesTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpClientResiliencePredicatesTests.cs new file mode 100644 index 0000000000..7e789ba2df --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpClientResiliencePredicatesTests.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using Polly.Timeout; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Polly; + +public class HttpClientResiliencePredicatesTests +{ +#pragma warning disable S2330 // Array covariance should not be used + public static readonly IEnumerable HandledExceptionsClassified = new[] + { + new object[] { new InvalidCastException(), false }, + new object[] { new HttpRequestException(), true }, + new object[] { new TimeoutRejectedException(), true }, + }; +#pragma warning restore S2330 // Array covariance should not be used + + [Fact] + public void IsTransientHttpException_NullException_ShouldThrow() + { + Assert.Throws(() => + HttpClientResiliencePredicates.IsTransientHttpException(null!)); + } + + [Theory] + [MemberData(nameof(HandledExceptionsClassified))] + public void IsTransientHttpException_Exception_ShouldClassify(Exception exceptions, bool expectedCondition) + { + var isTransientHttpException = HttpClientResiliencePredicates.IsTransientHttpException(exceptions); + Assert.Equal(expectedCondition, isTransientHttpException); + } + + public class HttpResponseMessageExtensionsTests + { + [Theory] + [InlineData(HttpStatusCode.OK, false)] + [InlineData(HttpStatusCode.BadRequest, false)] + [InlineData(HttpStatusCode.RequestEntityTooLarge, false)] + [InlineData(HttpStatusCode.InternalServerError, true)] + [InlineData(HttpStatusCode.HttpVersionNotSupported, true)] + [InlineData(HttpStatusCode.RequestTimeout, true)] + [InlineData((HttpStatusCode)429, true)] + public void IsTransientFailure_ShouldClassify(HttpStatusCode statusCode, bool expectedCondition) + { + var response = new HttpResponseMessage { StatusCode = statusCode }; + var isTransientFailure = HttpClientResiliencePredicates.IsTransientHttpFailure(response); + Assert.Equal(expectedCondition, isTransientFailure); + response.Dispose(); + } + + [Fact] + public void IsTransientFailure_NullResponse_ShouldThrow() + { + Assert.Throws(() => HttpClientResiliencePredicates.IsTransientHttpFailure(null!)); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpFallbackPolicyOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpFallbackPolicyOptionsTests.cs new file mode 100644 index 0000000000..a17302afc1 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpFallbackPolicyOptionsTests.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using Microsoft.Extensions.Resilience.Options; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Polly; + +public class HttpFallbackPolicyOptionsTests +{ +#pragma warning disable S2330 + public static readonly IEnumerable HandledExceptionsClassified = new[] + { + new object[] { new InvalidCastException(), false }, + new object[] { new HttpRequestException(), true } + }; + + [Theory] + [InlineData(HttpStatusCode.OK, false)] + [InlineData(HttpStatusCode.BadRequest, false)] + [InlineData(HttpStatusCode.RequestEntityTooLarge, false)] + [InlineData(HttpStatusCode.InternalServerError, true)] + [InlineData(HttpStatusCode.HttpVersionNotSupported, true)] + [InlineData(HttpStatusCode.RequestTimeout, true)] + public void ShouldHandleResultAsError_DefaultInstance_ShouldClassify( + HttpStatusCode statusCode, + bool expected) + { + using var httpReq = new HttpResponseMessage { StatusCode = statusCode }; + var actual = new HttpFallbackPolicyOptions().ShouldHandleResultAsError(httpReq); + + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(nameof(HandledExceptionsClassified))] + public void ShouldHandleException_DefaultInstance_ShouldClassify( + Exception exception, + bool expected) + { + var actual = new HttpFallbackPolicyOptions().ShouldHandleException(exception); + + Assert.Equal(expected, actual); + } + + [Fact] + public void OnFallback_NoOp() + { + var options = new HttpFallbackPolicyOptions(); + var context = new Context(); + var expectedError = "Something went wrong"; + var delegateResult = new DelegateResult(new InvalidOperationException(expectedError)); + + var task = options.OnFallbackAsync(new FallbackTaskArguments(delegateResult, context, CancellationToken.None)); + Assert.NotNull(task); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpResponseMessageExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpResponseMessageExtensionsTests.cs new file mode 100644 index 0000000000..96f92799cd --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpResponseMessageExtensionsTests.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using System.Net.Http; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Polly; + +public class HttpResponseMessageExtensionsTests +{ + private readonly TimeProvider _fakeClock = new FakeTimeProvider(); + + [Fact] + public void RetryAfter_WhenNoHeaderFound_ShouldReturnZero() + { + using var httpResponseMessage = new HttpResponseMessage(); + Assert.Equal(TimeSpan.Zero, RetryAfterHelper.ParseRetryAfterHeader(null!, _fakeClock)); + Assert.Equal(TimeSpan.Zero, RetryAfterHelper.ParseRetryAfterHeader(httpResponseMessage, _fakeClock)); + } + + [Theory] + [InlineData(10)] + [InlineData(33)] + public void RetryAfter_WhenRelativeHeaderIsFound_ShouldReturnHeaderInterval(int seconds) + { + using var httpResponseMessage = new HttpResponseMessage(); + httpResponseMessage.Headers.Add("Retry-After", seconds.ToString(CultureInfo.InvariantCulture)); + var interval = RetryAfterHelper.ParseRetryAfterHeader(httpResponseMessage, _fakeClock); + Assert.Equal(TimeSpan.FromSeconds(seconds), interval); + } + + [Theory] + [InlineData(10)] + [InlineData(33)] + public void RetryAfter_WhenAbsoluteHeaderIsFound_ShouldReturnHeaderInterval(int seconds) + { + using var httpResponseMessage = new HttpResponseMessage(); + httpResponseMessage.Headers.Add("Retry-After", (_fakeClock.GetUtcNow() + TimeSpan.FromSeconds(seconds)).ToString("r", CultureInfo.InvariantCulture)); + var interval = RetryAfterHelper.ParseRetryAfterHeader(httpResponseMessage, _fakeClock); + Assert.Equal(TimeSpan.FromSeconds(seconds), interval); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpRetryPolicyOptionTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpRetryPolicyOptionTests.cs new file mode 100644 index 0000000000..bb90788944 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpRetryPolicyOptionTests.cs @@ -0,0 +1,189 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using Microsoft.Extensions.Resilience.Options; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Polly; + +public class HttpRetryPolicyOptionTests +{ +#pragma warning disable S2330 + public static readonly IEnumerable HandledExceptionsClassified = new[] + { + new object[] { new InvalidCastException(), false }, + new object[] { new HttpRequestException(), true } + }; + + private readonly HttpRetryPolicyOptions _testClass; + + public HttpRetryPolicyOptionTests() + { + _testClass = new HttpRetryPolicyOptions(); + } + + [Fact] + public void ShouldHandleResultAsError_ShouldGetAndSet() + { + Predicate testValue = response => !response.IsSuccessStatusCode; + _testClass.ShouldHandleResultAsError = testValue; + + Assert.Equal(testValue, _testClass.ShouldHandleResultAsError); + } + + [Theory] + [InlineData(HttpStatusCode.OK, false)] + [InlineData(HttpStatusCode.BadRequest, false)] + [InlineData(HttpStatusCode.RequestEntityTooLarge, false)] + [InlineData(HttpStatusCode.InternalServerError, true)] + [InlineData(HttpStatusCode.HttpVersionNotSupported, true)] + [InlineData(HttpStatusCode.RequestTimeout, true)] + public void ShouldHandleResultAsError_DefaultValue_ShouldClassify(HttpStatusCode statusCode, bool expectedCondition) + { + var response = new HttpResponseMessage { StatusCode = statusCode }; + var isTransientFailure = _testClass.ShouldHandleResultAsError(response); + Assert.Equal(expectedCondition, isTransientFailure); + response.Dispose(); + } + + [Fact] + public void ShouldHandleException_ShouldGetAndSet() + { + Predicate testValue = ex => ex is ArgumentNullException; + _testClass.ShouldHandleException = testValue; + + Assert.Equal(testValue, _testClass.ShouldHandleException); + } + + [Theory] + [MemberData(nameof(HandledExceptionsClassified))] + public void ShouldHandleException_DefaultValue_ShouldClassify(Exception exception, bool expectedToHandle) + { + var shouldHandle = _testClass.ShouldHandleException(exception); + Assert.Equal(expectedToHandle, shouldHandle); + } + + [Theory] + [InlineData(HttpStatusCode.OK, false)] + [InlineData(HttpStatusCode.BadRequest, false)] + [InlineData(HttpStatusCode.RequestEntityTooLarge, false)] + [InlineData(HttpStatusCode.InternalServerError, true)] + [InlineData(HttpStatusCode.HttpVersionNotSupported, true)] + [InlineData(HttpStatusCode.RequestTimeout, true)] + public void ShouldHandleResultAsError_DefaultInstance_ShouldClassify(HttpStatusCode statusCode, bool expectedCondition) + { + var response = new HttpResponseMessage { StatusCode = statusCode }; + var isTransientFailure = new HttpRetryPolicyOptions().ShouldHandleResultAsError(response); + Assert.Equal(expectedCondition, isTransientFailure); + response.Dispose(); + } + + [Theory] + [MemberData(nameof(HandledExceptionsClassified))] + public void ShouldHandleException_DefaultInstance_ShouldClassify(Exception exception, bool expectedToHandle) + { + var shouldHandle = new HttpRetryPolicyOptions().ShouldHandleException(exception); + Assert.Equal(expectedToHandle, shouldHandle); + } + + [Fact] + public void ShouldRetryAfterHeader_WhenNullHeader_ShouldReturnZero() + { + var options = new HttpRetryPolicyOptions { ShouldRetryAfterHeader = true }; + using var responseMessage = new HttpResponseMessage { }; + var delegateResult = new DelegateResult(responseMessage); + var result = options.RetryDelayGenerator != null + ? options.RetryDelayGenerator( + new RetryDelayArguments(delegateResult, new Context(), CancellationToken.None)) + : TimeSpan.Zero; + Assert.Equal(result, TimeSpan.Zero); + } + + [Fact] + public void ShouldRetryAfterHeader_WhenResponseContainsRetryAfterHeader_ShouldReturnTimeSpan() + { + var options = new HttpRetryPolicyOptions { ShouldRetryAfterHeader = true }; + using var responseMessage = new HttpResponseMessage + { + Headers = + { + RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(10)) + } + }; + var args = new RetryDelayArguments( + new DelegateResult(responseMessage), + new Context(), + CancellationToken.None); + + var result = options.RetryDelayGenerator != null ? options.RetryDelayGenerator(args) : TimeSpan.Zero; + Assert.Equal(result, TimeSpan.FromSeconds(10)); + } + + [Fact] + public void ShouldRetryAfterHeader_WhenResponseContainsNullHeader_ShouldReturnZero() + { + var options = new HttpRetryPolicyOptions { ShouldRetryAfterHeader = true }; + using var responseMessage = new HttpResponseMessage + { + }; + var delegateResult = new DelegateResult(responseMessage); + var result = options.RetryDelayGenerator != null + ? options.RetryDelayGenerator( + new RetryDelayArguments(delegateResult, new Context(), CancellationToken.None)) + : TimeSpan.Zero; + Assert.Equal(result, TimeSpan.Zero); + + result = options.RetryDelayGenerator != null ? options.RetryDelayGenerator( + new RetryDelayArguments(null!, new Context(), CancellationToken.None)) + : TimeSpan.Zero; + Assert.Equal(result, TimeSpan.Zero); + } + + [Fact] + public void ShouldRetryAfterHeader_WhenDelegateHasException_ShouldReturnZero() + { + var options = new HttpRetryPolicyOptions { ShouldRetryAfterHeader = true }; + var args = new RetryDelayArguments( + new DelegateResult(new ArgumentNullException()), + new Context(), + CancellationToken.None); + + var result = options.RetryDelayGenerator!(args); + Assert.Equal(result, TimeSpan.Zero); + } + + [Fact] + public void ShouldRetryAfterHeader_WhenHeaderSetUsingAdd_ShouldReturnTimeSpan() + { + var options = new HttpRetryPolicyOptions { ShouldRetryAfterHeader = true }; + using var responseMessage = new HttpResponseMessage(); + responseMessage.Headers.Add("Retry-After", "10"); + var args = new RetryDelayArguments( + new DelegateResult(responseMessage), + new Context(), + CancellationToken.None); + + var result = options.RetryDelayGenerator != null ? options.RetryDelayGenerator(args) : TimeSpan.Zero; + Assert.Equal(result, TimeSpan.FromSeconds(10)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GetDelayGenerator_ShouldGetBasedOnShouldRetryAfterHeader(bool shouldRetryAfterHeader) + { + var options = new HttpRetryPolicyOptions + { + ShouldRetryAfterHeader = shouldRetryAfterHeader + }; + + Assert.Equal(shouldRetryAfterHeader, options.RetryDelayGenerator != null); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/configs/appsettings.json b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/configs/appsettings.json new file mode 100644 index 0000000000..c1fee85295 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/configs/appsettings.json @@ -0,0 +1,23 @@ +{ + "ChaosPolicyConfigurations": { + "ChaosPolicyOptionsGroups": { + "OptionsGroupTest": { + "HttpResponseInjectionPolicyOptions": { + "Enabled": false + }, + "ExceptionPolicyOptions": { + "Enabled": false + }, + "LatencyPolicyOptions": { + "Enabled": true + } + } + } + }, + "FaultPolicyWeightAssignments": { + "WeightAssignments": { + "TestA": 50, + "TestB": 50 + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/HttpClientLatencyTelemetryExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/HttpClientLatencyTelemetryExtensionsTest.cs new file mode 100644 index 0000000000..937975fce2 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/HttpClientLatencyTelemetryExtensionsTest.cs @@ -0,0 +1,119 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net.Http; +using FluentAssertions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Telemetry.Latency.Internal; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Latency; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Latency.Test; + +public class HttpClientLatencyTelemetryExtensionsTest +{ + [Fact] + public void HttpClientLatencyTelemtry_NullParameter_ThrowsException() + { + var act = () => ((IServiceCollection)null!).AddDefaultHttpClientLatencyTelemetry(); + act.Should().Throw(); + + act = () => Mock.Of().AddDefaultHttpClientLatencyTelemetry((Action)null!); + act.Should().Throw(); + + act = () => Mock.Of().AddDefaultHttpClientLatencyTelemetry((IConfigurationSection)null!); + act.Should().Throw(); + } + + [Fact] + public void HttpClientLatencyTelemtry_AddToServiceCollection() + { + using var sp = new ServiceCollection() + .AddHttpClient() + .AddNullLatencyContext() + .AddDefaultHttpClientLatencyTelemetry() + .BuildServiceProvider(); + + var listener = sp.GetRequiredService(); + Assert.NotNull(listener); + + var latencyContext = sp.GetRequiredService(); + Assert.NotNull(latencyContext); + Assert.Null(latencyContext.Get()); + + var options = sp.GetRequiredService>().Value; + Assert.NotNull(options); + Assert.True(options.EnableDetailedLatencyBreadkdown); + + var handler = sp.GetRequiredService(); + Assert.NotNull(handler); + } + + [Fact] + public void HttpClientLatencyTelemtry_AddToServiceCollection_CreatesClientSuccessfully() + { + using var sp = new ServiceCollection() + .AddNullLatencyContext() + .AddHttpClient() + .AddDefaultHttpClientLatencyTelemetry() + .BuildServiceProvider(); + + using var httpClient = sp.GetRequiredService().CreateClient(); + Assert.NotNull(httpClient); + } + + [Fact] + public void HttpClientLatencyTelemtryExtensions_Add_InvokesConfig() + { + bool invoked = false; + using var sp = new ServiceCollection() + .AddNullLatencyContext() + .AddDefaultHttpClientLatencyTelemetry(a => + { + invoked = true; + a.EnableDetailedLatencyBreadkdown = false; + }) + .BuildServiceProvider(); + + var options = sp.GetRequiredService>().Value; + Assert.NotNull(options); + Assert.False(options.EnableDetailedLatencyBreadkdown); + Assert.True(invoked); + } + + [Fact] + public void RequestLatencyExtensions_Add_BindsToConfigSection() + { + HttpClientLatencyTelemetryOptions expectedOptions = new() + { + EnableDetailedLatencyBreadkdown = false + }; + + var config = GetConfigSection(expectedOptions); + using var sp = new ServiceCollection() + .AddNullLatencyContext() + .AddDefaultHttpClientLatencyTelemetry(config) + .BuildServiceProvider(); + + var options = sp.GetRequiredService>().Value; + Assert.NotNull(options); + Assert.Equal(expectedOptions.EnableDetailedLatencyBreadkdown, options.EnableDetailedLatencyBreadkdown); + } + + private static IConfigurationSection GetConfigSection(HttpClientLatencyTelemetryOptions options) + { + return new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { $"{nameof(HttpClientLatencyTelemetryOptions)}:{nameof(options.EnableDetailedLatencyBreadkdown)}", options.EnableDetailedLatencyBreadkdown.ToString(CultureInfo.InvariantCulture) }, + }) + .Build() + .GetSection($"{nameof(HttpClientLatencyTelemetryOptions)}"); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpCheckpointsTest.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpCheckpointsTest.cs new file mode 100644 index 0000000000..e08f23c50e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpCheckpointsTest.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Http.Telemetry.Latency.Internal; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Latency.Test.Internal; + +public class HttpCheckpointsTest +{ + [Fact] + public void HttpCheckpoints_ContainsList() + { + Assert.NotNull(HttpCheckpoints.Checkpoints); + Assert.True(HttpCheckpoints.Checkpoints.Length > 0); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs new file mode 100644 index 0000000000..157f95ec9f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpClientLatencyLogEnricherTest.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using Microsoft.Extensions.Http.Telemetry.Latency.Internal; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Extensions.Telemetry.Latency; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Latency.Test.Internal; + +public class HttpClientLatencyLogEnricherTest +{ + [Fact] + public void HttpClientLatencyLogEnricher_NoOp_OnRequest() + { + var lcti = HttpMockProvider.GetTokenIssuer(); + var checkpoints = new ArraySegment(new[] { new Checkpoint("a", default, default), new Checkpoint("b", default, default) }); + var ld = new LatencyData(default, checkpoints, default, default, default); + var lc = HttpMockProvider.GetLatencyContext(); + lc.Setup(lc => lc.LatencyData).Returns(ld); + var context = new HttpClientLatencyContext(); + context.Set(lc.Object); + + var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object); + Mock mockEnrichmentPropertyBag = new Mock(); + enricher.Enrich(mockEnrichmentPropertyBag.Object, null, null); + mockEnrichmentPropertyBag.Verify(m => m.Add(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void HttpClientLatencyLogEnricher_Enriches_OnResponseWithoutHeader() + { + var lcti = HttpMockProvider.GetTokenIssuer(); + var checkpoints = new ArraySegment(new[] { new Checkpoint("a", default, default), new Checkpoint("b", default, default) }); + var ld = new LatencyData(default, checkpoints, default, default, default); + var lc = HttpMockProvider.GetLatencyContext(); + lc.Setup(lc => lc.LatencyData).Returns(ld); + var context = new HttpClientLatencyContext(); + context.Set(lc.Object); + + using HttpResponseMessage httpResponseMessage = new(); + + var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object); + Mock mockEnrichmentPropertyBag = new Mock(); + + enricher.Enrich(mockEnrichmentPropertyBag.Object, null, httpResponseMessage); + mockEnrichmentPropertyBag.Verify(m => m.Add(It.Is(s => s.Equals("latencyInfo")), It.Is(s => s.Contains("a/b"))), Times.Once); + } + + [Fact] + public void HttpClientLatencyLogEnricher_Enriches_OnResponseWithHeader() + { + var lcti = HttpMockProvider.GetTokenIssuer(); + var checkpoints = new ArraySegment(new[] { new Checkpoint("a", default, default), new Checkpoint("b", default, default) }); + var ld = new LatencyData(default, checkpoints, default, default, default); + var lc = HttpMockProvider.GetLatencyContext(); + lc.Setup(lc => lc.LatencyData).Returns(ld); + var context = new HttpClientLatencyContext(); + context.Set(lc.Object); + + using HttpResponseMessage httpResponseMessage = new(); + string serverName = "serverNameVal"; + httpResponseMessage.Headers.Add(TelemetryConstants.ServerApplicationNameHeader, serverName); + + var enricher = new HttpClientLatencyLogEnricher(context, lcti.Object); + Mock mockEnrichmentPropertyBag = new Mock(); + + enricher.Enrich(mockEnrichmentPropertyBag.Object, null, httpResponseMessage); + mockEnrichmentPropertyBag.Verify(m => m.Add(It.Is(s => s.Equals("latencyInfo")), It.Is(s => s.Contains("a/b") && s.Contains(serverName))), Times.Once); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs new file mode 100644 index 0000000000..4c15955780 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpLatencyTelemetryHandlerTest.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AmbientMetadata; +using Microsoft.Extensions.Http.Telemetry.Latency.Internal; +using Microsoft.Extensions.Options; +using Moq; +using Moq.Protected; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Latency.Test.Internal; + +public class HttpLatencyTelemetryHandlerTest +{ + [Fact] + public void HttpLatencyTelemetryHandler_InvokesTokenIssuer() + { + var lc = HttpMockProvider.GetLatencyContext(); + var lcp = HttpMockProvider.GetContextProvider(lc); + var context = new HttpClientLatencyContext(); + var sop = new Mock>(); + sop.Setup(a => a.Value).Returns(new ApplicationMetadata()); + var hop = new Mock>(); + hop.Setup(a => a.Value).Returns(new HttpClientLatencyTelemetryOptions()); + + var lcti = HttpMockProvider.GetTokenIssuer(); + var lcti2 = HttpMockProvider.GetTokenIssuer(); + + using var listener = HttpMockProvider.GetListener(context, lcti.Object); + using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object); + + lcti2.Verify(a => a.GetCheckpointToken(It.Is(s => !HttpCheckpoints.Checkpoints.Contains(s))), Times.Never); + lcti2.Verify(a => a.GetCheckpointToken(It.Is(s => HttpCheckpoints.Checkpoints.Contains(s)))); + } + + [Fact] + public async Task HttpLatencyTelemetryHandler_SetsLatencyContext() + { + var lc = HttpMockProvider.GetLatencyContext(); + var lcp = HttpMockProvider.GetContextProvider(lc); + var context = new HttpClientLatencyContext(); + var sop = new Mock>(); + sop.Setup(a => a.Value).Returns(new ApplicationMetadata()); + var hop = new Mock>(); + hop.Setup(a => a.Value).Returns(new HttpClientLatencyTelemetryOptions()); + + var lcti = HttpMockProvider.GetTokenIssuer(); + var lcti2 = HttpMockProvider.GetTokenIssuer(); + + using var listener = HttpMockProvider.GetListener(context, lcti.Object); + using var req = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new($"http://default-uri.com/foo") + }; + + var resp = new Mock(); + var mockHandler = new Mock(); + mockHandler.Protected().Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()).Callback((req, _) => + { + Assert.NotNull(context.Get()); + Assert.True(req.Headers.Contains(TelemetryConstants.ClientApplicationNameHeader)); + }).Returns(Task.FromResult(resp.Object)); + + using var handler = new HttpLatencyTelemetryHandler(listener, lcti2.Object, lcp.Object, hop.Object, sop.Object) + { + InnerHandler = mockHandler.Object + }; + + using var client = new System.Net.Http.HttpClient(handler); + await client.SendAsync(req, It.IsAny()).ConfigureAwait(false); + Assert.Null(context.Get()); + } + + [Fact] + public void HttpLatencyTelemetryHandler_IfDetailsDisabled_DoesNotEnableListener() + { + var lc = HttpMockProvider.GetLatencyContext(); + var lcp = HttpMockProvider.GetContextProvider(lc); + var context = new HttpClientLatencyContext(); + var sop = new Mock>(); + sop.Setup(a => a.Value).Returns(new ApplicationMetadata()); + var hop = new Mock>(); + hop.Setup(a => a.Value).Returns(new HttpClientLatencyTelemetryOptions { EnableDetailedLatencyBreadkdown = false }); + var lcti = HttpMockProvider.GetTokenIssuer(); + + using var listener = HttpMockProvider.GetListener(context, lcti.Object); + using var handler = new HttpLatencyTelemetryHandler(listener, lcti.Object, lcp.Object, hop.Object, sop.Object); + Assert.False(listener.Enabled); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpMockProvider.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpMockProvider.cs new file mode 100644 index 0000000000..a7f203bb51 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpMockProvider.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Tracing; +using System.Threading; +using Microsoft.Extensions.Http.Telemetry.Latency.Internal; +using Microsoft.Extensions.Telemetry.Latency; +using Moq; + +namespace Microsoft.Extensions.Http.Telemetry.Latency.Test.Internal; + +internal class HttpMockProvider +{ + public static HttpRequestLatencyListener GetListener(HttpClientLatencyContext httpClientLatencyContext, ILatencyContextTokenIssuer tokenIssuer) + { + HttpRequestLatencyListener hrll = new HttpRequestLatencyListener(httpClientLatencyContext, tokenIssuer); + return hrll; + } + + public static Mock GetTokenIssuer() + { + var lcti = new Mock(); + lcti.Setup(a => a.GetCheckpointToken(It.IsAny())) + .Returns((string c) => { return new CheckpointToken(c, 0); }); + return lcti; + } + + public static Mock GetContextProvider(Mock lc) + { + var lcp = new Mock(); + lcp.Setup(a => a.CreateContext()).Returns(lc.Object); + + return lcp; + } + + public static Mock GetLatencyContext() + { + var lc = new Mock(); + lc.Setup(a => a.AddCheckpoint(It.IsAny())); + return lc; + } + + public class MockEventSource : EventSource + { + public int OnEventInvoked; + + protected override void OnEventCommand(System.Diagnostics.Tracing.EventCommandEventArgs command) + { + Interlocked.Increment(ref OnEventInvoked); + } + } + + public class HttpMockEventSource : MockEventSource + { + } + + public class SockeyMockEventSource : MockEventSource + { + } + + public class NameResolutionEventSource : MockEventSource + { + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpRequestLatencyListenerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpRequestLatencyListenerTest.cs new file mode 100644 index 0000000000..70d1c46068 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Latency/Internal/HttpRequestLatencyListenerTest.cs @@ -0,0 +1,214 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.Extensions.Http.Telemetry.Latency.Internal; +using Microsoft.Extensions.Telemetry.Latency; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Latency.Test.Internal; + +public class HttpRequestLatencyListenerTest +{ + [Fact] + public void HttpClientLatencyContext_Set_BasicFunction() + { + var lc = HttpMockProvider.GetLatencyContext(); + var context = new HttpClientLatencyContext(); + context.Set(lc.Object); + Assert.Equal(context.Get(), lc.Object); + context.Unset(); + Assert.Null(context.Get()); + } + + [Fact] + public void HttpRequestLatencyListener_InvokesTokenIssuer() + { + var lcti = HttpMockProvider.GetTokenIssuer(); + var lc = HttpMockProvider.GetLatencyContext(); + var context = new HttpClientLatencyContext(); + context.Set(lc.Object); + + using var listener = HttpMockProvider.GetListener(context, lcti.Object); + Assert.NotNull(listener); + + lcti.Verify(a => a.GetCheckpointToken(It.Is(s => !HttpCheckpoints.Checkpoints.Contains(s))), Times.Never); + lcti.Verify(a => a.GetCheckpointToken(It.Is(s => HttpCheckpoints.Checkpoints.Contains(s)))); + } + + [Fact] + public void HttpRequestLatencyListener_OnDisabled_DoesNotEnableEventSource() + { + var lcti = HttpMockProvider.GetTokenIssuer(); + var lc = HttpMockProvider.GetLatencyContext(); + var context = new HttpClientLatencyContext(); + context.Set(lc.Object); + + using var listener = HttpMockProvider.GetListener(context, lcti.Object); + Assert.NotNull(listener); + + using var es = new HttpMockProvider.MockEventSource(); + listener.OnEventSourceCreated("test", es); + Assert.Equal(0, es.OnEventInvoked); + Assert.False(es.IsEnabled()); + + using var esSockets = new HttpMockProvider.SockeyMockEventSource(); + listener.OnEventSourceCreated("System.Net.Sockets", esSockets); + Assert.Equal(0, esSockets.OnEventInvoked); + Assert.False(esSockets.IsEnabled()); + + using var esHttp = new HttpMockProvider.HttpMockEventSource(); + listener.OnEventSourceCreated("System.Net.Http", esHttp); + Assert.Equal(0, esHttp.OnEventInvoked); + Assert.False(esHttp.IsEnabled()); + + using var esNameRes = new HttpMockProvider.NameResolutionEventSource(); + listener.OnEventSourceCreated("System.Net.NameResolution", esNameRes); + Assert.Equal(0, esNameRes.OnEventInvoked); + Assert.False(esNameRes.IsEnabled()); + } + + [Fact] + public void HttpRequestLatencyListener_OnEventSourceCreated_NonHttpSources() + { + var lcti = HttpMockProvider.GetTokenIssuer(); + var lc = HttpMockProvider.GetLatencyContext(); + var context = new HttpClientLatencyContext(); + context.Set(lc.Object); + + using var listener = HttpMockProvider.GetListener(context, lcti.Object); + Assert.NotNull(listener); + listener.Enable(); + + using var es = new HttpMockProvider.MockEventSource(); + listener.OnEventSourceCreated("test", es); + Assert.Equal(0, es.OnEventInvoked); + Assert.False(es.IsEnabled()); + } + + [Fact] + public void HttpRequestLatencyListener_OnEventSourceCreated_HttpSources() + { + var lcti = HttpMockProvider.GetTokenIssuer(); + var lc = HttpMockProvider.GetLatencyContext(); + var context = new HttpClientLatencyContext(); + context.Set(lc.Object); + + using var listener = HttpMockProvider.GetListener(context, lcti.Object); + Assert.NotNull(listener); + listener.Enable(); + + using var esSockets = new HttpMockProvider.SockeyMockEventSource(); + listener.OnEventSourceCreated("System.Net.Sockets", esSockets); + Assert.Equal(1, esSockets.OnEventInvoked); + Assert.True(esSockets.IsEnabled()); + + using var esHttp = new HttpMockProvider.HttpMockEventSource(); + listener.OnEventSourceCreated("System.Net.Http", esHttp); + Assert.Equal(1, esHttp.OnEventInvoked); + Assert.True(esHttp.IsEnabled()); + + using var esNameRes = new HttpMockProvider.NameResolutionEventSource(); + listener.OnEventSourceCreated("System.Net.NameResolution", esNameRes); + Assert.Equal(1, esNameRes.OnEventInvoked); + Assert.True(esNameRes.IsEnabled()); + } + + [Fact] + public void HttpRequestLatencyListener_OnEventSourceCreated_Twice() + { + var lcti = HttpMockProvider.GetTokenIssuer(); + var lc = HttpMockProvider.GetLatencyContext(); + var context = new HttpClientLatencyContext(); + context.Set(lc.Object); + + using var listener = HttpMockProvider.GetListener(context, lcti.Object); + Assert.NotNull(listener); + listener.Enable(); + + using var esSockets = new HttpMockProvider.SockeyMockEventSource(); + listener.OnEventSourceCreated("System.Net.Sockets", esSockets); + Assert.Equal(1, esSockets.OnEventInvoked); + Assert.True(esSockets.IsEnabled()); + + listener.OnEventSourceCreated("System.Net.Sockets", esSockets); + Assert.Equal(1, esSockets.OnEventInvoked); + Assert.True(esSockets.IsEnabled()); + } + + [Fact] + public void HttpRequestLatencyListener_OnEventWritten_DoesNotAddCheckpoints_NonHttp() + { + var lcti = HttpMockProvider.GetTokenIssuer(); + var lc = HttpMockProvider.GetLatencyContext(); + var context = new HttpClientLatencyContext(); + context.Set(lc.Object); + + using var listener = HttpMockProvider.GetListener(context, lcti.Object); + + var events = new[] + { + "ConnectionEstablished", "RequestLeftQueue", "ResolutionStop", "ConnectStart", "New" + }; + + for (int i = 0; i < events.Length; i++) + { + listener.OnEventWritten("System.Net", events[i]); + } + + lc.Verify(a => a.AddCheckpoint(It.IsAny()), Times.Never); + } + + [Fact] + public void HttpRequestLatencyListener_OnEventWritten_AddsCheckpoints_Http() + { + var lcti = HttpMockProvider.GetTokenIssuer(); + var lc = HttpMockProvider.GetLatencyContext(); + var context = new HttpClientLatencyContext(); + context.Set(lc.Object); + + using var listener = HttpMockProvider.GetListener(context, lcti.Object); + + var httpEvents = new[] + { + "ConnectionEstablished", "RequestLeftQueue", "RequestContentStart", "RequestContentStop", + "ResponseHeadersStart", "ResponseHeadersStop", "ResponseContentStart", "ResponseContentStop" + }; + int numHttpEvents = httpEvents.Length; + + for (int i = 0; i < numHttpEvents; i++) + { + listener.OnEventWritten("System.Net.Http", httpEvents[i]); + } + + lc.Verify(a => a.AddCheckpoint(It.IsAny()), Times.Exactly(numHttpEvents)); + + var socketEvents = new[] + { + "ConnectStart", "ConnectStop" + }; + int numSocketEvents = socketEvents.Length; + + for (int i = 0; i < numSocketEvents; i++) + { + listener.OnEventWritten("System.Net.Sockets", socketEvents[i]); + } + + lc.Verify(a => a.AddCheckpoint(It.IsAny()), Times.Exactly(numHttpEvents + numSocketEvents)); + + var dnsEvents = new[] + { + "ResolutionStart", "ResolutionStop" + }; + int numDnsEvents = dnsEvents.Length; + + for (int i = 0; i < numDnsEvents; i++) + { + listener.OnEventWritten("System.Net.NameResolution", dnsEvents[i]); + } + + lc.Verify(a => a.AddCheckpoint(It.IsAny()), + Times.Exactly(numHttpEvents + numSocketEvents + numDnsEvents)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpClientLoggingAcceptanceTest.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpClientLoggingAcceptanceTest.cs new file mode 100644 index 0000000000..3437c572c9 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpClientLoggingAcceptanceTest.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test; + +public class HttpClientLoggingAcceptanceTest +{ + [Theory] + [InlineData(4_096)] + [InlineData(8_192)] + [InlineData(16_384)] + [InlineData(315_883)] + public async Task HttpClientLoggingHandler_LogsBodyDataUpToSpecifiedLimit(int limit) + { + const string RequestPath = "https://we.wont.hit.this.dd22anyway.com"; + + await using var provider = new ServiceCollection() + .AddFakeLogging() + .AddFakeRedaction() + .AddHttpClient(nameof(HttpClientLoggingHandler_LogsBodyDataUpToSpecifiedLimit)) + .AddHttpClientLogging(x => + { + x.ResponseHeadersDataClasses.Add("ResponseHeader", SimpleClassifications.PrivateData); + x.RequestHeadersDataClasses.Add("RequestHeader", SimpleClassifications.PrivateData); + x.RequestHeadersDataClasses.Add("RequestHeader2", SimpleClassifications.PrivateData); + x.RequestBodyContentTypes.Add("application/json"); + x.ResponseBodyContentTypes.Add("application/json"); + x.BodySizeLimit = limit; + x.LogBody = true; + }) + .Services + .BlockRemoteCall() + .BuildServiceProvider(); + + var client = provider + .GetRequiredService() + .CreateClient(nameof(HttpClientLoggingHandler_LogsBodyDataUpToSpecifiedLimit)); + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri(RequestPath), + }; + httpRequestMessage.Headers.Add("requestHeader", "Request Value"); + httpRequestMessage.Headers.Add("ReQuEStHeAdEr2", new List { "Request Value 2", "Request Value 3" }); + + var content = await client.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + var responseStream = await content.Content.ReadAsStreamAsync(); + var length = (int)responseStream.Length > limit ? limit : (int)responseStream.Length; + var buffer = new byte[length]; + _ = await responseStream.ReadAsync(buffer, 0, length); + var responseString = Encoding.UTF8.GetString(buffer); + + var collector = provider.GetFakeLogCollector(); + var logRecord = collector.GetSnapshot().Single(l => l.Category == "Microsoft.Extensions.Http.Telemetry.Logging.Internal.HttpLoggingHandler"); + var state = logRecord.State as List>; + state.Should().Contain(kvp => kvp.Value == responseString); + state.Should().Contain(kvp => kvp.Value == "Request Value"); + state.Should().Contain(kvp => kvp.Value == "Request Value 2,Request Value 3"); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpClientLoggingDimensionsTest.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpClientLoggingDimensionsTest.cs new file mode 100644 index 0000000000..8addb515fb --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpClientLoggingDimensionsTest.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Reflection; +using FluentAssertions; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test; + +public class HttpClientLoggingDimensionsTest +{ + [Fact] + public void GetDimensionNames_ReturnsAnArrayOfDimensionNames() + { + var actualDimensions = HttpClientLoggingDimensions.DimensionNames; + var expectedDimensions = GetStringConstants(typeof(HttpClientLoggingDimensions)); + + actualDimensions.Should().BeEquivalentTo(expectedDimensions); + } + + private static string[] GetStringConstants(IReflect type) + { + var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static); + + return fields + .Where(f => f.IsLiteral && f.FieldType == typeof(string)) + .Select(f => (string)f.GetValue(null)!) + .ToArray(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpClientLoggingExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpClientLoggingExtensionsTest.cs new file mode 100644 index 0000000000..eb83e5f70c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpClientLoggingExtensionsTest.cs @@ -0,0 +1,858 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AutoFixture; +using FluentAssertions; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test; + +public class HttpClientLoggingExtensionsTest +{ + private readonly Fixture _fixture; + + public HttpClientLoggingExtensionsTest() + { + _fixture = new Fixture(); + } + + [Fact] + public void AddHttpClientLogging_AnyArgumentIsNull_Throws() + { + var act = () => ((IHttpClientBuilder)null!).AddHttpClientLogging(); + act.Should().Throw(); + + act = () => ((IHttpClientBuilder)null!).AddHttpClientLogging(_ => { }); + act.Should().Throw(); + + act = () => ((IHttpClientBuilder)null!).AddHttpClientLogging(Mock.Of()); + act.Should().Throw(); + + act = () => Mock.Of().AddHttpClientLogging((Action)null!); + act.Should().Throw(); + + act = () => Mock.Of().AddHttpClientLogging((IConfigurationSection)null!); + act.Should().Throw(); + } + + [Fact] + public void AddHttpClientLogging_ServiceCollection_AnyArgumentIsNull_Throws() + { + var act = () => ((IServiceCollection)null!).AddDefaultHttpClientLogging(); + act.Should().Throw(); + + act = () => ((IServiceCollection)null!).AddDefaultHttpClientLogging(_ => { }); + act.Should().Throw(); + + act = () => ((IServiceCollection)null!).AddDefaultHttpClientLogging(Mock.Of()); + act.Should().Throw(); + + act = () => Mock.Of().AddDefaultHttpClientLogging((Action)null!); + act.Should().Throw(); + + act = () => Mock.Of().AddDefaultHttpClientLogging((IConfigurationSection)null!); + act.Should().Throw(); + } + + [Fact] + public void AddHttpClientLogEnricher_AnyArgumentIsNull_Throws() + { + var act = () => ((IServiceCollection)null!).AddHttpClientLogEnricher(); + act.Should().Throw(); + } + + [Fact] + public void AddHttpClientLogging_ConfiguredOptionsWithNamedClient_ShouldNotBeSame() + { + var services = new ServiceCollection(); + + var provider = services + .AddHttpClient("test1") + .AddHttpClientLogging(options => options.BodyReadTimeout = TimeSpan.FromSeconds(1)) + .Services + .AddHttpClient("test2") + .AddHttpClientLogging(options => options.BodyReadTimeout = TimeSpan.FromSeconds(2)) + .Services + .BuildServiceProvider(); + + var optionsFirst = provider.GetRequiredService>().Get("test1"); + var optionsSecond = provider.GetRequiredService>().Get("test2"); + optionsFirst.Should().NotBeNull(); + optionsSecond.Should().NotBeNull(); + optionsFirst.Should().NotBeEquivalentTo(optionsSecond); + optionsFirst.BodyReadTimeout.Should().Be(TimeSpan.FromSeconds(1)); + optionsSecond.BodyReadTimeout.Should().Be(TimeSpan.FromSeconds(2)); + } + + [Fact] + public void AddHttpClientLogging_ConfiguredOptionsWithTypedClient_ShouldNotBeSame() + { + var services = new ServiceCollection(); + + var provider = services + .AddHttpClient() + .AddHttpClientLogging(options => options.BodyReadTimeout = TimeSpan.FromSeconds(1)) + .Services + .AddHttpClient() + .AddHttpClientLogging(options => options.BodyReadTimeout = TimeSpan.FromSeconds(2)) + .Services + .BuildServiceProvider(); + + var optionsFirst = provider.GetRequiredService>().Get(nameof(ITestHttpClient1)); + var optionsSecond = provider.GetRequiredService>().Get(nameof(ITestHttpClient2)); + optionsFirst.Should().NotBeNull(); + optionsSecond.Should().NotBeNull(); + optionsFirst.Should().NotBeEquivalentTo(optionsSecond); + optionsFirst.BodyReadTimeout.Should().Be(TimeSpan.FromSeconds(1)); + optionsSecond.BodyReadTimeout.Should().Be(TimeSpan.FromSeconds(2)); + } + + [Fact] + public void AddHttpClientLogging_DefaultOptions_CreatesOptionsCorrectly() + { + var services = new ServiceCollection(); + + var provider = services + .AddHttpClient("") + .AddHttpClientLogging(o => o.RequestHeadersDataClasses.Add("test1", SimpleClassifications.PrivateData)) + .Services + .AddHttpClient("") + .AddHttpClientLogging(o => o.RequestHeadersDataClasses.Add("test2", SimpleClassifications.PrivateData)) + .Services + .BuildServiceProvider(); + + var options = provider.GetRequiredService>().Value; + options.RequestHeadersDataClasses.Should().HaveCount(2); + options.RequestHeadersDataClasses.Should().ContainKeys(new List { "test1", "test2" }); + options.RequestHeadersDataClasses.Should().ContainValues(new List { SimpleClassifications.PrivateData }); + } + + [Fact] + public void AddHttpClientLogging_GivenActionDelegate_RegistersInDi() + { + var requestBodyContentType = "application/json"; + var responseBodyContentType = "application/json"; + var requestHeader = _fixture.Create(); + var responseHeader = _fixture.Create(); + var bodyReadTimeout = TimeSpan.FromSeconds(1); + var bodySizeLimit = 100; + var formatRequestPath = _fixture.Create(); + var formatRequestPathParameters = _fixture.Create(); + var logStart = _fixture.Create(); + var paramToRedact = new KeyValuePair("userId", SimpleClassifications.PrivateData); + + var services = new ServiceCollection(); + + services + .AddHttpClient("test") + .AddHttpClientLogging(options => + { + options.RequestBodyContentTypes.Add(requestBodyContentType); + options.ResponseBodyContentTypes.Add(responseBodyContentType); + options.BodyReadTimeout = bodyReadTimeout; + options.BodySizeLimit = bodySizeLimit; + options.RequestPathLoggingMode = formatRequestPath; + options.RequestPathParameterRedactionMode = formatRequestPathParameters; + options.RequestHeadersDataClasses.Add(requestHeader, SimpleClassifications.PrivateData); + options.ResponseHeadersDataClasses.Add(responseHeader, SimpleClassifications.PrivateData); + options.RouteParameterDataClasses.Add(paramToRedact); + options.LogRequestStart = logStart; + }); + + using var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().Get("test"); + + options.Should().NotBeNull(); + options.RequestBodyContentTypes.Should().ContainSingle(); + options.RequestBodyContentTypes.Should().Contain(requestBodyContentType); + options.ResponseBodyContentTypes.Should().ContainSingle(); + options.ResponseBodyContentTypes.Should().Contain(responseBodyContentType); + options.BodyReadTimeout.Should().Be(bodyReadTimeout); + options.BodySizeLimit.Should().Be(bodySizeLimit); + options.RequestPathLoggingMode.Should().Be(formatRequestPath); + options.RequestPathParameterRedactionMode.Should().Be(formatRequestPathParameters); + options.RequestHeadersDataClasses.Should().ContainSingle(); + options.RequestHeadersDataClasses.Should().Contain(requestHeader, SimpleClassifications.PrivateData); + options.ResponseHeadersDataClasses.Should().ContainSingle(); + options.ResponseHeadersDataClasses.Should().Contain(responseHeader, SimpleClassifications.PrivateData); + options.RouteParameterDataClasses.Should().ContainSingle(); + options.RouteParameterDataClasses.Should().Contain(paramToRedact); + options.LogRequestStart.Should().Be(logStart); + } + + [Fact] + public async Task AddHttpClientLogging_GivenInvalidOptions_Throws() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => + { + services + .AddFakeRedaction() + .AddHttpClient("test") + .AddHttpClientLogging(options => + { + options.BodyReadTimeout = TimeSpan.Zero; + options.BodySizeLimit = -1; + }); + }) + .Build(); + + var act = async () => await host.StartAsync().ConfigureAwait(false); + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [Theory] + [InlineData(2)] + [InlineData(5)] + [InlineData(30)] + [InlineData(59)] + [InlineData(17)] + public void AddHttpClientLogging_GivenConfigurationSection_SetsTimeoutCorrectly(int seconds) + { + var timeoutValue = TimeSpan.FromSeconds(seconds); + + using var provider = new ServiceCollection() + .AddHttpClient("test") + .AddHttpClientLogging(TestConfiguration.GetHttpClientLoggingConfigurationSection(timeoutValue)) + .Services + .BuildServiceProvider(); + var options = provider + .GetRequiredService>().Get("test"); + + options.Should().NotBeNull(); + options.BodyReadTimeout.Should().Be(timeoutValue); + } + + [Fact] + public void AddHttpClientLogEnricher_RegistersEnricherInDI() + { + using var provider = new ServiceCollection() + .AddHttpClientLogEnricher() + .BuildServiceProvider(); + + var enricherRegistered = provider.GetService(); + + enricherRegistered.Should().NotBeNull(); + enricherRegistered.Should().BeOfType(); + } + + [Fact] + public void AddHttpClientLogging_ServiceCollection_GivenActionDelegate_RegistersInDi() + { + var requestBodyContentType = "application/json"; + var responseBodyContentType = "application/json"; + var requestHeader = _fixture.Create(); + var responseHeader = _fixture.Create(); + var bodyReadTimeout = TimeSpan.FromSeconds(1); + var bodySizeLimit = 100; + var formatRequestPath = _fixture.Create(); + var formatRequestPathParameters = _fixture.Create(); + var logStart = _fixture.Create(); + var paramToRedact = new KeyValuePair("userId", SimpleClassifications.PrivateData); + + var services = new ServiceCollection(); + + services + .AddFakeRedaction() + .AddHttpClient() + .AddDefaultHttpClientLogging(options => + { + options.RequestBodyContentTypes.Add(requestBodyContentType); + options.ResponseBodyContentTypes.Add(responseBodyContentType); + options.BodyReadTimeout = bodyReadTimeout; + options.BodySizeLimit = bodySizeLimit; + options.RequestPathLoggingMode = formatRequestPath; + options.RequestPathParameterRedactionMode = formatRequestPathParameters; + options.RequestHeadersDataClasses.Add(requestHeader, SimpleClassifications.PrivateData); + options.ResponseHeadersDataClasses.Add(responseHeader, SimpleClassifications.PrivateData); + options.RouteParameterDataClasses.Add(paramToRedact); + options.LogRequestStart = logStart; + }); + + using var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().Value; + + options.Should().NotBeNull(); + options.RequestBodyContentTypes.Should().ContainSingle(); + options.RequestBodyContentTypes.Should().Contain(requestBodyContentType); + options.ResponseBodyContentTypes.Should().ContainSingle(); + options.ResponseBodyContentTypes.Should().Contain(responseBodyContentType); + options.BodyReadTimeout.Should().Be(bodyReadTimeout); + options.BodySizeLimit.Should().Be(bodySizeLimit); + options.RequestPathLoggingMode.Should().Be(formatRequestPath); + options.RequestPathParameterRedactionMode.Should().Be(formatRequestPathParameters); + options.RequestHeadersDataClasses.Should().ContainSingle(); + options.RequestHeadersDataClasses.Should().Contain(requestHeader, SimpleClassifications.PrivateData); + options.ResponseHeadersDataClasses.Should().ContainSingle(); + options.ResponseHeadersDataClasses.Should().Contain(responseHeader, SimpleClassifications.PrivateData); + options.RouteParameterDataClasses.Should().ContainSingle(); + options.RouteParameterDataClasses.Should().Contain(paramToRedact); + options.LogRequestStart.Should().Be(logStart); + + using var httpClient = provider.GetRequiredService().CreateClient(); + Assert.NotNull(httpClient); + } + + [Fact] + public async Task AddHttpClientLogging_ServiceCollection_GivenInvalidOptions_Throws() + { + var provider = new ServiceCollection() + .AddFakeRedaction() + .AddHttpClient() + .AddDefaultHttpClientLogging(options => + { + options.BodyReadTimeout = TimeSpan.Zero; + options.BodySizeLimit = -1; + }) + .BuildServiceProvider(); + + var act = () => + provider + .GetRequiredService() + .StartAsync(CancellationToken.None); + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [Fact] + public void AddHttpClientLogging_ServiceCollectionAndHttpClientBuilder_Throws() + { + var provider = new ServiceCollection() + .AddFakeRedaction() + .AddHttpClient("test") + .AddHttpClientLogging().Services + .AddDefaultHttpClientLogging() + .BuildServiceProvider(); + + var act = () => provider.GetRequiredService().CreateClient("test"); + act.Should().Throw().WithMessage(HttpClientLoggingExtensions.HandlerAddedTwiceExceptionMessage); + } + + [Fact] + public void AddHttpClientLogging_HttpClientBuilderAndServiceCollection_Throws() + { + var provider = new ServiceCollection() + .AddFakeRedaction() + .AddDefaultHttpClientLogging() + .AddHttpClient("test") + .ConfigureHttpMessageHandlerBuilder(b => + b.AdditionalHandlers.Add(Mock + .Of())) // this is to kill mutants with .Any() vs. .All() calls when detecting already added delegating handlers. + .AddHttpClientLogging().Services + .BuildServiceProvider(); + + var act = () => provider.GetRequiredService().CreateClient("test"); + act.Should().Throw().WithMessage(HttpClientLoggingExtensions.HandlerAddedTwiceExceptionMessage); + } + + [Theory] + [InlineData(2)] + [InlineData(5)] + [InlineData(30)] + [InlineData(59)] + [InlineData(17)] + public void AddHttpClientLogging_ServiceCollection_GivenConfigurationSection_SetsTimeoutCorrectly(int seconds) + { + var timeoutValue = TimeSpan.FromSeconds(seconds); + + using var provider = new ServiceCollection() + .AddFakeRedaction() + .AddHttpClient() + .AddDefaultHttpClientLogging(TestConfiguration.GetHttpClientLoggingConfigurationSection(timeoutValue)) + .BuildServiceProvider(); + var options = provider + .GetRequiredService>().Value; + + options.Should().NotBeNull(); + options.BodyReadTimeout.Should().Be(timeoutValue); + + using var httpClient = provider.GetRequiredService().CreateClient(); + Assert.NotNull(httpClient); + } + + [Fact] + public void AddHttpClientLogging_ServiceCollection_CreatesClientSuccessfully() + { + using var sp = new ServiceCollection() + .AddFakeRedaction() + .AddHttpClient() + .AddDefaultHttpClientLogging() + .BuildServiceProvider(); + + using var httpClient = sp.GetRequiredService().CreateClient(); + Assert.NotNull(httpClient); + } + + [Fact] + public async Task AddHttpClientLogging_ServiceCollectionAndEnrichers_EnrichesLogsWithAllEnrichers() + { + const string RequestPath = "https://we.wont.hit.this.dd22anyway.com"; + + await using var sp = new ServiceCollection() + .AddFakeLogging() + .AddFakeRedaction() + .AddDefaultHttpClientLogging() + .AddHttpClientLogEnricher() + .AddHttpClientLogEnricher() + .AddHttpClient("testClient").Services + .BlockRemoteCall() + .BuildServiceProvider(); + + using var httpClient = sp.GetRequiredService().CreateClient("testClient"); + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri(RequestPath), + }; + + _ = await httpClient.SendAsync(httpRequestMessage).ConfigureAwait(false); + var collector = sp.GetFakeLogCollector(); + var logRecord = collector.GetSnapshot().Single(logRecord => logRecord.Category == "Microsoft.Extensions.Http.Telemetry.Logging.Internal.HttpLoggingHandler"); + + Assert.Empty(logRecord.Message); + var state = logRecord.StructuredState; + var enricher1 = sp.GetServices().SingleOrDefault(enn => enn is EnricherWithCounter) as EnricherWithCounter; + var enricher2 = sp.GetServices().SingleOrDefault(enn => enn is TestEnricher) as TestEnricher; + + enricher1.Should().NotBeNull(); + enricher2.Should().NotBeNull(); + + enricher1!.TimesCalled.Should().Be(1); + state!.Single(kvp => kvp.Key == enricher2!.KvpRequest.Key).Value.Should().Be(enricher2!.KvpRequest.Value!.ToString()); + } + + [Fact] + public async Task AddHttpClientLogging_WithNamedHttpClients_WorksCorrectly() + { + const string RequestPath = "https://we.wont.hit.this.dd22anyway.com"; + + await using var provider = new ServiceCollection() + .AddFakeLogging() + .AddFakeRedaction() + .AddHttpClient("namedClient1") + .AddHttpClientLogging(o => + { + o.ResponseHeadersDataClasses.Add("ResponseHeader", SimpleClassifications.PrivateData); + o.RequestHeadersDataClasses.Add("RequestHeader", SimpleClassifications.PrivateData); + o.RequestHeadersDataClasses.Add("RequestHeaderFirst", SimpleClassifications.PrivateData); + o.RequestBodyContentTypes.Add("application/json"); + o.ResponseBodyContentTypes.Add("application/json"); + o.LogBody = true; + }).Services + .AddHttpClient("namedClient2") + .AddHttpClientLogging(o => + { + o.ResponseHeadersDataClasses.Add("ResponseHeader", SimpleClassifications.PrivateData); + o.RequestHeadersDataClasses.Add("RequestHeader", SimpleClassifications.PrivateData); + o.RequestHeadersDataClasses.Add("RequestHeaderSecond", SimpleClassifications.PrivateData); + o.RequestBodyContentTypes.Add("application/json"); + o.ResponseBodyContentTypes.Add("application/json"); + o.LogBody = true; + }).Services + .BlockRemoteCall() + .BuildServiceProvider(); + + using var namedClient1 = provider.GetRequiredService().CreateClient("namedClient1"); + using var namedClient2 = provider.GetRequiredService().CreateClient("namedClient2"); + + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri(RequestPath), + }; + httpRequestMessage.Headers.Add("requestHeader", "Request Value"); + httpRequestMessage.Headers.Add("ReQuEStHeAdErFirst", new List { "Request Value 2", "Request Value 3" }); + var responseString = await SendRequest(namedClient1, httpRequestMessage); + var collector = provider.GetFakeLogCollector(); + var logRecord = collector.GetSnapshot().Single(l => l.Category == "Microsoft.Extensions.Http.Telemetry.Logging.Internal.HttpLoggingHandler"); + var state = logRecord.State as List>; + state.Should().Contain(kvp => kvp.Value == responseString); + state.Should().Contain(kvp => kvp.Value == "Request Value"); + state.Should().Contain(kvp => kvp.Value == "Request Value 2,Request Value 3"); + + using var httpRequestMessage2 = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri(RequestPath), + }; + httpRequestMessage2.Headers.Add("requestHeader", "Request Value"); + httpRequestMessage2.Headers.Add("ReQuEStHeAdErSecond", new List { "Request Value 2", "Request Value 3" }); + collector.Clear(); + responseString = await SendRequest(namedClient2, httpRequestMessage2); + logRecord = collector.GetSnapshot().Single(l => l.Category == "Microsoft.Extensions.Http.Telemetry.Logging.Internal.HttpLoggingHandler"); + state = logRecord.State as List>; + state.Should().Contain(kvp => kvp.Value == responseString); + state.Should().Contain(kvp => kvp.Value == "Request Value"); + state.Should().Contain(kvp => kvp.Value == "Request Value 2,Request Value 3"); + } + + private static async Task SendRequest(System.Net.Http.HttpClient httpClient, HttpRequestMessage httpRequestMessage) + { + var content = await httpClient + .SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead) + .ConfigureAwait(false); + var responseStream = await content.Content.ReadAsStreamAsync(); + var buffer = new byte[32768]; + _ = await responseStream.ReadAsync(buffer, 0, 32768); + return Encoding.UTF8.GetString(buffer); + } + + [Fact] + public async Task AddHttpClientLogging_WithTypedHttpClients_WorksCorrectly() + { + const string RequestPath = "https://we.wont.hit.this.dd22anyway.com"; + + await using var provider = new ServiceCollection() + .AddFakeLogging() + .AddFakeRedaction() + .AddSingleton() + .AddSingleton() + .AddHttpClient() + .AddHttpClientLogging(x => + { + x.ResponseHeadersDataClasses.Add("ResponseHeader", SimpleClassifications.PrivateData); + x.RequestHeadersDataClasses.Add("RequestHeader", SimpleClassifications.PrivateData); + x.RequestHeadersDataClasses.Add("RequestHeader2", SimpleClassifications.PrivateData); + x.RequestBodyContentTypes.Add("application/json"); + x.ResponseBodyContentTypes.Add("application/json"); + x.BodySizeLimit = 10000; + x.LogBody = true; + }).Services + .AddHttpClient() + .AddHttpClientLogging(x => + { + x.ResponseHeadersDataClasses.Add("ResponseHeader", SimpleClassifications.PrivateData); + x.RequestHeadersDataClasses.Add("RequestHeader", SimpleClassifications.PrivateData); + x.RequestHeadersDataClasses.Add("RequestHeader2", SimpleClassifications.PrivateData); + x.RequestBodyContentTypes.Add("application/json"); + x.ResponseBodyContentTypes.Add("application/json"); + x.BodySizeLimit = 20000; + x.LogBody = true; + }).Services + .BlockRemoteCall() + .BuildServiceProvider(); + + var firstClient = provider.GetService() as TestHttpClient1; + var secondClient = provider.GetService() as TestHttpClient2; + + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri(RequestPath), + }; + httpRequestMessage.Headers.Add("requestHeader", "Request Value"); + httpRequestMessage.Headers.Add("ReQuEStHeAdEr2", new List { "Request Value 2", "Request Value 3" }); + var content = await firstClient!.SendRequest(httpRequestMessage).ConfigureAwait(false); + var collector = provider.GetFakeLogCollector(); + var responseStream = await content.Content.ReadAsStreamAsync(); + var buffer = new byte[10000]; + _ = await responseStream.ReadAsync(buffer, 0, 10000); + var responseString = Encoding.UTF8.GetString(buffer); + + var logRecord = collector.GetSnapshot().Single(l => l.Category == "Microsoft.Extensions.Http.Telemetry.Logging.Internal.HttpLoggingHandler"); + var state = logRecord.State as List>; + state.Should().Contain(kvp => kvp.Value == responseString); + state.Should().Contain(kvp => kvp.Value == "Request Value"); + state.Should().Contain(kvp => kvp.Value == "Request Value 2,Request Value 3"); + + using var httpRequestMessage2 = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri(RequestPath), + }; + httpRequestMessage2.Headers.Add("requestHeader", "Request Value"); + httpRequestMessage2.Headers.Add("ReQuEStHeAdEr2", new List { "Request Value 2", "Request Value 3" }); + collector.Clear(); + content = await secondClient!.SendRequest(httpRequestMessage2).ConfigureAwait(false); + responseStream = await content.Content.ReadAsStreamAsync(); + buffer = new byte[20000]; + _ = await responseStream.ReadAsync(buffer, 0, 20000); + responseString = Encoding.UTF8.GetString(buffer); + + logRecord = collector.GetSnapshot().Single(l => l.Category == "Microsoft.Extensions.Http.Telemetry.Logging.Internal.HttpLoggingHandler"); + state = logRecord.State as List>; + state.Should().Contain(kvp => kvp.Value == responseString); + state.Should().Contain(kvp => kvp.Value == "Request Value"); + state.Should().Contain(kvp => kvp.Value == "Request Value 2,Request Value 3"); + } + + [Theory] + [InlineData(HttpRouteParameterRedactionMode.Strict, "v1/unit/REDACTED/users/REDACTED:123")] + [InlineData(HttpRouteParameterRedactionMode.Loose, "v1/unit/999/users/REDACTED:123")] + [InlineData(HttpRouteParameterRedactionMode.None, "/v1/unit/999/users/123")] + public async Task AddHttpClientLogging_RedactSensitivePrams(HttpRouteParameterRedactionMode parameterRedactionMode, string redactedPath) + { + const string RequestPath = "https://fake.com/v1/unit/999/users/123"; + + await using var sp = new ServiceCollection() + .AddFakeLogging() + .AddFakeRedaction(o => o.RedactionFormat = "REDACTED:{0}") + .AddHttpClient() + .AddDefaultHttpClientLogging(o => + { + o.RouteParameterDataClasses.Add("userId", SimpleClassifications.PrivateData); + o.RequestPathParameterRedactionMode = parameterRedactionMode; + }) + .BlockRemoteCall() + .BuildServiceProvider(); + + using var httpClient = sp.GetRequiredService().CreateClient(); + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri(RequestPath), + }; + + var requestContext = sp.GetRequiredService(); + requestContext.RequestMetadata = new RequestMetadata + { + RequestRoute = "/v1/unit/{unitId}/users/{userId}" + }; + + _ = await httpClient.SendAsync(httpRequestMessage).ConfigureAwait(false); + + var collector = sp.GetFakeLogCollector(); + var logRecord = collector.GetSnapshot().Single(logRecord => logRecord.Category == "Microsoft.Extensions.Http.Telemetry.Logging.Internal.HttpLoggingHandler"); + var state = logRecord.State as List>; + state!.Single(kvp => kvp.Key == "httpPath").Value.Should().Be(redactedPath); + } + + [Theory] + [InlineData(HttpRouteParameterRedactionMode.Strict, "v1/unit/REDACTED/users/REDACTED:123")] + [InlineData(HttpRouteParameterRedactionMode.Loose, "v1/unit/999/users/REDACTED:123")] + public async Task AddHttpClientLogging_NamedHttpClient_RedactSensitivePrams(HttpRouteParameterRedactionMode parameterRedactionMode, string redactedPath) + { + const string RequestPath = "https://fake.com/v1/unit/999/users/123"; + + await using var sp = new ServiceCollection() + .AddFakeLogging() + .AddFakeRedaction(o => o.RedactionFormat = "REDACTED:{0}") + .AddHttpClient("test") + .AddHttpClientLogging(o => + { + o.RouteParameterDataClasses.Add("userId", SimpleClassifications.PrivateData); + o.RequestPathParameterRedactionMode = parameterRedactionMode; + }) + .Services + .BlockRemoteCall() + .BuildServiceProvider(); + + using var httpClient = sp.GetRequiredService().CreateClient("test"); + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri(RequestPath), + }; + + var requestContext = sp.GetRequiredService(); + requestContext.RequestMetadata = new RequestMetadata + { + RequestRoute = "/v1/unit/{unitId}/users/{userId}" + }; + + _ = await httpClient.SendAsync(httpRequestMessage).ConfigureAwait(false); + + var collector = sp.GetFakeLogCollector(); + var logRecord = collector.GetSnapshot().Single(logRecord => logRecord.Category == "Microsoft.Extensions.Http.Telemetry.Logging.Internal.HttpLoggingHandler"); + var state = logRecord.State as List>; + state!.Single(kvp => kvp.Key == "httpPath").Value.Should().Be(redactedPath); + } + + [Fact] + public void AddHttpClientLogging_WithNamedClients_RegistersNamedOptions() + { + const string FirstClientName = "1"; + const string SecondClientName = "2"; + + using var provider = new ServiceCollection() + .AddFakeRedaction() + .AddHttpClient(FirstClientName) + .AddHttpClientLogging(options => + { + options.LogRequestStart = true; + options.ResponseHeadersDataClasses = new Dictionary { { "test1", SimpleClassifications.PrivateData } }; + }) + .Services + .AddHttpClient(SecondClientName) + .AddHttpClientLogging(options => + { + options.LogRequestStart = false; + options.ResponseHeadersDataClasses = new Dictionary { { "test2", SimpleClassifications.PrivateData } }; + }) + .Services + .BuildServiceProvider(); + + var factory = provider.GetRequiredService(); + + var firstClient = factory.CreateClient(FirstClientName); + var secondClient = factory.CreateClient(SecondClientName); + firstClient.Should().NotBe(secondClient); + + var optionsFirst = provider.GetRequiredService>().Get(FirstClientName); + var optionsSecond = provider.GetRequiredService>().Get(SecondClientName); + optionsFirst.Should().NotBeNull(); + optionsSecond.Should().NotBeNull(); + optionsFirst.Should().NotBeEquivalentTo(optionsSecond); + } + + [Fact] + public void AddHttpClientLogging_WithTypedClients_RegistersNamedOptions() + { + using var provider = new ServiceCollection() + .AddFakeRedaction() + .AddSingleton() + .AddSingleton() + .AddHttpClient() + .AddHttpClientLogging(options => + { + options.LogRequestStart = true; + options.ResponseHeadersDataClasses = new Dictionary { { "test1", SimpleClassifications.PrivateData } }; + }) + .Services + .AddHttpClient() + .AddHttpClientLogging(options => + { + options.LogRequestStart = false; + options.ResponseHeadersDataClasses = new Dictionary { { "test2", SimpleClassifications.PrivateData } }; + }) + .Services + .BuildServiceProvider(); + + var firstClient = provider.GetService() as TestHttpClient1; + var secondClient = provider.GetService() as TestHttpClient2; + + firstClient.Should().NotBe(secondClient); + + var optionsFirst = provider.GetRequiredService>().Get(nameof(ITestHttpClient1)); + var optionsSecond = provider.GetRequiredService>().Get(nameof(ITestHttpClient2)); + optionsFirst.Should().NotBeNull(); + optionsSecond.Should().NotBeNull(); + optionsFirst.Should().NotBeEquivalentTo(optionsSecond); + } + + [Fact] + public void AddHttpClientLogging_WithTypedAndNamedClients_RegistersNamedOptions() + { + using var provider = new ServiceCollection() + .AddFakeRedaction() + .AddSingleton() + .AddSingleton() + .AddHttpClient() + .AddHttpClientLogging(options => + { + options.ResponseHeadersDataClasses = new Dictionary { { "test1", SimpleClassifications.PrivateData } }; + }) + .Services + .AddHttpClient() + .AddHttpClientLogging(options => + { + options.ResponseHeadersDataClasses = new Dictionary { { "test2", SimpleClassifications.PrivateData } }; + }) + .Services + .AddHttpClient("testClient3") + .AddHttpClientLogging(options => + { + options.ResponseHeadersDataClasses = new Dictionary { { "test3", SimpleClassifications.PrivateData } }; + }) + .Services + .AddHttpClient("testClient4") + .AddHttpClientLogging(options => + { + options.ResponseHeadersDataClasses = new Dictionary { { "test4", SimpleClassifications.PrivateData } }; + }) + .Services + .AddHttpClient("testClient5") + .AddHttpClientLogging(options => + { + options.ResponseHeadersDataClasses = new Dictionary { { "test5", SimpleClassifications.PrivateData } }; + }) + .Services + .AddDefaultHttpClientLogging(options => + { + options.ResponseHeadersDataClasses = new Dictionary { { "test6", SimpleClassifications.PrivateData } }; + }) + .BuildServiceProvider(); + + var optionsFirst = provider.GetRequiredService>().Get(nameof(ITestHttpClient1)); + var optionsSecond = provider.GetRequiredService>().Get(nameof(ITestHttpClient2)); + var optionsThird = provider.GetRequiredService>().Get("testClient3"); + var optionsFourth = provider.GetRequiredService>().Get("testClient4"); + var optionsFifth = provider.GetRequiredService>().Get("testClient5"); + var optionsSixth = provider.GetRequiredService>().Value; + + optionsFirst.Should().NotBeNull(); + optionsSecond.Should().NotBeNull(); + optionsFirst.Should().NotBeEquivalentTo(optionsSecond); + + optionsThird.Should().NotBeNull(); + optionsFourth.Should().NotBeNull(); + optionsThird.Should().NotBeEquivalentTo(optionsFourth); + + optionsFifth.Should().NotBeNull(); + optionsFifth.Should().NotBeEquivalentTo(optionsFourth); + + optionsSixth.Should().NotBeNull(); + optionsSixth.Should().NotBeEquivalentTo(optionsFifth); + } + + [Fact] + public async Task AddHttpClientLogging_DisablesNetScope() + { + const string RequestPath = "https://we.wont.hit.this.dd22anyway.com"; + await using var provider = new ServiceCollection() + .AddFakeLogging() + .AddFakeRedaction() + .AddHttpClient("test") + .AddHttpClientLogging() + .Services + .BlockRemoteCall() + .BuildServiceProvider(); + var options = provider.GetRequiredService>().Get("test"); + var client = provider.GetRequiredService().CreateClient("test"); + using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, new Uri(RequestPath)); + + _ = await client.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + var collector = provider.GetFakeLogCollector(); + var logRecord = collector.GetSnapshot().Single(l => l.Category == "Microsoft.Extensions.Http.Telemetry.Logging.Internal.HttpLoggingHandler"); + + logRecord.Scopes.Should().HaveCount(0); + } + + [Fact] + public async Task AddDefaultHttpClientLogging_DisablesNetScope() + { + const string RequestPath = "https://we.wont.hit.this.dd22anyway.com"; + await using var provider = new ServiceCollection() + .AddFakeLogging() + .AddFakeRedaction() + .AddHttpClient() + .AddDefaultHttpClientLogging() + .BlockRemoteCall() + .BuildServiceProvider(); + var options = provider.GetRequiredService>().Get("test"); + var client = provider.GetRequiredService().CreateClient("test"); + using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, new Uri(RequestPath)); + + _ = await client.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + var collector = provider.GetFakeLogCollector(); + var logRecord = collector.GetSnapshot().Single(l => l.Category == "Microsoft.Extensions.Http.Telemetry.Logging.Internal.HttpLoggingHandler"); + + logRecord.Scopes.Should().HaveCount(0); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpHeadersReaderTest.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpHeadersReaderTest.cs new file mode 100644 index 0000000000..d7f0e60d6e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpHeadersReaderTest.cs @@ -0,0 +1,126 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using FluentAssertions; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Http.Telemetry.Logging.Internal; +using Microsoft.Extensions.Telemetry.Internal; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test; + +public class HttpHeadersReaderTest +{ + [Fact] + public void HttpHeadersReader_WhenAnyArgumentIsNull_Throws() + { + var options = Microsoft.Extensions.Options.Options.Create((LoggingOptions)null!); + var act = () => new HttpHeadersReader(options, Mock.Of()); + act.Should().Throw(); + } + + [Fact] + public void HttpHeadersReader_WhenEmptyHeaders_DoesNothing() + { + using var httpRequest = new HttpRequestMessage(); + using var httpResponse = new HttpResponseMessage(); + var options = Microsoft.Extensions.Options.Options.Create(new LoggingOptions()); + + var headersReader = new HttpHeadersReader(options, Mock.Of()); + var buffer = new List>(); + + headersReader.ReadRequestHeaders(httpRequest, buffer); + buffer.Should().BeEmpty(); + + headersReader.ReadResponseHeaders(httpResponse, buffer); + buffer.Should().BeEmpty(); + } + + [Fact] + public void HttpHeadersReader_WhenHeadersProvided_ReadsThem() + { + const string Redacted = "REDACTED"; + using var httpRequest = new HttpRequestMessage(); + using var httpResponse = new HttpResponseMessage(); + + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor.Setup(r => r.Redact(It.IsAny>(), SimpleClassifications.PrivateData)) + .Returns(Redacted); + mockHeadersRedactor.Setup(r => r.Redact(It.IsAny>(), SimpleClassifications.PublicData)) + .Returns, DataClassification>((x, _) => string.Join(",", x)); + + var options = Microsoft.Extensions.Options.Options.Create(new LoggingOptions + { + RequestHeadersDataClasses = new Dictionary + { + { "Header1", SimpleClassifications.PrivateData }, + { "Header2", SimpleClassifications.PrivateData } + }, + ResponseHeadersDataClasses = new Dictionary + { + { "Header3", SimpleClassifications.PublicData }, + { "Header4", SimpleClassifications.PublicData }, + { "hEaDeR7", SimpleClassifications.PrivateData } + }, + }); + + var headersReader = new HttpHeadersReader(options, mockHeadersRedactor.Object); + var buffer = new List>(); + + headersReader.ReadRequestHeaders(httpRequest, buffer); + buffer.Should().BeEmpty(); + + headersReader.ReadResponseHeaders(httpResponse, buffer); + buffer.Should().BeEmpty(); + + httpRequest.Headers.Add("Header1", "Value.1"); + httpRequest.Headers.Add("Header2", "Value.2"); + httpResponse.Headers.Add("Header3", "Value.3"); + httpResponse.Headers.Add("Header4", "Value.4"); + httpRequest.Headers.Add("Header5", string.Empty); + httpResponse.Headers.Add("Header6", string.Empty); + httpResponse.Headers.Add("Header7", "Value.7"); + + var requestBuffer = new List>(); + var responseBuffer = new List>(); + var expectedRequest = new[] + { + new KeyValuePair("Header1", Redacted), + new KeyValuePair("Header2", Redacted) + }; + var expectedResponse = new[] + { + new KeyValuePair("Header3", "Value.3"), + new KeyValuePair("Header4", "Value.4"), + new KeyValuePair("hEaDeR7", Redacted), + }; + + headersReader.ReadRequestHeaders(httpRequest, requestBuffer); + headersReader.ReadResponseHeaders(httpResponse, responseBuffer); + + requestBuffer.Should().BeEquivalentTo(expectedRequest); + responseBuffer.Should().BeEquivalentTo(expectedResponse); + } + + [Fact] + public void HttpHeadersReader_WhenBufferIsNull_DoesNothing() + { + var options = Microsoft.Extensions.Options.Options.Create(new LoggingOptions()); + var headersReader = new HttpHeadersReader(options, Mock.Of()); + List>? responseBuffer = null; + + using var httpRequest = new HttpRequestMessage(); + using var httpResponse = new HttpResponseMessage(); + + headersReader.ReadResponseHeaders(httpResponse, responseBuffer); + responseBuffer.Should().BeNull(); + + headersReader.ReadRequestHeaders(httpRequest, responseBuffer); + responseBuffer.Should().BeNull(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpLoggingHandlerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpLoggingHandlerTest.cs new file mode 100644 index 0000000000..8035226485 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpLoggingHandlerTest.cs @@ -0,0 +1,1074 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AutoFixture; +using FluentAssertions; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Telemetry.Logging.Internal; +using Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Microsoft.Extensions.Time.Testing; +using Microsoft.Shared.Collections; +using Moq; +using Moq.Protected; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test; + +public class HttpLoggingHandlerTest +{ + private const string TestRequestHeader = "RequestHeader"; + private const string TestResponseHeader = "ResponseHeader"; + private const string TestExpectedRequestHeaderKey = $"{HttpClientLoggingDimensions.RequestHeaderPrefix}{TestRequestHeader}"; + private const string TestExpectedResponseHeaderKey = $"{HttpClientLoggingDimensions.ResponseHeaderPrefix}{TestResponseHeader}"; + + private const string TextPlain = "text/plain"; + + private const string Redacted = "REDACTED"; + + private readonly Fixture _fixture; + + public HttpLoggingHandlerTest() + { + _fixture = new(); + } + + [Fact] + public void HttpLoggingHandler_NullOptions_Throws() + { + var options = Microsoft.Extensions.Options.Options.Create(null!); + var act = () => new HttpLoggingHandler( + NullLogger.Instance, + Mock.Of(), + Empty.Enumerable(), + options); + + act.Should().Throw(); + } + + [Fact] + public async Task SendAsync_NullRequest_ThrowsException() + { + var responseCode = _fixture.Create(); + using var httpResponseMessage = new HttpResponseMessage { StatusCode = responseCode }; + using var client = HttpLoggingHandlerTest.CreateClient(httpResponseMessage); + + var act = async () => + await client.SendAsync(null!, It.IsAny()).ConfigureAwait(false); + + await act.Should().ThrowAsync().ConfigureAwait(false); + } + + [Fact] + public async Task SendAsync_HttpRequestException_ThrowsException() + { + var input = _fixture.Create(); + var exception = new HttpRequestException(); + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new("http://default-uri.com"), + Content = new StringContent(input, Encoding.UTF8, TextPlain) + }; + + using var client = HttpLoggingHandlerTest.CreateClientWithException(exception, isLoggingEnabled: false); + + var act = async () => + await client.SendAsync(httpRequestMessage, It.IsAny()).ConfigureAwait(false); + + await act.Should().ThrowAsync().Where(e => e == exception); + } + + [Fact] + public async Task SendAsync_ReadRequestAsyncThrowsOperationCancelled_ThrowsOperationCancelled() + { + using var cancellationTokenSource = new CancellationTokenSource(); + + var input = _fixture.Create(); + var operationCanceledException = new OperationCanceledException(); + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new("http://default-uri.com"), + Content = new StringContent(input, Encoding.UTF8, TextPlain) + }; + using var httpClient = HttpLoggingHandlerTest.CreateClientWithOperationCanceledException(operationCanceledException); + + var act = async () => + await httpClient.SendAsync(httpRequestMessage, It.IsAny()).ConfigureAwait(false); + + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HttpLoggingHandler_AllOptions_LogsOutgoingRequest() + { + var requestContent = _fixture.Create(); + var responseContent = _fixture.Create(); + var testRequestHeaderValue = _fixture.Create(); + var testResponseHeaderValue = _fixture.Create(); + + var fakeTimeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow); + + var testEnricher = new TestEnricher(); + + var testSharedRequestHeaderKey = $"{HttpClientLoggingDimensions.RequestHeaderPrefix}Header3"; + var testSharedResponseHeaderKey = $"{HttpClientLoggingDimensions.ResponseHeaderPrefix}Header3"; + + var expectedLogRecord = new LogRecord + { + Host = "default-uri.com", + Method = HttpMethod.Post, + Path = "foo/bar", + Duration = 1000, + StatusCode = 200, + ResponseHeaders = new() { new(TestExpectedResponseHeaderKey, Redacted), new(testSharedResponseHeaderKey, Redacted) }, + RequestHeaders = new() { new(TestExpectedRequestHeaderKey, Redacted), new(testSharedRequestHeaderKey, Redacted) }, + RequestBody = requestContent, + ResponseBody = responseContent, + EnrichmentProperties = testEnricher.EnrichmentBag + }; + + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new($"http://{expectedLogRecord.Host}/{expectedLogRecord.Path}"), + Content = new StringContent(requestContent, Encoding.UTF8, TextPlain) + }; + httpRequestMessage.Headers.Add(TestRequestHeader, testRequestHeaderValue); + httpRequestMessage.Headers.Add("Header3", testRequestHeaderValue); + + using var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(responseContent, Encoding.UTF8, TextPlain), + }; + httpResponseMessage.Headers.Add(TestResponseHeader, testResponseHeaderValue); + httpResponseMessage.Headers.Add("Header3", testRequestHeaderValue); + + var options = Microsoft.Extensions.Options.Options.Create(new LoggingOptions + { + ResponseHeadersDataClasses = new Dictionary { { TestResponseHeader, SimpleClassifications.PrivateData }, { "Header3", SimpleClassifications.PrivateData } }, + RequestHeadersDataClasses = new Dictionary { { TestRequestHeader, SimpleClassifications.PrivateData }, { "Header3", SimpleClassifications.PrivateData } }, + ResponseBodyContentTypes = new HashSet { TextPlain }, + RequestBodyContentTypes = new HashSet { TextPlain }, + BodySizeLimit = 32000, + BodyReadTimeout = TimeSpan.FromMinutes(5), + RequestPathLoggingMode = OutgoingPathLoggingMode.Structured, + LogRequestStart = false, + LogBody = true, + RouteParameterDataClasses = { { "userId", SimpleClassifications.PrivateData } }, + }); + + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor.Setup(r => r.Redact(It.IsAny>(), It.IsAny())) + .Returns(Redacted); + var headersReader = new HttpHeadersReader(options, mockHeadersRedactor.Object); + + var fakeLogger = new FakeLogger(new FakeLogCollector(Microsoft.Extensions.Options.Options.Create(new FakeLogCollectorOptions()))); + + using var handler = new HttpLoggingHandler( + fakeLogger, + new HttpRequestReader( + options, + GetHttpRouteFormatter(), + headersReader, RequestMetadataContext), + new List { testEnricher }, + options) + { + InnerHandler = new TestingHandlerStub((_, _) => + { + fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(1000)); + return Task.FromResult(httpResponseMessage); + }) + }; + handler.TimeProvider = fakeTimeProvider; + + using var client = new System.Net.Http.HttpClient(handler); + await client.SendAsync(httpRequestMessage, It.IsAny()).ConfigureAwait(false); + + var logRecords = fakeLogger.Collector.GetSnapshot(); + logRecords.Count.Should().Be(1); + + var logRecord = logRecords[0].GetStructuredState(); + logRecord.Contains(HttpClientLoggingDimensions.Host, expectedLogRecord.Host); + logRecord.Contains(HttpClientLoggingDimensions.Method, expectedLogRecord.Method.ToString()); + logRecord.Contains(HttpClientLoggingDimensions.Path, TelemetryConstants.Redacted); + logRecord.Contains(HttpClientLoggingDimensions.Duration, expectedLogRecord.Duration.ToString(CultureInfo.InvariantCulture)); + logRecord.Contains(HttpClientLoggingDimensions.StatusCode, expectedLogRecord.StatusCode.Value.ToString(CultureInfo.InvariantCulture)); + logRecord.Contains(HttpClientLoggingDimensions.RequestBody, expectedLogRecord.RequestBody); + logRecord.Contains(HttpClientLoggingDimensions.ResponseBody, expectedLogRecord.ResponseBody); + logRecord.Contains(TestExpectedRequestHeaderKey, expectedLogRecord.RequestHeaders[0].Value); + logRecord.Contains(TestExpectedResponseHeaderKey, expectedLogRecord.ResponseHeaders[0].Value); + logRecord.Contains(testSharedResponseHeaderKey, expectedLogRecord.ResponseHeaders[1].Value); + logRecord.Contains(testSharedRequestHeaderKey, expectedLogRecord.RequestHeaders[1].Value); + logRecord.Contains(testEnricher.KvpRequest.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpRequest.Key)); + } + + [Fact] + public async Task HttpLoggingHandler_AllOptionsWithLogRequestStart_LogsOutgoingRequestWithTwoRecords() + { + var requestContent = _fixture.Create(); + var responseContent = _fixture.Create(); + var requestHeaderValue = _fixture.Create(); + var responseHeaderValue = _fixture.Create(); + var fakeTimeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow); + var testEnricher = new TestEnricher(); + + var expectedLogRecord = new LogRecord + { + Host = "default-uri.com", + Method = HttpMethod.Post, + Path = "foo/bar", + Duration = 1000, + StatusCode = 200, + ResponseHeaders = new() { new(TestResponseHeader, Redacted) }, + RequestHeaders = new() { new(TestRequestHeader, Redacted) }, + RequestBody = requestContent, + ResponseBody = responseContent, + EnrichmentProperties = testEnricher.EnrichmentBag + }; + + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new($"http://{expectedLogRecord.Host}/{expectedLogRecord.Path}"), + Content = new StringContent(requestContent, Encoding.UTF8, TextPlain) + }; + httpRequestMessage.Headers.Add(TestRequestHeader, requestHeaderValue); + + using var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(responseContent, Encoding.UTF8, TextPlain), + }; + httpResponseMessage.Headers.Add(TestResponseHeader, responseHeaderValue); + + var options = Microsoft.Extensions.Options.Options.Create(new LoggingOptions + { + ResponseHeadersDataClasses = new Dictionary { { TestResponseHeader, SimpleClassifications.PrivateData } }, + RequestHeadersDataClasses = new Dictionary { { TestRequestHeader, SimpleClassifications.PrivateData } }, + ResponseBodyContentTypes = new HashSet { TextPlain }, + RequestBodyContentTypes = new HashSet { TextPlain }, + BodySizeLimit = 32000, + BodyReadTimeout = TimeSpan.FromMinutes(5), + RequestPathLoggingMode = OutgoingPathLoggingMode.Structured, + LogRequestStart = true, + LogBody = true, + RouteParameterDataClasses = { { "userId", SimpleClassifications.PrivateData } }, + }); + + var fakeLogger = new FakeLogger( + new FakeLogCollector( + Microsoft.Extensions.Options.Options.Create( + new FakeLogCollectorOptions()))); + + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor + .Setup(r => r.Redact(It.IsAny>(), It.IsAny())) + .Returns(Redacted); + + var headersReader = new HttpHeadersReader(options, mockHeadersRedactor.Object); + + using var handler = new HttpLoggingHandler( + fakeLogger, + new HttpRequestReader( + options, + GetHttpRouteFormatter(), + headersReader, RequestMetadataContext), + new List { testEnricher }, + options) + { + InnerHandler = new TestingHandlerStub((_, _) => + { + fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(1000)); + return Task.FromResult(httpResponseMessage); + }) + }; + handler.TimeProvider = fakeTimeProvider; + + using var client = new System.Net.Http.HttpClient(handler); + await client.SendAsync(httpRequestMessage, It.IsAny()).ConfigureAwait(false); + + var logRecords = fakeLogger.Collector.GetSnapshot(); + logRecords.Count.Should().Be(2); + + var logRecordRequest = logRecords[0].GetStructuredState(); + logRecordRequest.Contains(HttpClientLoggingDimensions.Host, expectedLogRecord.Host); + logRecordRequest.Contains(HttpClientLoggingDimensions.Method, expectedLogRecord.Method.ToString()); + logRecordRequest.Contains(HttpClientLoggingDimensions.Path, TelemetryConstants.Redacted); + logRecordRequest.Contains(HttpClientLoggingDimensions.RequestBody, expectedLogRecord.RequestBody); + logRecordRequest.NotContains(HttpClientLoggingDimensions.StatusCode); + logRecordRequest.Contains(TestExpectedRequestHeaderKey, expectedLogRecord.RequestHeaders.FirstOrDefault().Value); + logRecordRequest.NotContains(testEnricher.KvpRequest.Key); + + var logRecordFull = logRecords[1].GetStructuredState(); + logRecordFull.Contains(HttpClientLoggingDimensions.Host, expectedLogRecord.Host); + logRecordFull.Contains(HttpClientLoggingDimensions.Method, expectedLogRecord.Method.ToString()); + logRecordFull.Contains(HttpClientLoggingDimensions.Path, TelemetryConstants.Redacted); + logRecordFull.Contains(HttpClientLoggingDimensions.Duration, expectedLogRecord.Duration.ToString(CultureInfo.InvariantCulture)); + logRecordFull.Contains(HttpClientLoggingDimensions.StatusCode, expectedLogRecord.StatusCode.Value.ToString(CultureInfo.InvariantCulture)); + logRecordFull.Contains(HttpClientLoggingDimensions.RequestBody, expectedLogRecord.RequestBody); + logRecordFull.Contains(HttpClientLoggingDimensions.ResponseBody, expectedLogRecord.ResponseBody); + logRecordFull.Contains(TestExpectedRequestHeaderKey, expectedLogRecord.RequestHeaders.FirstOrDefault().Value); + logRecordFull.Contains(TestExpectedResponseHeaderKey, expectedLogRecord.ResponseHeaders.FirstOrDefault().Value); + logRecordFull.Contains(testEnricher.KvpRequest.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpRequest.Key)); + logRecordFull.Contains(testEnricher.KvpResponse.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpResponse.Key)); + } + + [Fact] + public async Task HttpLoggingHandler_AllOptionsSendAsyncFailed_LogsRequestInformation() + { + var requestContent = _fixture.Create(); + var responseContent = _fixture.Create(); + var requestHeaderValue = _fixture.Create(); + var responseHeaderValue = _fixture.Create(); + var fakeTimeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow); + var testEnricher = new TestEnricher(); + + var expectedLogRecord = new LogRecord + { + Host = "default-uri.com", + Method = HttpMethod.Post, + Path = "foo/bar", + Duration = 1000, + StatusCode = 200, + ResponseHeaders = new() { new(TestResponseHeader, Redacted) }, + RequestHeaders = new() { new(TestRequestHeader, Redacted) }, + RequestBody = requestContent, + ResponseBody = responseContent, + EnrichmentProperties = testEnricher.EnrichmentBag + }; + + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new($"http://{expectedLogRecord.Host}/{expectedLogRecord.Path}"), + Content = new StringContent(requestContent, Encoding.UTF8, TextPlain) + }; + httpRequestMessage.Headers.Add(TestRequestHeader, requestHeaderValue); + + using var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(responseContent, Encoding.UTF8, TextPlain), + }; + httpResponseMessage.Headers.Add(TestResponseHeader, responseHeaderValue); + + var options = Microsoft.Extensions.Options.Options.Create(new LoggingOptions + { + ResponseHeadersDataClasses = new Dictionary { { TestResponseHeader, SimpleClassifications.PrivateData } }, + RequestHeadersDataClasses = new Dictionary { { TestRequestHeader, SimpleClassifications.PrivateData } }, + ResponseBodyContentTypes = new HashSet { TextPlain }, + RequestBodyContentTypes = new HashSet { TextPlain }, + BodySizeLimit = 32000, + BodyReadTimeout = TimeSpan.FromMinutes(5), + RequestPathLoggingMode = OutgoingPathLoggingMode.Structured, + LogRequestStart = false, + LogBody = true, + RouteParameterDataClasses = { { "userId", SimpleClassifications.PrivateData } }, + }); + + var fakeLogger = new FakeLogger(new FakeLogCollector(Microsoft.Extensions.Options.Options.Create(new FakeLogCollectorOptions()))); + + var exception = new OperationCanceledException(); + + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor.Setup(r => r.Redact(It.IsAny>(), It.IsAny())) + .Returns(Redacted); + var headersReader = new HttpHeadersReader(options, mockHeadersRedactor.Object); + + using var handler = new HttpLoggingHandler( + fakeLogger, + new HttpRequestReader( + options, + GetHttpRouteFormatter(), + headersReader, RequestMetadataContext), + new List { testEnricher }, + options) + { + InnerHandler = new TestingHandlerStub((_, _) => + { + fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(expectedLogRecord.Duration)); + throw exception; + }), + TimeProvider = fakeTimeProvider + }; + + using var client = new System.Net.Http.HttpClient(handler); + var act = async () => await client.SendAsync(httpRequestMessage, It.IsAny()).ConfigureAwait(false); + await act.Should().ThrowAsync().ConfigureAwait(false); + + var logRecords = fakeLogger.Collector.GetSnapshot(); + logRecords.Count.Should().Be(1); + logRecords[0].Message.Should().BeEmpty(); + logRecords[0].Exception.Should().Be(exception); + + var logRecord = logRecords[0].GetStructuredState(); + logRecord.Contains(HttpClientLoggingDimensions.Host, expectedLogRecord.Host); + logRecord.Contains(HttpClientLoggingDimensions.Method, expectedLogRecord.Method.ToString()); + logRecord.Contains(HttpClientLoggingDimensions.Path, TelemetryConstants.Redacted); + logRecord.Contains(HttpClientLoggingDimensions.RequestBody, expectedLogRecord.RequestBody); + logRecord.NotContains(HttpClientLoggingDimensions.ResponseBody); + logRecord.NotContains(HttpClientLoggingDimensions.StatusCode); + logRecord.Contains(TestExpectedRequestHeaderKey, expectedLogRecord.RequestHeaders.FirstOrDefault().Value); + logRecord.Should().NotContain(kvp => kvp.Key.StartsWith(HttpClientLoggingDimensions.ResponseHeaderPrefix)); + logRecord.Contains(testEnricher.KvpRequest.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpRequest.Key)); + logRecord.NotContains(testEnricher.KvpResponse.Key); + logRecord.Contains(HttpClientLoggingDimensions.Duration, expectedLogRecord.Duration.ToString(CultureInfo.InvariantCulture)); + } + + [Fact(Skip = "Flaky test, see https://github.com/dotnet/r9/issues/372")] + public async Task HttpLoggingHandler_ReadResponseThrows_LogsException() + { + var requestContent = _fixture.Create(); + var responseContent = _fixture.Create(); + var requestHeaderValue = _fixture.Create(); + var responseHeaderValue = _fixture.Create(); + var fakeTimeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow); + var testEnricher = new TestEnricher(); + + var expectedLogRecord = new LogRecord + { + Host = "default-uri.com", + Method = HttpMethod.Post, + Path = "foo/bar", + Duration = 1000, + StatusCode = 200, + ResponseHeaders = new() { new(TestResponseHeader, Redacted) }, + RequestHeaders = new() { new(TestRequestHeader, Redacted) }, + RequestBody = requestContent, + ResponseBody = responseContent, + EnrichmentProperties = testEnricher.EnrichmentBag + }; + + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new($"http://{expectedLogRecord.Host}/{expectedLogRecord.Path}"), + Content = new StringContent(requestContent, Encoding.UTF8, TextPlain) + }; + httpRequestMessage.Headers.Add(TestRequestHeader, requestHeaderValue); + + using var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(responseContent, Encoding.UTF8, TextPlain), + }; + httpResponseMessage.Headers.Add(TestResponseHeader, responseHeaderValue); + + var options = Microsoft.Extensions.Options.Options.Create(new LoggingOptions + { + ResponseHeadersDataClasses = new Dictionary { { TestResponseHeader, SimpleClassifications.PrivateData } }, + RequestHeadersDataClasses = new Dictionary { { TestRequestHeader, SimpleClassifications.PrivateData } }, + ResponseBodyContentTypes = new HashSet { TextPlain }, + RequestBodyContentTypes = new HashSet { TextPlain }, + BodySizeLimit = 32000, + BodyReadTimeout = TimeSpan.FromMinutes(5), + RequestPathLoggingMode = OutgoingPathLoggingMode.Structured, + LogRequestStart = false, + LogBody = true, + RouteParameterDataClasses = { { "userId", SimpleClassifications.PrivateData } }, + }); + + var fakeLogger = new FakeLogger( + new FakeLogCollector( + Microsoft.Extensions.Options.Options.Create(new FakeLogCollectorOptions()))); + + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor + .Setup(r => r.Redact(It.IsAny>(), It.IsAny())) + .Returns(Redacted); + var headersReader = new HttpHeadersReader(options, mockHeadersRedactor.Object); + + var exception = new InvalidOperationException("test"); + + var actualRequestReader = new HttpRequestReader(options, GetHttpRouteFormatter(), headersReader, RequestMetadataContext); + var mockedRequestReader = new Mock(); + mockedRequestReader + .Setup(m => + m.ReadRequestAsync(// so this method is not mocked + It.IsAny(), + It.IsAny(), + It.IsAny>>(), + It.IsAny())) + .Returns((LogRecord a, HttpRequestMessage b, List> c, CancellationToken d) => + actualRequestReader.ReadRequestAsync(a, b, c, d)); + mockedRequestReader + .Setup(m => + m.ReadResponseAsync(// but this method is setup to throw an exception + It.IsAny(), + It.IsAny(), + It.IsAny>>(), + It.IsAny())) + .ThrowsAsync(exception); + + using var handler = new HttpLoggingHandler( + fakeLogger, + mockedRequestReader.Object, + new List { testEnricher }, + options) + { + InnerHandler = new TestingHandlerStub((_, _) => + { + fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(1000)); + return Task.FromResult(httpResponseMessage); + }) + }; + + handler.TimeProvider = fakeTimeProvider; + + using var client = new System.Net.Http.HttpClient(handler); + var act = async () => await client + .SendAsync(httpRequestMessage, It.IsAny()) + .ConfigureAwait(false); + await act.Should().ThrowAsync().ConfigureAwait(false); + + var logRecords = fakeLogger.Collector.GetSnapshot(); + logRecords.Count.Should().Be(1); + logRecords[0].Exception.Should().Be(exception); + + var logRecord = logRecords[0].GetStructuredState(); + logRecord.Contains(HttpClientLoggingDimensions.Host, expectedLogRecord.Host); + logRecord.Contains(HttpClientLoggingDimensions.Method, expectedLogRecord.Method.ToString()); + logRecord.Contains(HttpClientLoggingDimensions.Path, TelemetryConstants.Redacted); + logRecord.Contains(HttpClientLoggingDimensions.RequestBody, expectedLogRecord.RequestBody); + logRecord.NotContains(HttpClientLoggingDimensions.ResponseBody); + logRecord.NotContains(HttpClientLoggingDimensions.StatusCode); + logRecord.Contains(TestExpectedRequestHeaderKey, expectedLogRecord.RequestHeaders.FirstOrDefault().Value); + logRecord.Should().NotContain(kvp => kvp.Key.StartsWith(HttpClientLoggingDimensions.ResponseHeaderPrefix)); + logRecord.Contains(testEnricher.KvpRequest.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpRequest.Key)); + logRecord.Contains(testEnricher.KvpResponse.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpResponse.Key)); + logRecord.Contains(HttpClientLoggingDimensions.Duration, expectedLogRecord.Duration.ToString(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task HttpLoggingHandler_AllOptionsTransferEncodingIsNotChunked_LogsOutgoingRequest() + { + var requestContent = _fixture.Create(); + var responseContent = _fixture.Create(); + var requestHeaderValue = _fixture.Create(); + var responseHeaderValue = _fixture.Create(); + var fakeTimeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow); + var testEnricher = new TestEnricher(); + + var expectedLogRecord = new LogRecord + { + Host = "default-uri.com", + Method = HttpMethod.Post, + Path = "foo/bar", + Duration = 1000, + StatusCode = 200, + ResponseHeaders = new() { new(TestResponseHeader, Redacted) }, + RequestHeaders = new() { new(TestRequestHeader, Redacted) }, + RequestBody = requestContent, + ResponseBody = responseContent, + EnrichmentProperties = testEnricher.EnrichmentBag, + }; + + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new($"http://{expectedLogRecord.Host}/{expectedLogRecord.Path}"), + Content = new StringContent(requestContent, Encoding.UTF8, TextPlain) + }; + httpRequestMessage.Headers.Add(TestRequestHeader, requestHeaderValue); + + using var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(responseContent, Encoding.UTF8, TextPlain), + }; + httpResponseMessage.Headers.Add(TestResponseHeader, responseHeaderValue); + httpResponseMessage.Headers.TransferEncoding.Add(new("compress")); + + var options = Microsoft.Extensions.Options.Options.Create(new LoggingOptions + { + ResponseHeadersDataClasses = new Dictionary { { TestResponseHeader, SimpleClassifications.PrivateData } }, + RequestHeadersDataClasses = new Dictionary { { TestRequestHeader, SimpleClassifications.PrivateData } }, + ResponseBodyContentTypes = new HashSet { TextPlain }, + RequestBodyContentTypes = new HashSet { TextPlain }, + BodySizeLimit = 32000, + BodyReadTimeout = TimeSpan.FromMinutes(5), + RequestPathLoggingMode = OutgoingPathLoggingMode.Structured, + LogRequestStart = false, + LogBody = true, + RouteParameterDataClasses = { { "userId", SimpleClassifications.PrivateData } }, + }); + + var fakeLogger = new FakeLogger(new FakeLogCollector(Microsoft.Extensions.Options.Options.Create(new FakeLogCollectorOptions()))); + + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor.Setup(r => r.Redact(It.IsAny>(), It.IsAny())) + .Returns(Redacted); + var headersReader = new HttpHeadersReader(options, mockHeadersRedactor.Object); + + using var handler = new HttpLoggingHandler( + fakeLogger, + new HttpRequestReader( + options, + GetHttpRouteFormatter(), + headersReader, RequestMetadataContext), + new List { testEnricher }, + options) + { + InnerHandler = new TestingHandlerStub((_, _) => + { + fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(1000)); + return Task.FromResult(httpResponseMessage); + }) + }; + handler.TimeProvider = fakeTimeProvider; + + using var client = new System.Net.Http.HttpClient(handler); + await client.SendAsync(httpRequestMessage, It.IsAny()).ConfigureAwait(false); + + var logRecords = fakeLogger.Collector.GetSnapshot(); + logRecords.Count.Should().Be(1); + + var logRecord = logRecords[0].GetStructuredState(); + logRecord.Contains(HttpClientLoggingDimensions.Host, expectedLogRecord.Host); + logRecord.Contains(HttpClientLoggingDimensions.Method, expectedLogRecord.Method.ToString()); + logRecord.Contains(HttpClientLoggingDimensions.Path, TelemetryConstants.Redacted); + logRecord.Contains(HttpClientLoggingDimensions.Duration, expectedLogRecord.Duration.ToString(CultureInfo.InvariantCulture)); + logRecord.Contains(HttpClientLoggingDimensions.StatusCode, expectedLogRecord.StatusCode.Value.ToString(CultureInfo.InvariantCulture)); + logRecord.Contains(HttpClientLoggingDimensions.RequestBody, expectedLogRecord.RequestBody); + logRecord.Contains(HttpClientLoggingDimensions.ResponseBody, expectedLogRecord.ResponseBody); + logRecord.Contains(TestExpectedRequestHeaderKey, expectedLogRecord.RequestHeaders.FirstOrDefault().Value); + logRecord.Contains(TestExpectedResponseHeaderKey, expectedLogRecord.ResponseHeaders.FirstOrDefault().Value); + logRecord.Contains(testEnricher.KvpRequest.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpRequest.Key)); + } + + [Fact] + public async Task HttpLoggingHandler_WithEnrichers_CallsEnrichMethodExactlyOnce() + { + var enricher1 = new Mock(); + var enricher2 = new Mock(); + var options = Microsoft.Extensions.Options.Options.Create(new LoggingOptions()); + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor.Setup(r => r.Redact(It.IsAny>(), It.IsAny())) + .Returns(Redacted); + var fakeLogger = new FakeLogger(); + using var handler = new HttpLoggingHandler( + fakeLogger, + new HttpRequestReader( + options, + GetHttpRouteFormatter(), + new HttpHeadersReader(options, mockHeadersRedactor.Object), RequestMetadataContext), + new List { enricher1.Object, enricher2.Object }, + options) + { + InnerHandler = new TestingHandlerStub((_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK))) + }; + + using var client = new System.Net.Http.HttpClient(handler); + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new($"http://default-uri.com/foo/bar"), + Content = new StringContent(_fixture.Create(), Encoding.UTF8, TextPlain) + }; + + await client.SendAsync(httpRequestMessage, It.IsAny()).ConfigureAwait(false); + + var logRecords = fakeLogger.Collector.GetSnapshot(); + logRecords.Count.Should().Be(1); + + enricher1.Verify(e => e.Enrich(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + enricher2.Verify(e => e.Enrich(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + } + + [Fact] + public async Task HttpLoggingHandler_WithEnrichersAndLogRequestStart_CallsEnrichMethodExactlyOnce() + { + var enricher1 = new Mock(); + var enricher2 = new Mock(); + var options = Microsoft.Extensions.Options.Options.Create(new LoggingOptions + { + LogRequestStart = true + }); + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor.Setup(r => r.Redact(It.IsAny>(), It.IsAny())) + .Returns(Redacted); + var fakeLogger = new FakeLogger(); + using var handler = new HttpLoggingHandler( + fakeLogger, + new HttpRequestReader( + options, + GetHttpRouteFormatter(), + new HttpHeadersReader(options, mockHeadersRedactor.Object), RequestMetadataContext), + new List { enricher1.Object, enricher2.Object }, + options) + { + InnerHandler = new TestingHandlerStub((_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK))) + }; + + using var client = new System.Net.Http.HttpClient(handler); + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new($"http://default-uri.com/foo/bar"), + Content = new StringContent(_fixture.Create(), Encoding.UTF8, TextPlain) + }; + + await client.SendAsync(httpRequestMessage, It.IsAny()).ConfigureAwait(false); + + var logRecords = fakeLogger.Collector.GetSnapshot(); + logRecords.Count.Should().Be(2); + + enricher1.Verify(e => e.Enrich(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + enricher2.Verify(e => e.Enrich(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + } + + [Fact] + public async Task HttpLoggingHandler_WithEnrichers_OneEnricherThrows_LogsEnrichmentErrorAndRequest() + { + var exception = new ArgumentNullException(); + var enricher1 = new Mock(); + enricher1 + .Setup(e => e.Enrich(It.IsAny(), It.IsAny(), It.IsAny())) + .Throws(exception); + + var enricher2 = new Mock(); + var fakeLogger = new FakeLogger(); + var options = Microsoft.Extensions.Options.Options.Create(new LoggingOptions()); + var headersReader = new HttpHeadersReader(options, new Mock().Object); + var requestReader = new HttpRequestReader(options, GetHttpRouteFormatter(), headersReader, RequestMetadataContext); + var enrichers = new List { enricher1.Object, enricher2.Object }; + + using var handler = new HttpLoggingHandler(fakeLogger, requestReader, enrichers, options) + { + InnerHandler = new TestingHandlerStub( + (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK))) + }; + + using var client = new System.Net.Http.HttpClient(handler); + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new($"http://default-uri.com/foo/bar"), + Content = new StringContent(_fixture.Create(), Encoding.UTF8, TextPlain) + }; + + await client.SendAsync(httpRequestMessage, It.IsAny()).ConfigureAwait(false); + + var logRecords = fakeLogger.Collector.GetSnapshot(); + logRecords.Count.Should().Be(2); + + Assert.Equal(nameof(Log.EnrichmentError), logRecords[0].Id.Name); + Assert.Equal(exception, logRecords[0].Exception); + + Assert.Equal(nameof(Log.OutgoingRequest), logRecords[1].Id.Name); + + enricher1.Verify(e => e.Enrich(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + enricher2.Verify(e => e.Enrich(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + } + + [Fact] + public async Task HttpLoggingHandler_WithEnrichers_SendAsyncAndOneEnricher_LogsEnrichmentErrorAndRequestError() + { + var enrichmentException = new ArgumentNullException(); + var enricher1 = new Mock(); + enricher1 + .Setup(e => e.Enrich(It.IsAny(), It.IsAny(), It.IsAny())) + .Throws(enrichmentException) + .Verifiable(); + + var enricher2 = new Mock(); + var fakeLogger = new FakeLogger(); + + var sendAsyncException = new OperationCanceledException(); + using var handler = new HttpLoggingHandler( + fakeLogger, + new Mock().Object, + new List { enricher1.Object, enricher2.Object }, + Microsoft.Extensions.Options.Options.Create(new LoggingOptions())) + { + InnerHandler = new TestingHandlerStub((_, _) => throw sendAsyncException) + }; + + using var client = new System.Net.Http.HttpClient(handler); + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new($"http://default-uri.com/foo/bar"), + Content = new StringContent(_fixture.Create(), Encoding.UTF8, TextPlain) + }; + + var act = () => client.SendAsync(httpRequestMessage, It.IsAny()); + await act.Should().ThrowAsync(); + + var logRecords = fakeLogger.Collector.GetSnapshot(); + logRecords.Count.Should().Be(2); + + Assert.Equal(nameof(Log.EnrichmentError), logRecords[0].Id.Name); + Assert.Equal(enrichmentException, logRecords[0].Exception); + + Assert.Equal(nameof(Log.OutgoingRequestError), logRecords[1].Id.Name); + Assert.Equal(sendAsyncException, logRecords[1].Exception); + + enricher1.Verify(e => e.Enrich(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + enricher2.Verify(e => e.Enrich(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + } + + [Fact] + public async Task HttpLoggingHandler_AllOptionsTransferEncodingChunked_LogsOutgoingRequest() + { + var requestInput = _fixture.Create(); + var responseInput = _fixture.Create(); + var requestHeaderValue = _fixture.Create(); + var responseHeaderValue = _fixture.Create(); + var fakeTimeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow); + var testEnricher = new TestEnricher(); + + var expectedLogRecord = new LogRecord + { + Host = "default-uri.com", + Method = HttpMethod.Post, + Path = "foo/bar", + Duration = 1000, + StatusCode = 200, + ResponseHeaders = new() { new(TestExpectedResponseHeaderKey, Redacted) }, + RequestHeaders = new() { new(TestExpectedRequestHeaderKey, Redacted) }, + RequestBody = requestInput, + ResponseBody = responseInput, + EnrichmentProperties = testEnricher.EnrichmentBag + }; + + using var requestContent = new StreamContent(new NotSeekableStream(new(Encoding.UTF8.GetBytes(requestInput)))); + requestContent.Headers.Add("Content-Type", TextPlain); + + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new($"http://{expectedLogRecord.Host}/{expectedLogRecord.Path}"), + Content = requestContent, + }; + httpRequestMessage.Headers.Add(TestRequestHeader, requestHeaderValue); + + using var responseContent = new StreamContent(new NotSeekableStream(new(Encoding.UTF8.GetBytes(responseInput)))); + responseContent.Headers.Add("Content-Type", TextPlain); + + using var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = responseContent, + }; + httpResponseMessage.Headers.Add(TestResponseHeader, responseHeaderValue); + httpResponseMessage.Headers.TransferEncoding.Add(new("chunked")); + + var options = Microsoft.Extensions.Options.Options.Create(new LoggingOptions + { + ResponseHeadersDataClasses = new Dictionary { { TestResponseHeader, SimpleClassifications.PrivateData } }, + RequestHeadersDataClasses = new Dictionary { { TestRequestHeader, SimpleClassifications.PrivateData } }, + ResponseBodyContentTypes = new HashSet { TextPlain }, + RequestBodyContentTypes = new HashSet { TextPlain }, + BodySizeLimit = 32000, + BodyReadTimeout = TimeSpan.FromMinutes(5), + RequestPathLoggingMode = OutgoingPathLoggingMode.Structured, + LogRequestStart = false, + LogBody = true, + RouteParameterDataClasses = { { "userId", SimpleClassifications.PrivateData } }, + }); + + var fakeLogger = new FakeLogger(new FakeLogCollector(Microsoft.Extensions.Options.Options.Create(new FakeLogCollectorOptions()))); + + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor.Setup(r => r.Redact(It.IsAny>(), It.IsAny())) + .Returns(Redacted); + var headersReader = new HttpHeadersReader(options, mockHeadersRedactor.Object); + + using var handler = new HttpLoggingHandler( + fakeLogger, + new HttpRequestReader( + options, + GetHttpRouteFormatter(), + headersReader, RequestMetadataContext), + new List { testEnricher }, + options) + { + InnerHandler = new TestingHandlerStub((_, _) => + { + fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(1000)); + return Task.FromResult(httpResponseMessage); + }) + }; + handler.TimeProvider = fakeTimeProvider; + + using var client = new System.Net.Http.HttpClient(handler); + await client.SendAsync(httpRequestMessage, It.IsAny()).ConfigureAwait(false); + + var logRecords = fakeLogger.Collector.GetSnapshot(); + logRecords.Count.Should().Be(1); + + var logRecord = logRecords[0].GetStructuredState(); + logRecord.Contains(HttpClientLoggingDimensions.Host, expectedLogRecord.Host); + logRecord.Contains(HttpClientLoggingDimensions.Method, expectedLogRecord.Method.ToString()); + logRecord.Contains(HttpClientLoggingDimensions.Path, TelemetryConstants.Redacted); + logRecord.Contains(HttpClientLoggingDimensions.Duration, expectedLogRecord.Duration.ToString(CultureInfo.InvariantCulture)); + logRecord.Contains(HttpClientLoggingDimensions.StatusCode, expectedLogRecord.StatusCode.Value.ToString(CultureInfo.InvariantCulture)); + logRecord.Contains(HttpClientLoggingDimensions.RequestBody, expectedLogRecord.RequestBody); + logRecord.Contains(HttpClientLoggingDimensions.ResponseBody, expectedLogRecord.ResponseBody); + logRecord.Contains(TestExpectedRequestHeaderKey, expectedLogRecord.RequestHeaders.FirstOrDefault().Value); + logRecord.Contains(TestExpectedResponseHeaderKey, expectedLogRecord.ResponseHeaders.FirstOrDefault().Value); + logRecord.Contains(testEnricher.KvpRequest.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpRequest.Key)); + } + + [Theory] + [InlineData(399, LogLevel.Information)] + [InlineData(400, LogLevel.Error)] + [InlineData(499, LogLevel.Error)] + [InlineData(500, LogLevel.Error)] + [InlineData(599, LogLevel.Error)] + [InlineData(600, LogLevel.Information)] + public async Task HttpLoggingHandler_OnDifferentHttpStatusCodes_LogsOutgoingRequestWithAppropriateLogLevel( + int httpStatusCode, LogLevel expectedLogLevel) + { + var fakeLogger = new FakeLogger(new FakeLogCollector()); + var options = Microsoft.Extensions.Options.Options.Create(new LoggingOptions()); + var headersReader = new HttpHeadersReader(options, new Mock().Object); + var requestReader = new HttpRequestReader( + options, GetHttpRouteFormatter(), headersReader, RequestMetadataContext); + + using var handler = new HttpLoggingHandler( + fakeLogger, requestReader, new List(), options) + { + InnerHandler = new TestingHandlerStub((_, _) => + Task.FromResult(new HttpResponseMessage((HttpStatusCode)httpStatusCode))) + }; + + using var client = new System.Net.Http.HttpClient(handler); + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new($"http://default-uri.com/foo/bar"), + Content = new StringContent("request_content", Encoding.UTF8, TextPlain) + }; + await client.SendAsync(httpRequestMessage, It.IsAny()).ConfigureAwait(false); + + var logRecord = fakeLogger.Collector.GetSnapshot().Single(); + Assert.Equal(expectedLogLevel, logRecord.Level); + } + + private static System.Net.Http.HttpClient CreateClientWithException( + Exception exception, + bool isLoggingEnabled = true) + { + var loggerMock = new Mock>(MockBehavior.Strict); + loggerMock.Setup(m => m.IsEnabled(It.IsAny())).Returns(isLoggingEnabled); + var mockedLogger = new MockedLogger(loggerMock); + + var mockHandler = new Mock(); + mockHandler.Protected().Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()).Throws(exception); + + var options = Microsoft.Extensions.Options.Options.Create(new LoggingOptions()); + + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor.Setup(r => r.Redact(It.IsAny>(), It.IsAny())) + .Returns(Redacted); + var headersReader = new HttpHeadersReader(options, mockHeadersRedactor.Object); + +#pragma warning disable CA2000 // Dispose objects before losing scope - no, it is required for the HttpClient to work properly + var handler = new HttpLoggingHandler( + mockedLogger, + new HttpRequestReader( + options, + GetHttpRouteFormatter(), headersReader, RequestMetadataContext), + Enumerable.Empty(), + options) + { + InnerHandler = mockHandler.Object + }; +#pragma warning restore CA2000 // Dispose objects before losing scope + + var client = new System.Net.Http.HttpClient(handler); + return client; + } + + private static System.Net.Http.HttpClient CreateClientWithOperationCanceledException( + Exception exception, + bool isLoggingEnabled = true) + { + var requestReaderMock = new Mock(MockBehavior.Strict); + requestReaderMock.Setup(e => + e.ReadRequestAsync(It.IsAny(), + It.IsAny(), +It.IsAny>>(), + It.IsAny())).Throws(exception); + var mockedRequestReader = new MockedRequestReader(requestReaderMock); + + var loggerMock = new Mock>(); + loggerMock.Setup(m => m.IsEnabled(It.IsAny())).Returns(isLoggingEnabled); + var mockedLogger = new MockedLogger(loggerMock); + + var options = Microsoft.Extensions.Options.Options.Create(new LoggingOptions()); + + using var handler = new HttpLoggingHandler( + mockedLogger, + mockedRequestReader, + Enumerable.Empty(), + options); + + var client = new System.Net.Http.HttpClient(handler); + return client; + } + + private static System.Net.Http.HttpClient CreateClient( + HttpResponseMessage httpResponseMessage, + LogLevel logLevel = LogLevel.Information, + bool isLoggingEnabled = true, + Action? setupOptions = null) + { + var options = new LoggingOptions + { + BodyReadTimeout = TimeSpan.FromMinutes(5) + }; + + var loggerMock = new Mock>(MockBehavior.Strict); + loggerMock.Setup(m => m.IsEnabled(logLevel)).Returns(isLoggingEnabled); + var logger = new MockedLogger(loggerMock); + + setupOptions?.Invoke(options); + + using var handler = new HttpLoggingHandler( + logger, Mock.Of(), + Empty.Enumerable(), + Microsoft.Extensions.Options.Options.Create(options)) + { + InnerHandler = new TestingHandlerStub((_, _) => Task.FromResult(httpResponseMessage)) + }; + + var client = new System.Net.Http.HttpClient(handler); + return client; + } + + private static IHttpRouteFormatter GetHttpRouteFormatter() + { + var builder = new ServiceCollection() + .AddFakeRedaction() + .AddHttpRouteProcessor() + .BuildServiceProvider(); + + return builder.GetService()!; + } + + private static IOutgoingRequestContext RequestMetadataContext => new Mock().Object; +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpRequestBodyReaderTest.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpRequestBodyReaderTest.cs new file mode 100644 index 0000000000..07d3f13923 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpRequestBodyReaderTest.cs @@ -0,0 +1,263 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AutoFixture; +using FluentAssertions; +using Microsoft.Extensions.Http.Telemetry.Logging.Internal; +using Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; +using Microsoft.Shared.Diagnostics; +using Moq; +using Xunit; + +using IOptionsFactory = Microsoft.Extensions.Options.Options; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test; + +public class HttpRequestBodyReaderTest +{ + private readonly Fixture _fixture; + + public HttpRequestBodyReaderTest() + { + _fixture = new Fixture(); + } + + [Fact] + public void Reader_NullOptions_Throws() + { + var options = IOptionsFactory.Create((LoggingOptions)null!); + var act = () => new HttpRequestBodyReader(options); + act.Should().Throw(); + } + + [Fact] + public async Task Reader_SimpleContent_ReadsContent() + { + var input = _fixture.Create(); + var options = IOptionsFactory.Create(new LoggingOptions + { + RequestBodyContentTypes = new HashSet { "text/plain" } + }); + using var httpRequest = new HttpRequestMessage + { + Content = new StringContent(input, Encoding.UTF8, "text/plain"), + Method = HttpMethod.Post + }; + + var httpRequestBodyReader = new HttpRequestBodyReader(options); + var requestBody = await httpRequestBodyReader.ReadAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); + + requestBody.Should().BeEquivalentTo(input); + } + + [Fact] + public async Task Reader_EmptyContent_ErrorMessage() + { + var options = IOptionsFactory.Create(new LoggingOptions + { + RequestBodyContentTypes = new HashSet { "text/plain" } + }); + using var httpRequest = new HttpRequestMessage + { + Content = new StreamContent(new MemoryStream()), + Method = HttpMethod.Post + }; + var httpRequestBodyReader = new HttpRequestBodyReader(options); + + var requestBody = await httpRequestBodyReader.ReadAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); + + requestBody.Should().BeEquivalentTo(Constants.NoContent); + } + + [Theory] + [CombinatorialData] + public async Task Reader_UnreadableContent_ErrorMessage( + [CombinatorialValues("application/octet-stream", "image/png", "audio/ogg", "application/x-www-form-urlencoded", "application/javascript")] + string contentType) + { + var input = _fixture.Create(); + var options = IOptionsFactory.Create(new LoggingOptions + { + RequestBodyContentTypes = new HashSet { "text/plain" } + }); + + using var httpRequest = new HttpRequestMessage + { + Content = new StringContent(input, Encoding.UTF8, contentType), + Method = HttpMethod.Post + }; + + var httpRequestBodyReader = new HttpRequestBodyReader(options); + var requestBody = await httpRequestBodyReader.ReadAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); + + requestBody.Should().Be(Constants.UnreadableContent); + } + + [Fact] + public async Task Reader_OperationCanceled_ThrowsTaskCanceledException() + { + var input = _fixture.Create(); + var options = IOptionsFactory.Create(new LoggingOptions + { + RequestBodyContentTypes = new HashSet { "text/plain" } + }); + using var httpRequest = new HttpRequestMessage + { + Content = new StringContent(input, Encoding.UTF8, "text/plain"), + Method = HttpMethod.Post + }; + + var httpRequestBodyReader = new HttpRequestBodyReader(options); + var token = new CancellationToken(true); + + var act = async () => + await httpRequestBodyReader.ReadAsync(httpRequest, token).ConfigureAwait(false); + + await act.Should().ThrowAsync() + .Where(e => e.CancellationToken.IsCancellationRequested); + } + + [Theory] + [CombinatorialData] + public async Task Reader_BigContent_TrimsAtTheEnd([CombinatorialValues(32, 256, 4095, 4096, 4097, 65536, 131072)] int limit) + { + var input = RandomStringGenerator.Generate(limit * 2); + var options = IOptionsFactory.Create(new LoggingOptions + { + BodySizeLimit = limit, + RequestBodyContentTypes = new HashSet { "text/plain" }, + BodyReadTimeout = TimeSpan.FromMinutes(100) + }); + using var httpRequest = new HttpRequestMessage + { + Content = new StringContent(input, Encoding.UTF8, "text/plain"), + Method = HttpMethod.Post + }; + + var httpRequestBodyReader = new HttpRequestBodyReader(options); + var requestBody = await httpRequestBodyReader.ReadAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); + + requestBody.Should().BeEquivalentTo(input.Substring(0, limit)); + } + + [Theory] + [CombinatorialData] + public async Task Reader_SmallContentBigLimit_ReadsCorrectly([CombinatorialValues(32, 256, 4095, 4096, 4097, 65536, 131072)] int limit) + { + var input = RandomStringGenerator.Generate(limit / 2); + var options = IOptionsFactory.Create(new LoggingOptions + { + BodySizeLimit = limit, + RequestBodyContentTypes = new HashSet { "text/plain" }, + BodyReadTimeout = TimeSpan.FromMinutes(100) + }); + using var httpRequest = new HttpRequestMessage + { + Content = new StringContent(input, Encoding.UTF8, "text/plain"), + Method = HttpMethod.Post + }; + + var httpRequestBodyReader = new HttpRequestBodyReader(options); + var requestBody = await httpRequestBodyReader.ReadAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); + + requestBody.Should().BeEquivalentTo(input.Substring(0, limit / 2)); + } + + [Fact] + public async Task Reader_ReadingTakesTooLong_Timesout() + { + var options = IOptionsFactory.Create(new LoggingOptions + { + RequestBodyContentTypes = new HashSet { "text/plain" } + }); + var streamMock = new Mock(); +#if NETCOREAPP3_1_OR_GREATER + streamMock.Setup(x => x.ReadAsync(It.IsAny>(), It.IsAny())).Throws(); +#else + streamMock.Setup(x => x.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Throws(); +#endif + + using var httpRequest = new HttpRequestMessage + { + Content = new StreamContent(streamMock.Object), + Method = HttpMethod.Post + }; + + httpRequest.Content.Headers.Add("Content-type", "text/plain"); + + var httpRequestBodyReader = new HttpRequestBodyReader(options); + + var requestBody = await httpRequestBodyReader.ReadAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); + + var returnedValue = requestBody; + var expectedValue = Constants.ReadCancelled; + + returnedValue.Should().BeEquivalentTo(expectedValue); + } + + [Fact] + public async Task Reader_NullContent_ReturnsEmpty() + { + var options = IOptionsFactory.Create(new LoggingOptions + { + RequestBodyContentTypes = new HashSet { "text/plain" } + }); + using var httpRequest = new HttpRequestMessage + { + Content = null, + Method = HttpMethod.Post + }; + var httpRequestBodyReader = new HttpRequestBodyReader(options); + + var requestBody = await httpRequestBodyReader.ReadAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); + + requestBody.Should().Be(string.Empty); + } + + [Fact] + public async Task Reader_MethodIsGet_ReturnsEmpty() + { + var options = IOptionsFactory.Create(new LoggingOptions + { + RequestBodyContentTypes = new HashSet { "text/plain" } + }); + + using var httpRequest = new HttpRequestMessage + { + Content = new StringContent("content", Encoding.UTF8, "text/plain"), + Method = HttpMethod.Get + }; + + var httpRequestBodyReader = new HttpRequestBodyReader(options); + + var requestBody = await httpRequestBodyReader.ReadAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); + + requestBody.Should().Be(string.Empty); + } + + [Fact] + public void HttpRequestBodyReader_Has_Infinite_Timeout_For_Reading_A_Body_When_Debugger_Is_Attached() + { + var options = IOptionsFactory.Create(new LoggingOptions()); + var reader = new HttpRequestBodyReader(options, DebuggerState.Attached); + + Assert.Equal(reader.RequestReadTimeout, Timeout.InfiniteTimeSpan); + } + + [Fact] + public void HttpRequestBodyReader_Has_Option_Defined_Timeout_For_Reading_A_Body_When_Debugger_Is_Detached() + { + var timeout = TimeSpan.FromSeconds(274); + var options = IOptionsFactory.Create(new LoggingOptions { BodyReadTimeout = timeout }); + var reader = new HttpRequestBodyReader(options, DebuggerState.Detached); + + Assert.Equal(reader.RequestReadTimeout, timeout); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpRequestReaderTest.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpRequestReaderTest.cs new file mode 100644 index 0000000000..15316427cb --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpRequestReaderTest.cs @@ -0,0 +1,605 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AutoFixture; +using FluentAssertions; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Telemetry.Logging.Internal; +using Microsoft.Extensions.Telemetry; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Extensions.Telemetry.Logging; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test; + +public class HttpRequestReaderTest +{ + private const string Redacted = "REDACTED"; + + private readonly Fixture _fixture; + + public HttpRequestReaderTest() + { + _fixture = new Fixture(); + } + + [Fact] + public async Task ReadAsync_AllData_ReturnsLogRecord() + { + var requestContent = _fixture.Create(); + var responseContent = _fixture.Create(); + var host = "default-uri.com"; + var plainTextMedia = "text/plain"; + var header1 = new KeyValuePair("Header1", "Value1"); + var header2 = new KeyValuePair("Header2", "Value2"); + var header3 = new KeyValuePair("Header3", "Value3"); + var expectedRecord = new LogRecord + { + Host = host, + Method = HttpMethod.Post, + Path = TelemetryConstants.Redacted, + StatusCode = 200, + RequestHeaders = new() { new("Header1", Redacted), new("Header3", Redacted) }, + ResponseHeaders = new() { new("Header2", Redacted), new("Header3", Redacted) }, + RequestBody = requestContent, + ResponseBody = responseContent, + }; + + var options = Microsoft.Extensions.Options.Options.Create(new LoggingOptions + { + RequestHeadersDataClasses = new Dictionary { { header1.Key, SimpleClassifications.PrivateData }, { header3.Key, SimpleClassifications.PrivateData } }, + ResponseHeadersDataClasses = new Dictionary { { header2.Key, SimpleClassifications.PrivateData }, { header3.Key, SimpleClassifications.PrivateData } }, + RequestBodyContentTypes = new HashSet { plainTextMedia }, + ResponseBodyContentTypes = new HashSet { plainTextMedia }, + BodyReadTimeout = TimeSpan.FromSeconds(100000), + LogBody = true, + }); + + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor.Setup(r => r.Redact(It.IsAny>(), It.IsAny())) + .Returns(Redacted); + var headersReader = new HttpHeadersReader(options, mockHeadersRedactor.Object); + + var reader = new HttpRequestReader(options, GetHttpRouteFormatter(), headersReader, RequestMetadataContext); + + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri("http://default-uri.com/foo"), + Content = new StringContent(requestContent, Encoding.UTF8) + }; + httpRequestMessage.Headers.Add(header1.Key, header1.Value); + httpRequestMessage.Headers.Add(header3.Key, header3.Value); + + using var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(responseContent, Encoding.UTF8) + }; + httpResponseMessage.Headers.Add(header2.Key, header2.Value); + httpResponseMessage.Headers.Add(header3.Key, header3.Value); + + var logRecord = new LogRecord(); + var requestHeadersBuffer = new List>(); + var responseHeadersBuffer = new List>(); + await reader.ReadRequestAsync(logRecord, httpRequestMessage, requestHeadersBuffer, CancellationToken.None).ConfigureAwait(false); + await reader.ReadResponseAsync(logRecord, httpResponseMessage, responseHeadersBuffer, CancellationToken.None).ConfigureAwait(false); + + logRecord.Should().BeEquivalentTo( + expectedRecord, + o => o + .Excluding(m => m.RequestBody) + .Excluding(m => m.ResponseBody) + .ComparingByMembers()); + logRecord.RequestBody.Should().BeEquivalentTo(expectedRecord.RequestBody); + logRecord.ResponseBody.Should().BeEquivalentTo(expectedRecord.ResponseBody); + } + + [Fact] + public async Task ReadAsync_NoHost_ReturnsLogRecordWithoutHost() + { + var requestContent = _fixture.Create(); + var responseContent = _fixture.Create(); + const string PlainTextMedia = "text/plain"; + + var expectedRecord = new LogRecord + { + Host = TelemetryConstants.Unknown, + Method = HttpMethod.Post, + Path = TelemetryConstants.Unknown, + StatusCode = 200, + RequestBody = requestContent, + ResponseBody = responseContent, + }; + var options = Microsoft.Extensions.Options.Options.Create(new LoggingOptions + { + RequestBodyContentTypes = new HashSet { PlainTextMedia }, + ResponseBodyContentTypes = new HashSet { PlainTextMedia }, + BodyReadTimeout = TimeSpan.FromSeconds(10), + LogBody = true, + }); + + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor.Setup(r => r.Redact(It.IsAny>(), It.IsAny())) + .Returns(Redacted); + var headersReader = new HttpHeadersReader(options, mockHeadersRedactor.Object); + var reader = new HttpRequestReader(options, GetHttpRouteFormatter(), headersReader, RequestMetadataContext); + + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = null, + Content = new StringContent(requestContent, Encoding.UTF8) + }; + + using var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(responseContent, Encoding.UTF8) + }; + + var actualRecord = new LogRecord(); + var requestHeadersBuffer = new List>(); + var responseHeadersBuffer = new List>(); + var propertyBag = new LogMethodHelper(); + await reader.ReadRequestAsync(actualRecord, httpRequestMessage, requestHeadersBuffer, CancellationToken.None).ConfigureAwait(false); + await reader.ReadResponseAsync(actualRecord, httpResponseMessage, responseHeadersBuffer, CancellationToken.None).ConfigureAwait(false); + + actualRecord.Should().BeEquivalentTo( + expectedRecord, + o => o + .Excluding(m => m.RequestBody) + .Excluding(m => m.ResponseBody) + .ComparingByMembers()); + actualRecord.RequestBody.Should().BeEquivalentTo(expectedRecord.RequestBody); + actualRecord.ResponseBody.Should().BeEquivalentTo(expectedRecord.ResponseBody); + } + + [Fact] + public async Task ReadAsync_AllDataWithRequestMetadataSet_ReturnsLogRecord() + { + var requestContent = _fixture.Create(); + var responseContent = _fixture.Create(); + var host = "default-uri.com"; + var plainTextMedia = "text/plain"; + var header1 = new KeyValuePair("Header1", "Value1"); + var header2 = new KeyValuePair("Header2", "Value2"); + + var expectedRecord = new LogRecord + { + Host = host, + Method = HttpMethod.Post, + Path = "foo/bar/123", + StatusCode = 200, + RequestHeaders = new() { new("Header1", Redacted) }, + ResponseHeaders = new() { new("Header2", Redacted) }, + RequestBody = requestContent, + ResponseBody = responseContent, + }; + + var opts = new LoggingOptions + { + RequestHeadersDataClasses = new Dictionary { { header1.Key, SimpleClassifications.PrivateData } }, + ResponseHeadersDataClasses = new Dictionary { { header2.Key, SimpleClassifications.PrivateData } }, + RequestBodyContentTypes = new HashSet { plainTextMedia }, + ResponseBodyContentTypes = new HashSet { plainTextMedia }, + BodyReadTimeout = TimeSpan.FromSeconds(10), + LogBody = true, + }; + opts.RouteParameterDataClasses.Add("userId", SimpleClassifications.PrivateData); + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor.Setup(r => r.Redact(It.IsAny>(), It.IsAny())) + .Returns(Redacted); + var headersReader = new HttpHeadersReader(Microsoft.Extensions.Options.Options.Create(opts), mockHeadersRedactor.Object); + var reader = new HttpRequestReader(Microsoft.Extensions.Options.Options.Create(opts), + GetHttpRouteFormatter(), headersReader, RequestMetadataContext); + + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri("http://default-uri.com/foo/bar/123"), + Content = new StringContent(requestContent, Encoding.UTF8), + }; + httpRequestMessage.Headers.Add(header1.Key, header1.Value); + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + RequestRoute = "/foo/bar/{userId}" + }); + + using var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(responseContent, Encoding.UTF8) + }; + httpResponseMessage.Headers.Add(header2.Key, header2.Value); + + var requestHeadersBuffer = new List>(); + var responseHeadersBuffer = new List>(); + var propertyBag = new LogMethodHelper(); + var actualRecord = new LogRecord(); + await reader.ReadRequestAsync(actualRecord, httpRequestMessage, requestHeadersBuffer, CancellationToken.None).ConfigureAwait(false); + await reader.ReadResponseAsync(actualRecord, httpResponseMessage, responseHeadersBuffer, CancellationToken.None).ConfigureAwait(false); + + actualRecord.Should().BeEquivalentTo( + expectedRecord, + o => o + .Excluding(m => m.RequestBody) + .Excluding(m => m.ResponseBody) + .ComparingByMembers()); + actualRecord.RequestBody.Should().BeEquivalentTo(expectedRecord.RequestBody); + actualRecord.ResponseBody.Should().BeEquivalentTo(expectedRecord.ResponseBody); + } + + [Fact] + public async Task ReadAsync_FormatRequestPathDisabled_ReturnsLogRecordWithRoute() + { + var requestContent = _fixture.Create(); + var responseContent = _fixture.Create(); + var host = "default-uri.com"; + var plainTextMedia = "text/plain"; + var header1 = new KeyValuePair("Header1", "Value1"); + var header2 = new KeyValuePair("Header2", "Value2"); + + var expectedRecord = new LogRecord + { + Host = host, + Method = HttpMethod.Post, + Path = "foo/bar/{userId}", + StatusCode = 200, + RequestHeaders = new() { new("Header1", Redacted) }, + ResponseHeaders = new() { new("Header2", Redacted) }, + RequestBody = requestContent, + ResponseBody = responseContent, + }; + var opts = new LoggingOptions + { + LogRequestStart = true, + LogBody = true, + RequestHeadersDataClasses = new Dictionary { { header1.Key, SimpleClassifications.PrivateData } }, + ResponseHeadersDataClasses = new Dictionary { { header2.Key, SimpleClassifications.PrivateData } }, + RequestBodyContentTypes = new HashSet { plainTextMedia }, + ResponseBodyContentTypes = new HashSet { plainTextMedia }, + BodyReadTimeout = TimeSpan.FromSeconds(10), + RequestPathLoggingMode = OutgoingPathLoggingMode.Structured + }; + opts.RouteParameterDataClasses.Add("userId", SimpleClassifications.PrivateData); + + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor.Setup(r => r.Redact(It.IsAny>(), It.IsAny())) + .Returns(Redacted); + var headersReader = new HttpHeadersReader(Microsoft.Extensions.Options.Options.Create(opts), mockHeadersRedactor.Object); + var reader = new HttpRequestReader(Microsoft.Extensions.Options.Options.Create(opts), + GetHttpRouteFormatter(), headersReader, RequestMetadataContext); + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri("http://default-uri.com/foo/bar/123"), + Content = new StringContent(requestContent, Encoding.UTF8), + }; + httpRequestMessage.Headers.Add(header1.Key, header1.Value); + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + RequestRoute = "foo/bar/{userId}" + }); + + using var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(responseContent, Encoding.UTF8) + }; + httpResponseMessage.Headers.Add(header2.Key, header2.Value); + + var requestHeadersBuffer = new List>(); + var responseHeadersBuffer = new List>(); + var propertyBag = new LogMethodHelper(); + var actualRecord = new LogRecord(); + await reader.ReadRequestAsync(actualRecord, httpRequestMessage, requestHeadersBuffer, CancellationToken.None).ConfigureAwait(false); + await reader.ReadResponseAsync(actualRecord, httpResponseMessage, responseHeadersBuffer, CancellationToken.None).ConfigureAwait(false); + + actualRecord.Should().BeEquivalentTo( + expectedRecord, + o => o + .Excluding(m => m.RequestBody) + .Excluding(m => m.ResponseBody) + .ComparingByMembers()); + actualRecord.RequestBody.Should().BeEquivalentTo(expectedRecord.RequestBody); + actualRecord.ResponseBody.Should().BeEquivalentTo(expectedRecord.ResponseBody); + } + + [Fact] + public async Task ReadAsync_RouteParameterRedactionModeNone_ReturnsLogRecordWithUnredactedRoute() + { + var requestContent = _fixture.Create(); + var host = "default-uri.com"; + var plainTextMedia = "text/plain"; + var header1 = new KeyValuePair("Header1", "Value1"); + var header2 = new KeyValuePair("Header2", "Value2"); + + var expectedRecord = new LogRecord + { + Host = host, + Method = HttpMethod.Post, + Path = "/foo/bar/123", + RequestHeaders = new() { new("Header1", Redacted) }, + RequestBody = requestContent, + }; + var opts = new LoggingOptions + { + LogRequestStart = true, + LogBody = true, + RequestPathParameterRedactionMode = HttpRouteParameterRedactionMode.None, + RequestHeadersDataClasses = new Dictionary { { header1.Key, SimpleClassifications.PrivateData } }, + RequestBodyContentTypes = new HashSet { plainTextMedia }, + BodyReadTimeout = TimeSpan.FromSeconds(10), + RequestPathLoggingMode = OutgoingPathLoggingMode.Structured + }; + opts.RouteParameterDataClasses.Add("userId", SimpleClassifications.PrivateData); + + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor.Setup(r => r.Redact(It.IsAny>(), It.IsAny())) + .Returns(Redacted); + var headersReader = new HttpHeadersReader(Microsoft.Extensions.Options.Options.Create(opts), mockHeadersRedactor.Object); + var reader = new HttpRequestReader(Microsoft.Extensions.Options.Options.Create(opts), + GetHttpRouteFormatter(), headersReader, RequestMetadataContext); + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri("http://default-uri.com/foo/bar/123"), + Content = new StringContent(requestContent, Encoding.UTF8), + }; + httpRequestMessage.Headers.Add(header1.Key, header1.Value); + + var requestHeadersBuffer = new List>(); + var responseHeadersBuffer = new List>(); + var propertyBag = new LogMethodHelper(); + var actualRecord = new LogRecord(); + await reader.ReadRequestAsync(actualRecord, httpRequestMessage, requestHeadersBuffer, CancellationToken.None).ConfigureAwait(false); + + actualRecord.Should().BeEquivalentTo( + expectedRecord, + o => o + .Excluding(m => m.RequestBody) + .Excluding(m => m.ResponseBody) + .ComparingByMembers()); + } + + [Fact] + public async Task ReadAsync_RequestMetadataRequestNameSetAndRouteMissing_ReturnsLogRecord() + { + var requestContent = _fixture.Create(); + var responseContent = _fixture.Create(); + var host = "default-uri.com"; + var plainTextMedia = "text/plain"; + var header1 = new KeyValuePair("Header1", "Value1"); + var header2 = new KeyValuePair("Header2", "Value2"); + + var expectedRecord = new LogRecord + { + Host = host, + Method = HttpMethod.Post, + Path = "TestRequest", + StatusCode = 200, + RequestHeaders = new() { new("Header1", Redacted) }, + ResponseHeaders = new() { new("Header2", Redacted) }, + RequestBody = requestContent, + ResponseBody = responseContent, + }; + + var opts = new LoggingOptions + { + RequestHeadersDataClasses = new Dictionary { { header1.Key, SimpleClassifications.PrivateData } }, + ResponseHeadersDataClasses = new Dictionary { { header2.Key, SimpleClassifications.PrivateData } }, + RequestBodyContentTypes = new HashSet { plainTextMedia }, + ResponseBodyContentTypes = new HashSet { plainTextMedia }, + BodyReadTimeout = TimeSpan.FromSeconds(10), + LogBody = true, + }; + opts.RouteParameterDataClasses.Add("userId", SimpleClassifications.PrivateData); + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor.Setup(r => r.Redact(It.IsAny>(), It.IsAny())) + .Returns(Redacted); + var headersReader = new HttpHeadersReader(Microsoft.Extensions.Options.Options.Create(opts), mockHeadersRedactor.Object); + var reader = new HttpRequestReader(Microsoft.Extensions.Options.Options.Create(opts), + GetHttpRouteFormatter(), headersReader, RequestMetadataContext); + + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri("http://default-uri.com/foo/bar/123"), + Content = new StringContent(requestContent, Encoding.UTF8), + }; + httpRequestMessage.Headers.Add(header1.Key, header1.Value); + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + RequestName = "TestRequest" + }); + + using var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(responseContent, Encoding.UTF8) + }; + httpResponseMessage.Headers.Add(header2.Key, header2.Value); + + var requestHeadersBuffer = new List>(); + var responseHeadersBuffer = new List>(); + var propertyBag = new LogMethodHelper(); + var actualRecord = new LogRecord(); + await reader.ReadRequestAsync(actualRecord, httpRequestMessage, requestHeadersBuffer, CancellationToken.None).ConfigureAwait(false); + await reader.ReadResponseAsync(actualRecord, httpResponseMessage, responseHeadersBuffer, CancellationToken.None).ConfigureAwait(false); + + actualRecord.Should().BeEquivalentTo( + expectedRecord, + o => o + .Excluding(m => m.RequestBody) + .Excluding(m => m.ResponseBody) + .ComparingByMembers()); + actualRecord.RequestBody.Should().BeEquivalentTo(expectedRecord.RequestBody); + actualRecord.ResponseBody.Should().BeEquivalentTo(expectedRecord.ResponseBody); + } + + [Fact] + public async Task ReadAsync_NoMetadataUsesRedactedString_ReturnsLogRecord() + { + var requestContent = _fixture.Create(); + var responseContent = _fixture.Create(); + var host = "default-uri.com"; + var plainTextMedia = "text/plain"; + var header1 = new KeyValuePair("Header1", "Value1"); + var header2 = new KeyValuePair("Header2", "Value2"); + + var expectedRecord = new LogRecord + { + Host = host, + Method = HttpMethod.Post, + Path = TelemetryConstants.Redacted, + StatusCode = 200, + RequestHeaders = new() { new("Header1", Redacted) }, + ResponseHeaders = new() { new("Header2", Redacted) }, + RequestBody = requestContent, + ResponseBody = responseContent, + }; + + var opts = new LoggingOptions + { + RequestHeadersDataClasses = new Dictionary { { header1.Key, SimpleClassifications.PrivateData } }, + ResponseHeadersDataClasses = new Dictionary { { header2.Key, SimpleClassifications.PrivateData } }, + RequestBodyContentTypes = new HashSet { plainTextMedia }, + ResponseBodyContentTypes = new HashSet { plainTextMedia }, + BodyReadTimeout = TimeSpan.FromSeconds(10), + LogBody = true, + }; + opts.RouteParameterDataClasses.Add("userId", SimpleClassifications.PrivateData); + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor.Setup(r => r.Redact(It.IsAny>(), It.IsAny())) + .Returns(Redacted); + var headersReader = new HttpHeadersReader(Microsoft.Extensions.Options.Options.Create(opts), mockHeadersRedactor.Object); + var reader = new HttpRequestReader(Microsoft.Extensions.Options.Options.Create(opts), + GetHttpRouteFormatter(), headersReader, RequestMetadataContext); + + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri("http://default-uri.com/foo/bar/123"), + Content = new StringContent(requestContent, Encoding.UTF8), + }; + httpRequestMessage.Headers.Add(header1.Key, header1.Value); + + using var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(responseContent, Encoding.UTF8) + }; + httpResponseMessage.Headers.Add(header2.Key, header2.Value); + + var requestHeadersBuffer = new List>(); + var responseHeadersBuffer = new List>(); + var propertyBag = new LogMethodHelper(); + var actualRecord = new LogRecord(); + await reader.ReadRequestAsync(actualRecord, httpRequestMessage, requestHeadersBuffer, CancellationToken.None).ConfigureAwait(false); + await reader.ReadResponseAsync(actualRecord, httpResponseMessage, responseHeadersBuffer, CancellationToken.None).ConfigureAwait(false); + + actualRecord.Should().BeEquivalentTo( + expectedRecord, + o => o + .Excluding(m => m.RequestBody) + .Excluding(m => m.ResponseBody) + .ComparingByMembers()); + actualRecord.RequestBody.Should().BeEquivalentTo(expectedRecord.RequestBody); + actualRecord.ResponseBody.Should().BeEquivalentTo(expectedRecord.ResponseBody); + } + + [Fact] + public async Task ReadAsync_MetadataWithoutRequestRouteOrNameUsesConstants_ReturnsLogRecord() + { + var requestContent = _fixture.Create(); + var responseContent = _fixture.Create(); + var host = "default-uri.com"; + var plainTextMedia = "text/plain"; + var header1 = new KeyValuePair("Header1", "Value1"); + var header2 = new KeyValuePair("Header2", "Value2"); + + var expectedRecord = new LogRecord + { + Host = host, + Method = HttpMethod.Post, + Path = TelemetryConstants.Unknown, + StatusCode = 200, + RequestHeaders = new() { new("Header1", Redacted) }, + ResponseHeaders = new() { new("Header2", Redacted) }, + RequestBody = requestContent, + ResponseBody = responseContent, + }; + + var opts = new LoggingOptions + { + RequestHeadersDataClasses = new Dictionary { { header1.Key, SimpleClassifications.PrivateData } }, + ResponseHeadersDataClasses = new Dictionary { { header2.Key, SimpleClassifications.PrivateData } }, + RequestBodyContentTypes = new HashSet { plainTextMedia }, + ResponseBodyContentTypes = new HashSet { plainTextMedia }, + BodyReadTimeout = TimeSpan.FromSeconds(10), + LogBody = true, + }; + opts.RouteParameterDataClasses.Add("userId", SimpleClassifications.PrivateData); + var mockHeadersRedactor = new Mock(); + mockHeadersRedactor.Setup(r => r.Redact(It.IsAny>(), It.IsAny())) + .Returns(Redacted); + var headersReader = new HttpHeadersReader(Microsoft.Extensions.Options.Options.Create(opts), mockHeadersRedactor.Object); + var reader = new HttpRequestReader(Microsoft.Extensions.Options.Options.Create(opts), + GetHttpRouteFormatter(), headersReader, RequestMetadataContext); + + using var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri("http://default-uri.com/foo/bar/123"), + Content = new StringContent(requestContent, Encoding.UTF8), + }; + httpRequestMessage.Headers.Add(header1.Key, header1.Value); + httpRequestMessage.SetRequestMetadata(new RequestMetadata()); + + using var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(responseContent, Encoding.UTF8) + }; + httpResponseMessage.Headers.Add(header2.Key, header2.Value); + + var requestHeadersBuffer = new List>(); + var responseHeadersBuffer = new List>(); + var propertyBag = new LogMethodHelper(); + var actualRecord = new LogRecord(); + await reader.ReadRequestAsync(actualRecord, httpRequestMessage, requestHeadersBuffer, CancellationToken.None).ConfigureAwait(false); + await reader.ReadResponseAsync(actualRecord, httpResponseMessage, responseHeadersBuffer, CancellationToken.None).ConfigureAwait(false); + + actualRecord.Should().BeEquivalentTo( + expectedRecord, + o => o + .Excluding(m => m.RequestBody) + .Excluding(m => m.ResponseBody) + .ComparingByMembers()); + actualRecord.RequestBody.Should().BeEquivalentTo(expectedRecord.RequestBody); + actualRecord.ResponseBody.Should().BeEquivalentTo(expectedRecord.ResponseBody); + } + + private static IHttpRouteFormatter GetHttpRouteFormatter() + { + var builder = new ServiceCollection() + .AddFakeRedaction() + .AddHttpRouteProcessor() + .BuildServiceProvider(); + + return builder.GetService()!; + } + + private static IOutgoingRequestContext RequestMetadataContext => new Mock().Object; +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpResponseBodyReaderTest.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpResponseBodyReaderTest.cs new file mode 100644 index 0000000000..7dd011f542 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/HttpResponseBodyReaderTest.cs @@ -0,0 +1,185 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AutoFixture; +using FluentAssertions; +using Microsoft.Extensions.Http.Telemetry.Logging.Internal; +using Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; +using Microsoft.Shared.Diagnostics; +using Moq; +using Xunit; + +using IOptionsFactory = Microsoft.Extensions.Options.Options; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test; + +public class HttpResponseBodyReaderTest +{ + private readonly Fixture _fixture; + + public HttpResponseBodyReaderTest() + { + _fixture = new Fixture(); + } + + [Fact] + public void Reader_NullOptions_Throws() + { + var options = IOptionsFactory.Create((LoggingOptions)null!); + var act = () => new HttpResponseBodyReader(options); + act.Should().Throw(); + } + + [Fact] + public async Task Reader_SimpleContent_ReadsContent() + { + var options = IOptionsFactory.Create(new LoggingOptions + { + ResponseBodyContentTypes = new HashSet { "text/plain" } + }); + var httpResponseBodyReader = new HttpResponseBodyReader(options); + var expectedContentBody = _fixture.Create(); + using var httpResponse = new HttpResponseMessage + { + Content = new StringContent(expectedContentBody, Encoding.UTF8, "text/plain") + }; + + var responseBody = await httpResponseBodyReader.ReadAsync(httpResponse, CancellationToken.None).ConfigureAwait(false); + + responseBody.Should().BeEquivalentTo(expectedContentBody); + } + + [Fact] + public async Task Reader_EmptyContent_ErrorMessage() + { + var options = IOptionsFactory.Create(new LoggingOptions + { + ResponseBodyContentTypes = new HashSet { "text/plain" } + }); + using var httpResponse = new HttpResponseMessage + { + Content = new StreamContent(new MemoryStream()) + }; + + var httpResponseBodyReader = new HttpResponseBodyReader(options); + var responseBody = await httpResponseBodyReader.ReadAsync(httpResponse, CancellationToken.None).ConfigureAwait(false); + + responseBody.Should().Be(Constants.NoContent); + } + + [Theory] + [CombinatorialData] + public async Task Reader_UnreadableContent_ErrorMessage( + [CombinatorialValues("application/octet-stream", "image/png", "audio/ogg", "application/x-www-form-urlencoded", + "application/javascript")] + string contentType) + { + var options = IOptionsFactory.Create(new LoggingOptions + { + ResponseBodyContentTypes = new HashSet { "text/plain" } + }); + var httpResponseBodyReader = new HttpResponseBodyReader(options); + var expectedContentBody = _fixture.Create(); + using var httpResponse = new HttpResponseMessage + { + Content = new StringContent(expectedContentBody, Encoding.UTF8, contentType) + }; + + var responseBody = await httpResponseBodyReader.ReadAsync(httpResponse, CancellationToken.None).ConfigureAwait(false); + + responseBody.Should().Be(Constants.UnreadableContent); + } + + [Fact] + public async Task Reader_OperationCanceled_ThrowsTaskCanceledException() + { + var options = IOptionsFactory.Create(new LoggingOptions + { + ResponseBodyContentTypes = new HashSet { "text/plain" } + }); + var httpResponseBodyReader = new HttpResponseBodyReader(options); + var input = _fixture.Create(); + using var httpResponse = new HttpResponseMessage + { + Content = new StringContent(input, Encoding.UTF8, "text/plain") + }; + + var token = new CancellationToken(true); + + var act = async () => await httpResponseBodyReader.ReadAsync(httpResponse, token); + + await act.Should().ThrowAsync().Where(e => e.CancellationToken.IsCancellationRequested); + } + + [Theory] + [CombinatorialData] + public async Task Reader_BigContent_TrimsAtTheEnd([CombinatorialValues(32, 256, 4095, 4096, 4097, 65536, 131072)] int limit) + { + var options = IOptionsFactory.Create(new LoggingOptions + { + BodySizeLimit = limit, + ResponseBodyContentTypes = new HashSet { "text/plain" } + }); + var httpResponseBodyReader = new HttpResponseBodyReader(options); + var bigContent = RandomStringGenerator.Generate(limit * 2); + using var httpResponse = new HttpResponseMessage + { + Content = new StringContent(bigContent, Encoding.UTF8, "text/plain") + }; + + var responseBody = await httpResponseBodyReader.ReadAsync(httpResponse, CancellationToken.None).ConfigureAwait(false); + + responseBody.Should().Be(bigContent.Substring(0, limit)); + } + + [Fact] + public async Task Reader_ReadingTakesTooLong_TimesOut() + { + var options = IOptionsFactory.Create(new LoggingOptions + { + ResponseBodyContentTypes = new HashSet { "text/plain" } + }); + var httpResponseBodyReader = new HttpResponseBodyReader(options); + var streamMock = new Mock(); +#if NETCOREAPP3_1_OR_GREATER + streamMock.Setup(x => x.ReadAsync(It.IsAny>(), It.IsAny())).Throws(); +#else + streamMock.Setup(x => x.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Throws(); +#endif + using var httpResponse = new HttpResponseMessage + { + Content = new StreamContent(streamMock.Object) + }; + httpResponse.Content.Headers.Add("Content-type", "text/plain"); + + var requestBody = await httpResponseBodyReader.ReadAsync(httpResponse, CancellationToken.None).ConfigureAwait(false); + + requestBody.Should().Be(Constants.ReadCancelled); + } + + [Fact] + public void HttpResponseBodyReader_Has_Infinite_Timeout_For_Reading_A_Body_When_Debugger_Is_Attached() + { + var options = IOptionsFactory.Create(new LoggingOptions()); + var reader = new HttpResponseBodyReader(options, DebuggerState.Attached); + + Assert.Equal(reader.ResponseReadTimeout, Timeout.InfiniteTimeSpan); + } + + [Fact] + public void HttpResponseBodyReader_Has_Option_Defined_Timeout_For_Reading_A_Body_When_Debugger_Is_Detached() + { + var timeout = TimeSpan.FromSeconds(274); + var options = IOptionsFactory.Create(new LoggingOptions { BodyReadTimeout = timeout }); + var reader = new HttpResponseBodyReader(options, DebuggerState.Detached); + + Assert.Equal(reader.ResponseReadTimeout, timeout); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/EmptyEnricher.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/EmptyEnricher.cs new file mode 100644 index 0000000000..9dd8024fc6 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/EmptyEnricher.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; + +internal class EmptyEnricher : IHttpClientLogEnricher +{ + public void Enrich(IEnrichmentPropertyBag enrichmentBag, HttpRequestMessage? request = null, HttpResponseMessage? response = null) + { + // intentionally left empty. + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/EnricherWithCounter.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/EnricherWithCounter.cs new file mode 100644 index 0000000000..af5be4ca28 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/EnricherWithCounter.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; + +internal class EnricherWithCounter : IHttpClientLogEnricher +{ + public int TimesCalled; + + public void Enrich(IEnrichmentPropertyBag enrichmentBag, HttpRequestMessage? request = null, + HttpResponseMessage? response = null) => + TimesCalled++; +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/HelperExtensions.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/HelperExtensions.cs new file mode 100644 index 0000000000..bdb2aca45d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/HelperExtensions.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; + +internal static class HelperExtensions +{ + public static IServiceCollection BlockRemoteCall(this IServiceCollection services) + { + return services + .AddTransient() + .ConfigureAll(options => + { + options.HttpMessageHandlerBuilderActions.Add(builder => + { + builder.AdditionalHandlers.Add(builder.Services.GetRequiredService()); + }); + }); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/ITestHttpClient1.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/ITestHttpClient1.cs new file mode 100644 index 0000000000..9aeb572303 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/ITestHttpClient1.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; + +internal interface ITestHttpClient1 +{ + Task SendRequest(HttpRequestMessage httpRequestMessage); +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/ITestHttpClient2.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/ITestHttpClient2.cs new file mode 100644 index 0000000000..013312bb09 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/ITestHttpClient2.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; + +internal interface ITestHttpClient2 +{ + Task SendRequest(HttpRequestMessage httpRequestMessage); +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/LogRecordExtensions.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/LogRecordExtensions.cs new file mode 100644 index 0000000000..5f5e669ea5 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/LogRecordExtensions.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Http.Telemetry.Logging.Internal; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; + +internal static class LogRecordExtensions +{ + public static Dictionary GetStructuredState(this FakeLogRecord logRecord) + { + Assert.NotNull(logRecord.StructuredState); + Assert.NotEmpty(logRecord.StructuredState); + return logRecord.StructuredState.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + } + + public static string GetEnrichmentProperty(this LogRecord logRecord, string name) + { + return logRecord.EnrichmentProperties!.FirstOrDefault(kvp => kvp.Key == name).Value!.ToString()!; + } + + public static void Contains(this Dictionary logRecord, string key, string value) + { + Assert.True(logRecord.ContainsKey(key)); + Assert.Equal(value, logRecord[key]); + } + + public static void NotContains(this Dictionary logRecord, string key) + { + Assert.False(logRecord.ContainsKey(key)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/MockedLogger.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/MockedLogger.cs new file mode 100644 index 0000000000..aa3634759a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/MockedLogger.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; + +public class MockedLogger : ILogger +{ + public Mock> Mock { get; } + + public MockedLogger(Mock> mock) + { + Mock = mock; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, + Func formatter) => Mock.Object.Log(logLevel, eventId, state, exception, formatter); + + public bool IsEnabled(LogLevel logLevel) => Mock.Object.IsEnabled(logLevel); + +#pragma warning disable CS8633 +#pragma warning disable CS8766 + public IDisposable? BeginScope(TState state) + where TState : notnull => Mock.Object.BeginScope(state); +#pragma warning restore CS8633 +#pragma warning restore CS8766 +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/MockedRequestReader.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/MockedRequestReader.cs new file mode 100644 index 0000000000..602aeaefb9 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/MockedRequestReader.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.Telemetry.Logging.Internal; +using Moq; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; + +internal class MockedRequestReader : IHttpRequestReader +{ + internal Mock Mock { get; } + + internal MockedRequestReader(Mock mock) + { + Mock = mock; + } + + public Task ReadResponseAsync(LogRecord record, + HttpResponseMessage response, + List>? buffer, + CancellationToken cancellationToken) => Mock.Object.ReadResponseAsync(record, response, buffer, cancellationToken); + + public Task ReadRequestAsync(LogRecord record, + HttpRequestMessage request, + List>? buffer, + CancellationToken cancellationToken) => Mock.Object.ReadRequestAsync(record, request, buffer, cancellationToken); +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/NoRemoteCallHandler.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/NoRemoteCallHandler.cs new file mode 100644 index 0000000000..e3a844c49c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/NoRemoteCallHandler.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; + +internal class NoRemoteCallHandler : DelegatingHandler +{ + private const string FileName = "Text.txt"; + private readonly byte[] _fileContent; + + public NoRemoteCallHandler() + { + var assemblyFileLocation = Path.GetDirectoryName(typeof(NoRemoteCallHandler).Assembly.Location)!; + var uri = new Uri(assemblyFileLocation).LocalPath; + + var responseFilePath = Path.Combine(Directory.GetFiles( + path: uri, + searchPattern: FileName)); + + _fileContent = File.ReadAllBytes(responseFilePath); + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var response = new HttpResponseMessage + { + StatusCode = System.Net.HttpStatusCode.OK, + RequestMessage = request, + Content = new StreamContent(new MemoryStream(_fileContent)) + }; + + response.Content.Headers.ContentType = new("application/json"); + + return Task.FromResult(response); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/NotSeekableStream.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/NotSeekableStream.cs new file mode 100644 index 0000000000..3d24596c3f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/NotSeekableStream.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; + +internal class NotSeekableStream : Stream +{ + private readonly MemoryStream _innerStream; + + public NotSeekableStream(MemoryStream memoryStream) + { + _innerStream = memoryStream; + } + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => true; + + public override long Length => _innerStream.Length; + + public override long Position { get => _innerStream.Position; set => _innerStream.Position = value; } + + public override void Flush() => _innerStream.Flush(); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _innerStream.ReadAsync(buffer, offset, count, cancellationToken); + +#if NETCOREAPP3_1_OR_GREATER + public override ValueTask ReadAsync(System.Memory buffer, CancellationToken cancellationToken = default) => _innerStream.ReadAsync(buffer, cancellationToken); +#endif + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _innerStream.WriteAsync(buffer, offset, count, cancellationToken); + +#if NETCOREAPP3_1_OR_GREATER + public override ValueTask WriteAsync(System.ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => _innerStream.WriteAsync(buffer, cancellationToken); +#endif + + public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin); + + public override void SetLength(long value) => _innerStream.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) => _innerStream.Write(buffer, offset, count); +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/RandomStringGenerator.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/RandomStringGenerator.cs new file mode 100644 index 0000000000..010792a989 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/RandomStringGenerator.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; + +public static class RandomStringGenerator +{ + private static readonly Random _random = new(); + + public static string Generate(int length) + { + const string Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + return new string( + Enumerable + .Repeat(Chars, length) + .Select(s => s[_random.Next(s.Length)]).ToArray()); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestConfiguration.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestConfiguration.cs new file mode 100644 index 0000000000..274e02f28d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestConfiguration.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; + +internal static class TestConfiguration +{ + /// + /// It returns section with request body read timeout set to specified value. + /// + /// Timeout to be set for request body read timeout. + /// Instance of configuration section. + public static IConfigurationSection GetHttpClientLoggingConfigurationSection(TimeSpan timeout) => + new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { + $"{nameof(LoggingOptions)}:{nameof(LoggingOptions.BodyReadTimeout)}", + timeout.ToString() + } + }) + .Build() + .GetSection($"{nameof(LoggingOptions)}"); +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestEnricher.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestEnricher.cs new file mode 100644 index 0000000000..c3364caf64 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestEnricher.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Net.Http; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; + +internal class TestEnricher : IHttpClientLogEnricher +{ + internal readonly KeyValuePair KvpRequest = new("test key request", "test value"); + internal readonly KeyValuePair KvpResponse = new("test key response", "test value"); + public LogMethodHelper EnrichmentBag => new() { KvpRequest, KvpResponse }; + + public void Enrich(IEnrichmentPropertyBag enrichmentBag, HttpRequestMessage? request = null, + HttpResponseMessage? response = null) + { + if (request is not null) + { + enrichmentBag.Add(KvpRequest.Key, KvpRequest.Value!); + } + + if (response is not null) + { + enrichmentBag.Add(KvpResponse.Key, KvpResponse.Value!); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestHttpClient1.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestHttpClient1.cs new file mode 100644 index 0000000000..bc31491513 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestHttpClient1.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; + +public class TestHttpClient1 : ITestHttpClient1 +{ + private readonly System.Net.Http.HttpClient _httpClient; + + public TestHttpClient1(System.Net.Http.HttpClient httpClient) + { + _httpClient = httpClient; + } + + public Task SendRequest(HttpRequestMessage httpRequestMessage) + { + return _httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestHttpClient2.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestHttpClient2.cs new file mode 100644 index 0000000000..170b63994f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestHttpClient2.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; + +internal class TestHttpClient2 : ITestHttpClient2 +{ + private readonly System.Net.Http.HttpClient _httpClient; + + public TestHttpClient2(System.Net.Http.HttpClient httpClient) + { + _httpClient = httpClient; + } + + public Task SendRequest(HttpRequestMessage httpRequestMessage) + { + return _httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestHttpMessageHandlerBuilder.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestHttpMessageHandlerBuilder.cs new file mode 100644 index 0000000000..2118bc7edc --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestHttpMessageHandlerBuilder.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; + +internal class TestHttpMessageHandlerBuilder : HttpMessageHandlerBuilder +{ + private readonly IServiceCollection _services; + + public TestHttpMessageHandlerBuilder(IServiceCollection services) + { + _services = services; + } + + public override HttpMessageHandler Build() => throw new NotSupportedException("Test"); + + public override string Name { get; set; } = string.Empty; +#pragma warning disable CS8764 // Nullability of return type doesn't match overridden member (possibly because of nullability attributes). + public override HttpMessageHandler? PrimaryHandler { get; set; } +#pragma warning restore CS8764 // Nullability of return type doesn't match overridden member (possibly because of nullability attributes). + public override IList AdditionalHandlers { get; } = new List(); + public override IServiceProvider Services => _services.BuildServiceProvider(); +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestingHandlerStub.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestingHandlerStub.cs new file mode 100644 index 0000000000..d6a2f63731 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/Internal/TestingHandlerStub.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test.Internal; + +public class TestingHandlerStub : DelegatingHandler +{ + private readonly Func> _handlerFunc; + + public TestingHandlerStub(Func> handlerFunc) + { + _handlerFunc = handlerFunc; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => _handlerFunc(request, cancellationToken); +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/LogRecordPooledObjectPolicyTest.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/LogRecordPooledObjectPolicyTest.cs new file mode 100644 index 0000000000..23126176b2 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/LogRecordPooledObjectPolicyTest.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using AutoFixture; +using FluentAssertions; +using Microsoft.Extensions.Http.Telemetry.Logging.Internal; +using Microsoft.Shared.Pools; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test; + +public class LogRecordPooledObjectPolicyTest +{ + [Fact] + public void LogRecordPooledObjectPolicy_ResetsLogRecord() + { + var pool = PoolFactory.CreatePool(new LogRecordPooledObjectPolicy()); + var testObject = new Fixture().Create(); + testObject.RequestHeaders!.Add(new KeyValuePair("key1", "value1")); + testObject.ResponseHeaders!.Add(new KeyValuePair("key2", "value2")); + testObject.EnrichmentProperties!.Add("key3", "value3"); + + var logRecord1 = pool.Get(); + logRecord1.Host = testObject.Host; + logRecord1.Method = testObject.Method; + logRecord1.Path = testObject.Path; + logRecord1.Duration = testObject.Duration; + logRecord1.StatusCode = testObject.StatusCode; + logRecord1.RequestHeaders = testObject.RequestHeaders; + logRecord1.ResponseHeaders = testObject.ResponseHeaders; + logRecord1.RequestBody = testObject.RequestBody; + logRecord1.ResponseBody = testObject.ResponseBody; + logRecord1.EnrichmentProperties = testObject.EnrichmentProperties; + pool.Return(logRecord1); + + var logRecord2 = pool.Get(); + logRecord2.Host.Should().Be(string.Empty); + logRecord2.Method.Should().Be(default); + logRecord2.Path.Should().Be(string.Empty); + logRecord2.Duration.Should().Be(default); + logRecord2.StatusCode.Should().BeNull(); + logRecord2.RequestHeaders.Should().BeNull(); + logRecord2.ResponseHeaders.Should().BeNull(); + logRecord2.RequestBody.Should().Be(string.Empty); + logRecord2.ResponseBody.Should().Be(string.Empty); + logRecord2.EnrichmentProperties.Should().BeNull(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/LoggingOptionsTest.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/LoggingOptionsTest.cs new file mode 100644 index 0000000000..0ca22a36ab --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/LoggingOptionsTest.cs @@ -0,0 +1,133 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Testing; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test; + +public class LoggingOptionsTest +{ + private readonly LoggingOptions _sut; + + public LoggingOptionsTest() + { + _sut = new LoggingOptions(); + } + + [Fact] + public void CanConstruct_Class() + { + var sut = new LoggingOptions(); + + sut.Should().NotBeNull(); + } + + [Fact] + public void CanSetAndGet_BodySizeLimit() + { + const int TestSizeLimit = 1; + + _sut.BodySizeLimit = TestSizeLimit; + + _sut.BodySizeLimit.Should().Be(TestSizeLimit); + } + + [Fact] + public void CanSetAndGet_BodyReadTimeout() + { + var testTimeout = TimeSpan.FromMinutes(1); + + _sut.BodyReadTimeout = testTimeout; + + testTimeout.Should().Be(testTimeout); + } + + [Fact] + public void CanSetAndGet_RequestBodyContentTypes() + { + var testContentTypes = new HashSet { "application/xml" }; + + _sut.RequestBodyContentTypes = testContentTypes; + + _sut.RequestBodyContentTypes.Should().BeEquivalentTo(testContentTypes); + } + + [Fact] + public void CanSetAndGet_ResponseBodyContentTypes() + { + var testContentTypes = new HashSet { "application/xml" }; + + _sut.ResponseBodyContentTypes = testContentTypes; + + _sut.ResponseBodyContentTypes.Should().BeEquivalentTo(testContentTypes); + } + + [Fact] + public void CanSetAndGet_RequestHeaders() + { + var testHeaders = new Dictionary + { + { "header 1", SimpleClassifications.PrivateData }, + { "header 2", SimpleClassifications.PrivateData } + }; + + _sut.RequestHeadersDataClasses = testHeaders; + + _sut.RequestHeadersDataClasses.Should().BeEquivalentTo(testHeaders); + } + + [Fact] + public void CanSetAndGet_ResponseHeaders() + { + var testHeaders = new Dictionary + { + { "header 1", SimpleClassifications.PrivateData }, + { "header 2", SimpleClassifications.PrivateData } + }; + + _sut.ResponseHeadersDataClasses = testHeaders; + + _sut.ResponseHeadersDataClasses.Should().BeEquivalentTo(testHeaders); + } + + [Theory] + [CombinatorialData] + public void CanSetAndGet_FormatRequestPath( + [CombinatorialValues(OutgoingPathLoggingMode.Structured, OutgoingPathLoggingMode.Formatted)] + OutgoingPathLoggingMode testValue) + { + _sut.RequestPathLoggingMode = testValue; + + _sut.RequestPathLoggingMode.Should().Be(testValue); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CanSetAndGet_LogRequestStart(bool testValue) + { + _sut.LogRequestStart = testValue; + + _sut.LogRequestStart.Should().Be(testValue); + } + + [Fact] + public void CanAndAndGet_RouteTemplateParametersToRedact() + { + var paramsToRedacts = new Dictionary + { + { "foo", SimpleClassifications.PrivateData }, + { "bar", SimpleClassifications.PrivateData }, + }; + + _sut.RouteParameterDataClasses.Add("foo", SimpleClassifications.PrivateData); + _sut.RouteParameterDataClasses.Add("bar", SimpleClassifications.PrivateData); + + _sut.RouteParameterDataClasses.Should().BeEquivalentTo(paramsToRedacts); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/LoggingOptionsValidatorTest.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/LoggingOptionsValidatorTest.cs new file mode 100644 index 0000000000..6bbc66da11 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/LoggingOptionsValidatorTest.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using FluentAssertions; +using Microsoft.Extensions.Http.Telemetry.Logging.Internal; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test; + +public class LoggingOptionsValidatorTest +{ + [Fact] + public void Ctor_CreatesAnInstance() + { + var act = () => _ = new LoggingOptionsValidator(); + + act.Should().NotThrow(); + } + + [Fact] + public void Validate_ObjectHasNoIssues_Success() + { + var validator = new LoggingOptionsValidator(); + var result = validator.Validate("model", new LoggingOptions()); + + result.Succeeded.Should().BeTrue(); + } + + [Fact] + public void Validate_ObjectHasOneIssues_Fails() + { + var validator = new LoggingOptionsValidator(); + var options = new LoggingOptions { BodyReadTimeout = TimeSpan.Zero }; + + validator.Validate("model", options).Failed.Should().BeTrue(); + } + + [Fact] + public void Validate_ObjectHasTwoIssues_Fails() + { + var validator = new LoggingOptionsValidator(); + var options = new LoggingOptions { BodySizeLimit = -1 }; + + validator.Validate("model", options).Failed.Should().BeTrue(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/MediaTypeCollectionExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/MediaTypeCollectionExtensionsTest.cs new file mode 100644 index 0000000000..107dcc4af6 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Logging/MediaTypeCollectionExtensionsTest.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.Extensions.Http.Telemetry.Logging.Internal; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Logging.Test; + +public class MediaTypeCollectionExtensionsTest +{ + private readonly string[] _readableContentTypes = + { + "application/*+json", + "application/*+xml", + "application/json", + "application/xml", + "text/*" + }; + + [Fact] + public void Covers_WhenCovers_ReturnsTrue() + { + var collection = new HashSet(_readableContentTypes, StringComparer.OrdinalIgnoreCase).ToFrozenSet(StringComparer.OrdinalIgnoreCase, optimizeForReading: true); + collection.Covers("application/xml").Should().BeTrue(); + collection.Covers("APPLICATION/XML").Should().BeTrue(); + collection.Covers("application/json").Should().BeTrue(); + collection.Covers("APPLICATION/JSON").Should().BeTrue(); + collection.Covers("application/atom+xml").Should().BeTrue(); + collection.Covers("APPLICATION/ATOM+XML").Should().BeTrue(); + collection.Covers("application/mud+json").Should().BeTrue(); + collection.Covers("APPLICATION/MUD+JSON").Should().BeTrue(); + collection.Covers("TEXT/WHATEVER").Should().BeTrue(); + collection.Covers("text/whatever").Should().BeTrue(); + collection.Covers("text/whatever-else").Should().BeTrue(); + } + + [Fact] + public void Covers_WhenNotCovers_ReturnsFalse() + { + var collection = new HashSet(_readableContentTypes, StringComparer.OrdinalIgnoreCase).ToFrozenSet(StringComparer.OrdinalIgnoreCase, optimizeForReading: true); + + collection.Covers(null!).Should().BeFalse(); + collection.Covers("").Should().BeFalse(); + collection.Covers("image").Should().BeFalse(); + collection.Covers("image/png").Should().BeFalse(); + collection.Covers("audio/ogg").Should().BeFalse(); + collection.Covers("application").Should().BeFalse(); + collection.Covers("application/octet-stream").Should().BeFalse(); + collection.Covers("application/x-httpd-php").Should().BeFalse(); + collection.Covers("application/json-seq").Should().BeFalse(); + collection.Covers("application/missing-blocks+cbor-seq").Should().BeFalse(); + collection.Covers("application/secevent+jwt").Should().BeFalse(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/HttpMeteringHandlerTests.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/HttpMeteringHandlerTests.cs new file mode 100644 index 0000000000..3dabcc580a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/HttpMeteringHandlerTests.cs @@ -0,0 +1,729 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Telemetry.Metering.Internal; +using Microsoft.Extensions.Http.Telemetry.Metering.Test.Internal; +using Microsoft.Extensions.Telemetry; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metering; +using Microsoft.Extensions.Time.Testing; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Metering.Test; +#pragma warning disable CA2000 // Not necessary to dispose all resources in test class. +#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits +public sealed class HttpMeteringHandlerTests : IDisposable +{ + private const long DefaultClockAdvanceMs = 200; + private const string DelayPropertyName = nameof(DelayPropertyName); + + private static readonly Uri _failureUri = new("https://www.example-failure.com/foo?bar"); + private static readonly Uri _successfullUri = new("https://www.example-success.com/foo?bar"); + + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly FakeTimeProvider _fakeTimeProvider = new(); + + private long _clockAdvanceMs = DefaultClockAdvanceMs; + + public HttpMeteringHandlerTests() + { + _cancellationTokenSource = new CancellationTokenSource(); + } + + public void Dispose() + { + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); + } + + [Fact] + public void SendAsync_Success_NoNamesSet() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + var client = CreateClientWithHandler(meter); + + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Put, _successfullUri); + + _ = client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token).Result; + + var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!; + Assert.NotNull(latest); + Assert.Equal("www.example-success.com", latest.GetDimension(Metric.ReqHost)); + Assert.Equal(TelemetryConstants.Unknown, latest.GetDimension(Metric.DependencyName)); + Assert.Equal($"PUT {TelemetryConstants.Unknown}", latest.GetDimension(Metric.ReqName)); + Assert.Equal(201, latest.GetDimension(Metric.RspResultCode)); + } + + [Fact] + public void SendAsync_Success() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + var client = CreateClientWithHandler(meter); + + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _successfullUri); + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + DependencyName = "success_service", + RequestRoute = "/foo" + }); + + _ = client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token).Result; + + var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!; + Assert.NotNull(latest); + Assert.Equal("www.example-success.com", latest.GetDimension(Metric.ReqHost)); + Assert.Equal("success_service", latest.GetDimension(Metric.DependencyName)); + Assert.Equal($"GET /foo", latest.GetDimension(Metric.ReqName)); + Assert.Equal(201, latest.GetDimension(Metric.RspResultCode)); + } + + [Fact] + public void PerfStopwatch_ReturnsTotalMiliseconds_InsteadOfFraction() + { + const long TimeAdvanceMs = 1500L; // We need to use any value greater than 1000 (1 second) + const string ServiceName = "success_service"; + + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + var client = CreateClientWithHandler(meter); + + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _successfullUri); + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + DependencyName = ServiceName, + RequestRoute = "/foo" + }); + _clockAdvanceMs = TimeAdvanceMs; + + _ = client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token).Result; + + var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal("www.example-success.com", latest.GetDimension(Metric.ReqHost)); + Assert.Equal("success_service", latest.GetDimension(Metric.DependencyName)); + Assert.Equal("GET /foo", latest.GetDimension(Metric.ReqName)); + Assert.Equal(201, latest.GetDimension(Metric.RspResultCode)); + } + + [Fact] + public async Task SendAsync_Exception() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + var client = CreateClientWithHandler(meter); + + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _failureUri); + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + DependencyName = "failure_service", + RequestRoute = "/foo/failure", + RequestName = "TestRequestName" + }); + + await Assert.ThrowsAsync(async () => await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token)); + + var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal("www.example-failure.com", latest.GetDimension(Metric.ReqHost)); + Assert.Equal("failure_service", latest.GetDimension(Metric.DependencyName)); + Assert.Equal("POST TestRequestName", latest.GetDimension(Metric.ReqName)); + Assert.Equal(500, latest.GetDimension(Metric.RspResultCode)); + } + + [Fact] + public void SendAsync_SetReqMetadata_OnAsyncContext_Success() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + var requestMetadataContextMock = new Mock(); + var client = CreateClientWithHandler(meter, requestMetadataContextMock.Object); + + var requestMetadata = new RequestMetadata + { + DependencyName = "success_service", + RequestRoute = "/foo" + }; + requestMetadataContextMock.Setup(m => m.RequestMetadata).Returns(requestMetadata); + + _ = client.GetAsync(_successfullUri, _cancellationTokenSource.Token).Result; + + var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal("www.example-success.com", latest.GetDimension(Metric.ReqHost)); + Assert.Equal("success_service", latest.GetDimension(Metric.DependencyName)); + Assert.Equal("GET /foo", latest.GetDimension(Metric.ReqName)); + Assert.Equal(201, latest.GetDimension(Metric.RspResultCode)); + } + + [Fact] + public void PerfStopwatch_SetReqMetadata_OnAsyncContext_ReturnsTotalMiliseconds_InsteadOfFraction() + { + const long TimeAdvanceMs = 1500L; // We need to use any value greater than 1000 (1 second) + const string ServiceName = "success_service"; + + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + var requestMetadataContextMock = new Mock(); + var client = CreateClientWithHandler(meter, requestMetadataContextMock.Object); + + var requestMetadata = new RequestMetadata + { + DependencyName = ServiceName, + RequestRoute = "/foo" + }; + + requestMetadataContextMock.Setup(m => m.RequestMetadata).Returns(requestMetadata); + + _clockAdvanceMs = TimeAdvanceMs; + + _ = client.GetAsync(_successfullUri, _cancellationTokenSource.Token).Result; + + var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal("www.example-success.com", latest.GetDimension(Metric.ReqHost)); + Assert.Equal("success_service", latest.GetDimension(Metric.DependencyName)); + Assert.Equal("GET /foo", latest.GetDimension(Metric.ReqName)); + Assert.Equal(201, latest.GetDimension(Metric.RspResultCode)); + } + + [Fact] + public async Task SendAsync_SetReqMetadata_OnAsyncContext_Exception() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + var requestMetadataContextMock = new Mock(); + var client = CreateClientWithHandler(meter, requestMetadataContextMock.Object); + + var requestMetadata = new RequestMetadata + { + DependencyName = "failure_service", + RequestRoute = "/foo/failure", + RequestName = "TestRequestName" + }; + requestMetadataContextMock.Setup(m => m.RequestMetadata).Returns(requestMetadata); + + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _failureUri); + await Assert.ThrowsAsync(async () => await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token)); + + var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal("www.example-failure.com", latest.GetDimension(Metric.ReqHost)); + Assert.Equal("failure_service", latest.GetDimension(Metric.DependencyName)); + Assert.Equal("POST TestRequestName", latest.GetDimension(Metric.ReqName)); + Assert.Equal(500, latest.GetDimension(Metric.RspResultCode)); + } + + [Fact] + public void SendAsync_WithDownstreamDependencyMetadata_OnAsyncContext_Success() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + var downstreamDependencyMetadataManagerMock = new Mock(); + var client = CreateClientWithHandler(meter, downstreamDependencyMetadataManager: downstreamDependencyMetadataManagerMock.Object); + + var requestMetadata = new RequestMetadata + { + DependencyName = "success_service", + RequestRoute = "/foo" + }; + downstreamDependencyMetadataManagerMock.Setup(m => m.GetRequestMetadata(It.IsAny())).Returns(requestMetadata); + + _ = client.GetAsync(_successfullUri, _cancellationTokenSource.Token).Result; + + var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal("www.example-success.com", latest.GetDimension(Metric.ReqHost)); + Assert.Equal("success_service", latest.GetDimension(Metric.DependencyName)); + Assert.Equal("GET /foo", latest.GetDimension(Metric.ReqName)); + Assert.Equal(201, latest.GetDimension(Metric.RspResultCode)); + } + + [Fact] + public void PerfStopwatch_WithDownstreamDependencyMetadata_OnAsyncContext_ReturnsTotalMiliseconds_InsteadOfFraction() + { + const long TimeAdvanceMs = 1500L; // We need to use any value greater than 1000 (1 second) + const string ServiceName = "success_service"; + + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + var downstreamDependencyMetadataManagerMock = new Mock(); + var client = CreateClientWithHandler(meter, downstreamDependencyMetadataManager: downstreamDependencyMetadataManagerMock.Object); + + var requestMetadata = new RequestMetadata + { + DependencyName = ServiceName, + RequestRoute = "/foo" + }; + + downstreamDependencyMetadataManagerMock.Setup(m => m.GetRequestMetadata(It.IsAny())).Returns(requestMetadata); + + _clockAdvanceMs = TimeAdvanceMs; + + _ = client.GetAsync(_successfullUri, _cancellationTokenSource.Token).Result; + + var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal("www.example-success.com", latest.GetDimension(Metric.ReqHost)); + Assert.Equal("success_service", latest.GetDimension(Metric.DependencyName)); + Assert.Equal("GET /foo", latest.GetDimension(Metric.ReqName)); + Assert.Equal(201, latest.GetDimension(Metric.RspResultCode)); + } + + [Fact] + public async Task SendAsync_WithDownstreamDependencyMetadata_OnAsyncContext_Exception() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + var dependencyDataManagerMock = new Mock(); + var client = CreateClientWithHandler(meter, downstreamDependencyMetadataManager: dependencyDataManagerMock.Object); + + var requestMetadata = new RequestMetadata + { + DependencyName = "failure_service", + RequestRoute = "/foo/failure", + RequestName = "TestRequestName" + }; + dependencyDataManagerMock.Setup(m => m.GetRequestMetadata(It.IsAny())).Returns(requestMetadata); + + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _failureUri); + await Assert.ThrowsAsync(async () => await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token)); + + var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal("www.example-failure.com", latest.GetDimension(Metric.ReqHost)); + Assert.Equal("failure_service", latest.GetDimension(Metric.DependencyName)); + Assert.Equal("POST TestRequestName", latest.GetDimension(Metric.ReqName)); + Assert.Equal(500, latest.GetDimension(Metric.RspResultCode)); + } + + [Fact] + public void GetHostName_Returns_Unknown_For_Empty_RequestUri() + { + var c = HttpMeteringHandler.GetHostName(new HttpRequestMessage()); + + Assert.Equal(TelemetryConstants.Unknown, c); + } + + [Fact] + public async Task SendAsync_MultiEnrich() + { + for (int i = 1; i <= 14; i++) + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + var client = CreateClientWithHandler(meter, new List + { + new TestEnricher(i), + }); + + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _successfullUri); + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + DependencyName = "success_service", + RequestRoute = "/foo" + }); + + _ = await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token); + + var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal("www.example-success.com", latest.GetDimension(Metric.ReqHost)); + Assert.Equal("success_service", latest.GetDimension(Metric.DependencyName)); + Assert.Equal("GET /foo", latest.GetDimension(Metric.ReqName)); + Assert.Equal(201, latest.GetDimension(Metric.RspResultCode)); + + for (int j = 0; j < i; j++) + { + Assert.Equal($"test_value_{j + 1}", latest.GetDimension($"test_property_{j + 1}")); + } + } + } + + [Fact] + public async Task SendAsync_MultiEnrich_UsingIMeter() + { + for (int i = 1; i <= 14; i++) + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + var handler = new HttpMeteringHandler(meter, new List + { + new TestEnricher(i), + }) + { + InnerHandler = new TestHandlerStub(InnerHandlerFunction) + }; + + var client = new System.Net.Http.HttpClient(handler); + + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _successfullUri); + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + DependencyName = "success_service", + RequestRoute = "/foo" + }); + + _ = await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token); + + var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal("www.example-success.com", latest.GetDimension(Metric.ReqHost)); + Assert.Equal("success_service", latest.GetDimension(Metric.DependencyName)); + Assert.Equal("GET /foo", latest.GetDimension(Metric.ReqName)); + Assert.Equal(201, latest.GetDimension(Metric.RspResultCode)); + + for (int j = 0; j < i; j++) + { + Assert.Equal($"test_value_{j + 1}", latest.GetDimension($"test_property_{j + 1}")); + } + } + } + + [Fact] + public async Task InvokeAsync_HttpMeteringHandler_MultipleEnrichers() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + var client = CreateClientWithHandler(meter, new List + { + new TestEnricher(2), + new TestEnricher(2, "2"), + }); + + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _successfullUri); + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + DependencyName = "success_service", + RequestRoute = "/foo" + }); + + _ = await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token); + var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal("www.example-success.com", latest.GetDimension(Metric.ReqHost)); + Assert.Equal("success_service", latest.GetDimension(Metric.DependencyName)); + Assert.Equal("GET /foo", latest.GetDimension(Metric.ReqName)); + Assert.Equal(201, latest.GetDimension(Metric.RspResultCode)); + Assert.Equal("test_value_1", latest.GetDimension("test_property_1")); + Assert.Equal("test_value_2", latest.GetDimension("test_property_2")); + Assert.Equal("test_value_21", latest.GetDimension("test_property_21")); + Assert.Equal("test_value_22", latest.GetDimension("test_property_22")); + } + + [Fact] + public async Task InvokeAsync_HttpMeteringHandler_PropertyBagEdgeCase() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + var client = CreateClientWithHandler(meter, new List + { + new PropertyBagEdgeCaseEnricher(), + }); + + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, _successfullUri); + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + DependencyName = "success_service", + RequestRoute = "/foo" + }); + + _ = await client.SendAsync(httpRequestMessage, _cancellationTokenSource.Token); + var latest = metricCollector.GetHistogramValues(Metric.OutgoingRequestMetricName)!.LatestWritten!; + + Assert.NotNull(latest); + Assert.Equal("www.example-success.com", latest.GetDimension(Metric.ReqHost)); + Assert.Equal("success_service", latest.GetDimension(Metric.DependencyName)); + Assert.Equal("GET /foo", latest.GetDimension(Metric.ReqName)); + Assert.Equal(201, latest.GetDimension(Metric.RspResultCode)); + Assert.Equal("test_val", latest.GetDimension("non_null_object_property")); + } + + [Fact] + public void HttpMeteringHandler_Fail_16DEnrich() + { + using var meter = new Meter(); + Assert.Throws(() => + CreateClientWithHandler(meter, new List + { + new TestEnricher(16) + })); + } + + [Fact] + public void HttpMeteringHandler_Fail_RepeatCustomDimensions() + { + using var meter = new Meter(); + Assert.Throws(() => + CreateClientWithHandler(meter, new List + { + new TestEnricher(1), + new TestEnricher(1), + })); + } + + [Fact] + public void HttpMeteringHandler_Fail_RepeatDefaultDimensions() + { + using var meter = new Meter(); + Assert.Throws(() => + CreateClientWithHandler(meter, new List + { + new SameDefaultDimEnricher(), + })); + } + + [Fact] + public void ServiceCollection_GivenNullArguments_Throws() + { + Assert.Throws(() => + ((IServiceCollection)null!).AddOutgoingRequestMetricEnricher()); + + Assert.Throws(() => + ((IServiceCollection)null!).AddOutgoingRequestMetricEnricher(Mock.Of())); + + Assert.Throws(() => + new ServiceCollection() + .AddOutgoingRequestMetricEnricher(null!)); + } + + [Fact] + public void ServiceCollection_AddMultipleOutgoingRequestEnrichersSuccessfully() + { + IOutgoingRequestMetricEnricher testEnricher = new TestEnricher(); + var services = new ServiceCollection(); + services.AddOutgoingRequestMetricEnricher(); + services.AddOutgoingRequestMetricEnricher(testEnricher); + + using var provider = services.BuildServiceProvider(); + var enrichersCollection = provider.GetServices(); + + var enricherCount = 0; + foreach (var enricher in enrichersCollection) + { + enricherCount++; + } + + Assert.Equal(2, enricherCount); + } + + private System.Net.Http.HttpClient CreateClientWithHandler( + Meter meter, + IEnumerable outgoingRequestMetricEnrichers) + { + var handler = new HttpMeteringHandler(meter, outgoingRequestMetricEnrichers) + { + InnerHandler = new TestHandlerStub(InnerHandlerFunction) + }; + + var client = new System.Net.Http.HttpClient(handler); + return client; + } + + private System.Net.Http.HttpClient CreateClientWithHandler( + Meter meter, + IOutgoingRequestContext? requestMetadataContext = null, + IDownstreamDependencyMetadataManager? downstreamDependencyMetadataManager = null) + { + var handler = new HttpMeteringHandler(meter, Array.Empty(), requestMetadataContext, downstreamDependencyMetadataManager) + { + InnerHandler = new TestHandlerStub(InnerHandlerFunction), + TimeProvider = _fakeTimeProvider + }; + + var client = new System.Net.Http.HttpClient(handler); + return client; + } + + private Task InnerHandlerFunction(HttpRequestMessage request, CancellationToken cancellationToken) + { + _fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(_clockAdvanceMs)); + + if (request.RequestUri == _failureUri) + { + throw new HttpRequestException("Something went wrong"); + } + + var response = new HttpResponseMessage { StatusCode = HttpStatusCode.Created }; + return Task.FromResult(response); + } + + [Fact] + public static void AddHttpClientMetering_NullHttpClient() + { + IHttpClientBuilder nullBuilder = null!; + Assert.Throws(() => nullBuilder.AddHttpClientMetering()); + } + + [Fact] + public static void AddHttpClientMetering_NullServiceCollection_Throws() + { + IServiceCollection services = null!; + Assert.Throws(() => services.AddDefaultHttpClientMetering()); + } + + [Fact] + public static void AddHttpClientMetering_CreatesClientSuccessfully() + { + using var sp = new ServiceCollection() + .RegisterMetering() + .AddHttpClient() + .AddDefaultHttpClientMetering() + .BuildServiceProvider(); + + var httpClientFactory = sp.GetRequiredService(); + + using var httpClient = httpClientFactory?.CreateClient(); + + Assert.NotNull(httpClient); + } + + [Fact] + public static void AddHttpClientMetering_EnsureBuild() + { + const string HttpClientIdentifier = "HttpClientClass"; + + using var provider = new ServiceCollection() + .RegisterMetering() + .AddHttpClient(HttpClientIdentifier) + .AddHttpClientMetering() + .Services +#if NETCOREAPP3_1_OR_GREATER + .BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); +#else + .BuildServiceProvider(validateScopes: true); +#endif + var client = provider + .GetRequiredService() + .CreateClient(HttpClientIdentifier); + + Assert.NotNull(client); + } + + [Fact] + public static void AddHttpClientMetering_Enrichers_EnsureBuild() + { + const string HttpClientIdentifier = "HttpClientClass"; + IOutgoingRequestMetricEnricher testEnricher = new TestEnricher(1, "prefxi"); + + using var provider = new ServiceCollection() + .RegisterMetering() + .AddHttpClient(HttpClientIdentifier) + .AddHttpClientMetering() + .Services + .AddOutgoingRequestMetricEnricher(testEnricher) + .AddOutgoingRequestMetricEnricher() +#if NETCOREAPP3_1_OR_GREATER + .BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); +#else + .BuildServiceProvider(validateScopes: true); +#endif + var client = provider + .GetRequiredService() + .CreateClient(HttpClientIdentifier); + + Assert.NotNull(client); + } + + [Fact] + public static void AddDefaultHttpClientMetering_RequestMetadataSetSuccessfully() + { + using var sp = new ServiceCollection() + .RegisterMetering() + .AddHttpClient() + .AddDefaultHttpClientMetering() + .BuildServiceProvider(); + + var requestMetadataContext = sp.GetRequiredService(); + + var requestMetadata = new RequestMetadata + { + DependencyName = "success_service", + RequestRoute = "/foo" + }; + + if (requestMetadataContext != null) + { + requestMetadataContext.RequestMetadata = requestMetadata; + } + + Assert.NotNull(requestMetadataContext); + } + + [Fact] + public static async Task AddDefaultHttpClientMetering_WithDownstreamDependencyMetadata_UsesIt() + { + var dependencyMetadata = new TestDownstreamDependencyMetadata(); + var downstreamDependencyMetadataManagerMock = new Mock(); + downstreamDependencyMetadataManagerMock + .Setup(m => m.GetRequestMetadata(It.IsAny())) + .Returns(It.IsAny()); + + using var sp = new ServiceCollection() + .RegisterMetering() + .AddHttpClient() + .AddDefaultHttpClientMetering() + .AddDownstreamDependencyMetadata() + .AddSingleton(downstreamDependencyMetadataManagerMock.Object) + .BlockRemoteCall() + .BuildServiceProvider(); + + var client = sp + .GetRequiredService() + .CreateClient(nameof(AddDefaultHttpClientMetering_WithDownstreamDependencyMetadata_UsesIt)); + + _ = await client.GetAsync("https://contoso.com"); + + downstreamDependencyMetadataManagerMock.Verify(m => m.GetRequestMetadata(It.IsAny()), Times.Once); + } + + [Fact] + public static async Task AddtHttpClientMetering_WithDownstreamDependencyMetadata_UsesIt() + { + var downstreamDependencyMetadataManagerMock = new Mock(); + downstreamDependencyMetadataManagerMock + .Setup(m => m.GetRequestMetadata(It.IsAny())) + .Returns(It.IsAny()); + + using var sp = new ServiceCollection() + .RegisterMetering() + .AddHttpClient(nameof(AddtHttpClientMetering_WithDownstreamDependencyMetadata_UsesIt)) + .AddHttpClientMetering() + .Services + .AddDownstreamDependencyMetadata() + .AddSingleton(downstreamDependencyMetadataManagerMock.Object) + .BlockRemoteCall() + .BuildServiceProvider(); + + var client = sp + .GetRequiredService() + .CreateClient(nameof(AddtHttpClientMetering_WithDownstreamDependencyMetadata_UsesIt)); + + _ = await client.GetAsync("https://contoso.com"); + + downstreamDependencyMetadataManagerMock.Verify(m => m.GetRequestMetadata(It.IsAny()), Times.Once); + } +#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/HelperExtensions.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/HelperExtensions.cs new file mode 100644 index 0000000000..ee3372ba40 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/HelperExtensions.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http; + +namespace Microsoft.Extensions.Http.Telemetry.Metering.Test.Internal; + +internal static class HelperExtensions +{ + public static IServiceCollection BlockRemoteCall(this IServiceCollection services) + { + return services + .AddTransient() + .ConfigureAll(options => + { + options.HttpMessageHandlerBuilderActions.Add(builder => + { + builder.AdditionalHandlers.Add(builder.Services.GetRequiredService()); + }); + }); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/NoRemoteCallHandler.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/NoRemoteCallHandler.cs new file mode 100644 index 0000000000..73d4f82365 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/NoRemoteCallHandler.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Http.Telemetry.Metering.Test.Internal; + +internal class NoRemoteCallHandler : DelegatingHandler +{ + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(new HttpResponseMessage + { + StatusCode = System.Net.HttpStatusCode.OK, + RequestMessage = request, + }); + } +} + diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/NullOutgoingRequestMetricEnricher.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/NullOutgoingRequestMetricEnricher.cs new file mode 100644 index 0000000000..9970e8691f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/NullOutgoingRequestMetricEnricher.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Http.Telemetry.Metering.Test.Internal; + +internal class NullOutgoingRequestMetricEnricher : IOutgoingRequestMetricEnricher +{ + public IReadOnlyList DimensionNames => null!; + + public void Enrich(IEnrichmentPropertyBag enrichmentBag) + { + enrichmentBag.Add(null!, null!); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/PropertyBagEdgeCaseEnricher.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/PropertyBagEdgeCaseEnricher.cs new file mode 100644 index 0000000000..7429c248f1 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/PropertyBagEdgeCaseEnricher.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Http.Telemetry.Metering.Test.Internal; + +internal class PropertyBagEdgeCaseEnricher : IOutgoingRequestMetricEnricher +{ + public IReadOnlyList DimensionNames => new[] { "non_null_object_property" }; + private readonly object _stringObj = "test_val"; + + public void Enrich(IEnrichmentPropertyBag enrichmentBag) + { + enrichmentBag.Add("non_null_object_property", _stringObj); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/SameDefaultDimEnricher.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/SameDefaultDimEnricher.cs new file mode 100644 index 0000000000..0f4dd7be07 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/SameDefaultDimEnricher.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Http.Telemetry.Metering.Test.Internal; + +internal class SameDefaultDimEnricher : IOutgoingRequestMetricEnricher +{ + public IReadOnlyList DimensionNames => new[] { "req_host" }; + + public void Enrich(IEnrichmentPropertyBag enrichmentBag) + { + enrichmentBag.Add("req_host", "req_host_value"); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/TestDownstreamDependencyMetadata.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/TestDownstreamDependencyMetadata.cs new file mode 100644 index 0000000000..0c19c7ea82 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/TestDownstreamDependencyMetadata.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Http.Telemetry; + +namespace Microsoft.Extensions.Http.Telemetry.Metering.Test.Internal; + +internal sealed class TestDownstreamDependencyMetadata : IDownstreamDependencyMetadata +{ + public string DependencyName => "testdep"; + + private static readonly ISet _uniqueHostNameSuffixes = new HashSet + { + ".test.com", + }; + + private static readonly ISet _requestMetadataSet = new HashSet + { + new ("GET", "testroute", "testrequestname"), + }; + + public ISet UniqueHostNameSuffixes => _uniqueHostNameSuffixes; + + public ISet RequestMetadata => _requestMetadataSet; +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/TestEnricher.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/TestEnricher.cs new file mode 100644 index 0000000000..47b277e6e2 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/Internal/TestEnricher.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Http.Telemetry.Metering.Test.Internal; + +internal class TestEnricher : IOutgoingRequestMetricEnricher +{ + private readonly List _dimensionNames = new(); + private readonly int _numDimensions; + private readonly string _prefix; + + public TestEnricher() + { + _numDimensions = 1; + _prefix = string.Empty; + + for (int i = 1; i <= _numDimensions; i++) + { + _dimensionNames.Add($"test_property_{_prefix}{i}"); + } + } + + public TestEnricher(int numDimensions, string prefix = "") + { + _numDimensions = numDimensions; + _prefix = prefix; + + for (int i = 1; i <= _numDimensions; i++) + { + _dimensionNames.Add($"test_property_{_prefix}{i}"); + } + } + + public IReadOnlyList DimensionNames => _dimensionNames; + + public void Enrich(IEnrichmentPropertyBag enrichmentBag) + { + for (int i = 1; i <= _numDimensions; i++) + { + enrichmentBag.Add($"test_property_{_prefix}{i}", $"test_value_{_prefix}{i}"); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/TestHandlerStub.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/TestHandlerStub.cs new file mode 100644 index 0000000000..3c09bb09ad --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Metering/TestHandlerStub.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Http.Telemetry.Metering.Test; + +public class TestHandlerStub : DelegatingHandler +{ + private readonly Func> _handlerFunc; + + public TestHandlerStub(Func> handlerFunc) + { + _handlerFunc = handlerFunc; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return _handlerFunc(request, cancellationToken); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Microsoft.Extensions.Http.Telemetry.Tests.csproj b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Microsoft.Extensions.Http.Telemetry.Tests.csproj new file mode 100644 index 0000000000..1905330b7f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Microsoft.Extensions.Http.Telemetry.Tests.csproj @@ -0,0 +1,35 @@ + + + Microsoft.Extensions.Http.Telemetry.Test + Unit tests for Microsoft.Extensions.Http.Telemetry. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Text.txt b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Text.txt new file mode 100644 index 0000000000..cda76d56a2 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Text.txt @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae tincidunt nibh. Aliquam egestas interdum metus et egestas. Suspendisse lacinia a dolor vel cursus. Aliquam tincidunt mauris mauris, vitae posuere mi pharetra eget. Integer congue massa sed ligula condimentum, id sodales justo vulputate. Nunc orci erat, commodo sit amet nisi id, vestibulum tincidunt lectus. Quisque et magna vel elit venenatis porttitor quis facilisis nunc. Etiam sed elit ac lectus hendrerit dapibus vel nec risus. Sed ante mi, finibus eget cursus nec, accumsan sit amet nisl. Vestibulum vulputate leo eget metus tempor finibus. Ut iaculis finibus pretium. Suspendisse ornare dapibus nunc, eu aliquam lacus bibendum in. Aenean risus enim, dictum eget enim vel, pulvinar viverra erat. Aenean iaculis in nibh eu ornare. Sed fermentum orci sed accumsan scelerisque. Suspendisse maximus felis nec nisl consectetur dapibus. Ut eu augue blandit, rhoncus arcu vel, tincidunt turpis. Proin gravida, sapien vitae vulputate ornare, lorem justo lacinia libero, in fringilla nibh elit eget arcu. Quisque convallis dui at odio volutpat, id laoreet nibh faucibus. Vivamus a neque porttitor, ultrices tellus id, facilisis leo. Curabitur dignissim turpis sit amet erat efficitur, sed congue leo sollicitudin. Proin mattis rutrum nisl imperdiet ornare. Proin dignissim eleifend elit, at finibus justo pretium sed. Nulla fermentum, risus nec congue molestie, metus quam euismod est, gravida laoreet arcu ante iaculis nisi. Aenean commodo massa quis dignissim tincidunt. Etiam non congue quam. Vestibulum nunc ipsum, congue ac massa ut, condimentum euismod risus. Phasellus volutpat egestas ipsum, id fermentum quam malesuada id. Mauris vitae sagittis lacus, nec tincidunt lorem. Morbi id bibendum ligula, eget sagittis lacus. Aenean elementum euismod ornare. Curabitur rhoncus cursus libero, vel elementum lorem maximus a. In fringilla pharetra placerat. Ut condimentum ante sit amet posuere lacinia. Mauris aliquam quam magna, nec bibendum risus venenatis nec. Sed eu metus eleifend, venenatis tellus quis, facilisis massa. Mauris at ornare risus. Suspendisse justo sem, rutrum non ipsum vitae, eleifend varius nibh. Nullam rutrum eleifend sapien eu eleifend. Vestibulum velit arcu, aliquam vel neque vel, feugiat tincidunt turpis. Donec semper augue at tempus auctor. Donec congue consectetur nunc a feugiat. Pellentesque urna diam, tincidunt eget massa vel, tincidunt ornare tellus. Nunc aliquet nulla eget orci finibus, eu dapibus nunc dictum. Ut in nulla placerat, consequat elit in, rhoncus purus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Cras eu vulputate nisi. In malesuada nisi ut lectus lacinia rutrum. Ut non arcu nec lorem ultricies condimentum. Praesent aliquet pulvinar purus, in tincidunt dui. Nam sit amet sem et turpis iaculis iaculis. Etiam in massa nec ipsum fringilla fringilla. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis sed aliquet massa, eu suscipit felis. Nullam eget gravida tellus, vitae maximus tortor. Aliquam vitae augue arcu. In accumsan tellus eros, sed placerat neque venenatis sed. Ut velit ligula, cursus pellentesque rhoncus sit amet, semper eget eros. Sed sed risus a leo elementum hendrerit. Donec id tortor finibus, cursus diam a, commodo ante. Nam ultricies massa nec nisl dignissim volutpat. Proin porttitor pretium rhoncus. Nunc viverra pretium urna, sit amet commodo arcu sollicitudin at. Ut rhoncus, ante non efficitur auctor, tortor est venenatis lectus, vel maximus augue nisi ac massa. Fusce in risus mauris. Vivamus ullamcorper dui odio. Pellentesque quis tortor eu libero congue porttitor. Vestibulum facilisis augue vitae odio porta, quis viverra dolor accumsan. Proin dignissim elit ac erat laoreet sagittis. Fusce mollis ornare augue non laoreet. Nam varius elit dolor, vitae egestas mi egestas congue. Maecenas sodales rhoncus magna, in cursus eros molestie quis. Vestibulum non enim vel arcu rutrum varius. Suspendisse porta sem erat, quis elementum quam consectetur eget. Sed lectus metus, bibendum lobortis nibh non, mattis egestas justo. Aliquam eget molestie leo. Nullam ultricies ac ipsum ut convallis. In hac habitasse platea dictumst. Suspendisse in interdum eros. Pellentesque ac elementum urna. Proin vulputate vel ligula quis ullamcorper. Curabitur aliquam, metus at vestibulum laoreet, urna massa sollicitudin justo, a efficitur risus orci a justo. Pellentesque id condimentum orci. Morbi est metus, tincidunt rutrum sodales sed, condimentum ac diam. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam facilisis tempus semper. Mauris vulputate nisl in elit semper, vel viverra mauris efficitur. Ut volutpat nisi eu ipsum lacinia molestie. In hac habitasse platea dictumst. Suspendisse ut posuere metus. Sed congue sodales suscipit. Aliquam a ullamcorper nisi. Maecenas vitae nibh porta, tempus libero ut, vehicula metus. Curabitur venenatis mauris ac placerat porta. Pellentesque nulla enim, aliquet eu sollicitudin ac, euismod ut elit. Sed nec urna finibus, maximus nibh eget, rutrum massa. Pellentesque et dui rutrum, ultrices risus nec, aliquet neque. Aliquam erat volutpat. Praesent semper porta tortor, nec euismod eros hendrerit et. Morbi vel vulputate sem. Ut varius quam quam, a mollis lectus fermentum sed. Nunc in volutpat ligula. Ut venenatis ligula vitae pharetra lobortis. Sed vitae eros non diam tincidunt rutrum quis a ipsum. Donec condimentum non est at pretium. Phasellus rhoncus non urna vel volutpat. Duis eleifend nunc id efficitur pharetra. Ut et pellentesque mi, eget pellentesque ex. Mauris volutpat libero lectus, sit amet pretium lectus ultrices ac. Etiam congue ex a nisl aliquet, sed condimentum metus iaculis. Donec non erat euismod, facilisis ligula pulvinar, placerat odio. Aenean eget orci fringilla, laoreet leo eu, semper enim. Aenean sed fermentum mi, ut feugiat quam. Morbi ex ipsum, malesuada ac vestibulum non, elementum sit amet ipsum. Pellentesque elementum urna quis nunc aliquet efficitur non vel est. Nunc ultricies fermentum quam sed luctus. Aliquam maximus mi vestibulum est ornare facilisis. Morbi ut aliquam dolor. Sed efficitur ligula quis odio pulvinar placerat. Vestibulum tellus neque, scelerisque eu posuere id, elementum congue felis. Proin in dolor venenatis tellus blandit consequat ac et mauris. Donec luctus, elit sit amet lacinia efficitur, dolor sapien porttitor nunc, id dignissim leo arcu vitae dolor. Aenean mattis bibendum nibh, vel faucibus neque maximus non. Vivamus mi eros, sodales et nulla quis, tristique tempus ante. Vestibulum sagittis molestie justo nec imperdiet. Sed fermentum ipsum vitae enim auctor porttitor. Aenean facilisis vestibulum tristique. Curabitur ac convallis enim, eu sagittis urna. Aenean rhoncus gravida eros. Ut mollis mi eget metus efficitur malesuada. Aenean lacus nisl, sodales at leo at, tempor pretium tortor. Vestibulum felis tellus, tincidunt a fermentum mattis, aliquam non sapien. Donec ut tortor erat. Maecenas malesuada at eros eget tincidunt. Nam maximus, est et euismod molestie, quam turpis malesuada arcu, sit amet fringilla mauris urna vitae orci. Donec tincidunt dolor risus, eu porta felis maximus at. Morbi placerat eget nibh tempus sodales. Nulla luctus urna et ipsum tempus, at mollis arcu rutrum. Nulla ultrices ligula in felis venenatis porttitor. Mauris vel velit felis. Sed consectetur varius semper. Nulla eu vehicula massa, sit amet euismod eros. Quisque dui tortor, posuere vel tempus non, pellentesque sit amet urna. Ut sit amet ultricies ex. Ut metus dolor, porttitor a lacus ut, volutpat porttitor velit. Suspendisse efficitur erat vel ex aliquam dignissim. Donec vulputate vel magna in convallis. Ut auctor augue neque, aliquet aliquam tellus vulputate nec. Nunc eu rhoncus odio. Phasellus sollicitudin tortor ut libero molestie, in blandit sem sodales. Mauris molestie condimentum pharetra. Duis in lacus scelerisque, pretium urna id, blandit est. Phasellus in ullamcorper leo, a sodales diam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae tincidunt nibh. Aliquam egestas interdum metus et egestas. Suspendisse lacinia a dolor vel cursus. Aliquam tincidunt mauris mauris, vitae posuere mi pharetra eget. Integer congue massa sed ligula condimentum, id sodales justo vulputate. Nunc orci erat, commodo sit amet nisi id, vestibulum tincidunt lectus. Quisque et magna vel elit venenatis porttitor quis facilisis nunc. Etiam sed elit ac lectus hendrerit dapibus vel nec risus. Sed ante mi, finibus eget cursus nec, accumsan sit amet nisl. Vestibulum vulputate leo eget metus tempor finibus. Ut iaculis finibus pretium. Suspendisse ornare dapibus nunc, eu aliquam lacus bibendum in. Aenean risus enim, dictum eget enim vel, pulvinar viverra erat. Aenean iaculis in nibh eu ornare. Sed fermentum orci sed accumsan scelerisque. Suspendisse maximus felis nec nisl consectetur dapibus. Ut eu augue blandit, rhoncus arcu vel, tincidunt turpis. Proin gravida, sapien vitae vulputate ornare, lorem justo lacinia libero, in fringilla nibh elit eget arcu. Quisque convallis dui at odio volutpat, id laoreet nibh faucibus. Vivamus a neque porttitor, ultrices tellus id, facilisis leo. Curabitur dignissim turpis sit amet erat efficitur, sed congue leo sollicitudin. Proin mattis rutrum nisl imperdiet ornare. Proin dignissim eleifend elit, at finibus justo pretium sed. Nulla fermentum, risus nec congue molestie, metus quam euismod est, gravida laoreet arcu ante iaculis nisi. Aenean commodo massa quis dignissim tincidunt. Etiam non congue quam. Vestibulum nunc ipsum, congue ac massa ut, condimentum euismod risus. Phasellus volutpat egestas ipsum, id fermentum quam malesuada id. Mauris vitae sagittis lacus, nec tincidunt lorem. Morbi id bibendum ligula, eget sagittis lacus. Aenean elementum euismod ornare. Curabitur rhoncus cursus libero, vel elementum lorem maximus a. In fringilla pharetra placerat. Ut condimentum ante sit amet posuere lacinia. Mauris aliquam quam magna, nec bibendum risus venenatis nec. Sed eu metus eleifend, venenatis tellus quis, facilisis massa. Mauris at ornare risus. Suspendisse justo sem, rutrum non ipsum vitae, eleifend varius nibh. Nullam rutrum eleifend sapien eu eleifend. Vestibulum velit arcu, aliquam vel neque vel, feugiat tincidunt turpis. Donec semper augue at tempus auctor. Donec congue consectetur nunc a feugiat. Pellentesque urna diam, tincidunt eget massa vel, tincidunt ornare tellus. Nunc aliquet nulla eget orci finibus, eu dapibus nunc dictum. Ut in nulla placerat, consequat elit in, rhoncus purus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Cras eu vulputate nisi. In malesuada nisi ut lectus lacinia rutrum. Ut non arcu nec lorem ultricies condimentum. Praesent aliquet pulvinar purus, in tincidunt dui. Nam sit amet sem et turpis iaculis iaculis. Etiam in massa nec ipsum fringilla fringilla. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis sed aliquet massa, eu suscipit felis. Nullam eget gravida tellus, vitae maximus tortor. Aliquam vitae augue arcu. In accumsan tellus eros, sed placerat neque venenatis sed. Ut velit ligula, cursus pellentesque rhoncus sit amet, semper eget eros. Sed sed risus a leo elementum hendrerit. Donec id tortor finibus, cursus diam a, commodo ante. Nam ultricies massa nec nisl dignissim volutpat. Proin porttitor pretium rhoncus. Nunc viverra pretium urna, sit amet commodo arcu sollicitudin at. Ut rhoncus, ante non efficitur auctor, tortor est venenatis lectus, vel maximus augue nisi ac massa. Fusce in risus mauris. Vivamus ullamcorper dui odio. Pellentesque quis tortor eu libero congue porttitor. Vestibulum facilisis augue vitae odio porta, quis viverra dolor accumsan. Proin dignissim elit ac erat laoreet sagittis. Fusce mollis ornare augue non laoreet. Nam varius elit dolor, vitae egestas mi egestas congue. Maecenas sodales rhoncus magna, in cursus eros molestie quis. Vestibulum non enim vel arcu rutrum varius. Suspendisse porta sem erat, quis elementum quam consectetur eget. Sed lectus metus, bibendum lobortis nibh non, mattis egestas justo. Aliquam eget molestie leo. Nullam ultricies ac ipsum ut convallis. In hac habitasse platea dictumst. Suspendisse in interdum eros. Pellentesque ac elementum urna. Proin vulputate vel ligula quis ullamcorper. Curabitur aliquam, metus at vestibulum laoreet, urna massa sollicitudin justo, a efficitur risus orci a justo. Pellentesque id condimentum orci. Morbi est metus, tincidunt rutrum sodales sed, condimentum ac diam. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam facilisis tempus semper. Mauris vulputate nisl in elit semper, vel viverra mauris efficitur. Ut volutpat nisi eu ipsum lacinia molestie. In hac habitasse platea dictumst. Suspendisse ut posuere metus. Sed congue sodales suscipit. Aliquam a ullamcorper nisi. Maecenas vitae nibh porta, tempus libero ut, vehicula metus. Curabitur venenatis mauris ac placerat porta. Pellentesque nulla enim, aliquet eu sollicitudin ac, euismod ut elit. Sed nec urna finibus, maximus nibh eget, rutrum massa. Pellentesque et dui rutrum, ultrices risus nec, aliquet neque. Aliquam erat volutpat. Praesent semper porta tortor, nec euismod eros hendrerit et. Morbi vel vulputate sem. Ut varius quam quam, a mollis lectus fermentum sed. Nunc in volutpat ligula. Ut venenatis ligula vitae pharetra lobortis. Sed vitae eros non diam tincidunt rutrum quis a ipsum. Donec condimentum non est at pretium. Phasellus rhoncus non urna vel volutpat. Duis eleifend nunc id efficitur pharetra. Ut et pellentesque mi, eget pellentesque ex. Mauris volutpat libero lectus, sit amet pretium lectus ultrices ac. Etiam congue ex a nisl aliquet, sed condimentum metus iaculis. Donec non erat euismod, facilisis ligula pulvinar, placerat odio. Aenean eget orci fringilla, laoreet leo eu, semper enim. Aenean sed fermentum mi, ut feugiat quam. Morbi ex ipsum, malesuada ac vestibulum non, elementum sit amet ipsum. Pellentesque elementum urna quis nunc aliquet efficitur non vel est. Nunc ultricies fermentum quam sed luctus. Aliquam maximus mi vestibulum est ornare facilisis. Morbi ut aliquam dolor. Sed efficitur ligula quis odio pulvinar placerat. Vestibulum tellus neque, scelerisque eu posuere id, elementum congue felis. Proin in dolor venenatis tellus blandit consequat ac et mauris. Donec luctus, elit sit amet lacinia efficitur, dolor sapien porttitor nunc, id dignissim leo arcu vitae dolor. Aenean mattis bibendum nibh, vel faucibus neque maximus non. Vivamus mi eros, sodales et nulla quis, tristique tempus ante. Vestibulum sagittis molestie justo nec imperdiet. Sed fermentum ipsum vitae enim auctor porttitor. Aenean facilisis vestibulum tristique. Curabitur ac convallis enim, eu sagittis urna. Aenean rhoncus gravida eros. Ut mollis mi eget metus efficitur malesuada. Aenean lacus nisl, sodales at leo at, tempor pretium tortor. Vestibulum felis tellus, tincidunt a fermentum mattis, aliquam non sapien. Donec ut tortor erat. Maecenas malesuada at eros eget tincidunt. Nam maximus, est et euismod molestie, quam turpis malesuada arcu, sit amet fringilla mauris urna vitae orci. Donec tincidunt dolor risus, eu porta felis maximus at. Morbi placerat eget nibh tempus sodales. Nulla luctus urna et ipsum tempus, at mollis arcu rutrum. Nulla ultrices ligula in felis venenatis porttitor. Mauris vel velit felis. Sed consectetur varius semper. Nulla eu vehicula massa, sit amet euismod eros. Quisque dui tortor, posuere vel tempus non, pellentesque sit amet urna. Ut sit amet ultricies ex. Ut metus dolor, porttitor a lacus ut, volutpat porttitor velit. Suspendisse efficitur erat vel ex aliquam dignissim. Donec vulputate vel magna in convallis. Ut auctor augue neque, aliquet aliquam tellus vulputate nec. Nunc eu rhoncus odio. Phasellus sollicitudin tortor ut libero molestie, in blandit sem sodales. Mauris molestie condimentum pharetra. Duis in lacus scelerisque, pretium urna id, blandit est. Phasellus in ullamcorper leo, a sodales diam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae tincidunt nibh. Aliquam egestas interdum metus et egestas. Suspendisse lacinia a dolor vel cursus. Aliquam tincidunt mauris mauris, vitae posuere mi pharetra eget. Integer congue massa sed ligula condimentum, id sodales justo vulputate. Nunc orci erat, commodo sit amet nisi id, vestibulum tincidunt lectus. Quisque et magna vel elit venenatis porttitor quis facilisis nunc. Etiam sed elit ac lectus hendrerit dapibus vel nec risus. Sed ante mi, finibus eget cursus nec, accumsan sit amet nisl. Vestibulum vulputate leo eget metus tempor finibus. Ut iaculis finibus pretium. Suspendisse ornare dapibus nunc, eu aliquam lacus bibendum in. Aenean risus enim, dictum eget enim vel, pulvinar viverra erat. Aenean iaculis in nibh eu ornare. Sed fermentum orci sed accumsan scelerisque. Suspendisse maximus felis nec nisl consectetur dapibus. Ut eu augue blandit, rhoncus arcu vel, tincidunt turpis. Proin gravida, sapien vitae vulputate ornare, lorem justo lacinia libero, in fringilla nibh elit eget arcu. Quisque convallis dui at odio volutpat, id laoreet nibh faucibus. Vivamus a neque porttitor, ultrices tellus id, facilisis leo. Curabitur dignissim turpis sit amet erat efficitur, sed congue leo sollicitudin. Proin mattis rutrum nisl imperdiet ornare. Proin dignissim eleifend elit, at finibus justo pretium sed. Nulla fermentum, risus nec congue molestie, metus quam euismod est, gravida laoreet arcu ante iaculis nisi. Aenean commodo massa quis dignissim tincidunt. Etiam non congue quam. Vestibulum nunc ipsum, congue ac massa ut, condimentum euismod risus. Phasellus volutpat egestas ipsum, id fermentum quam malesuada id. Mauris vitae sagittis lacus, nec tincidunt lorem. Morbi id bibendum ligula, eget sagittis lacus. Aenean elementum euismod ornare. Curabitur rhoncus cursus libero, vel elementum lorem maximus a. In fringilla pharetra placerat. Ut condimentum ante sit amet posuere lacinia. Mauris aliquam quam magna, nec bibendum risus venenatis nec. Sed eu metus eleifend, venenatis tellus quis, facilisis massa. Mauris at ornare risus. Suspendisse justo sem, rutrum non ipsum vitae, eleifend varius nibh. Nullam rutrum eleifend sapien eu eleifend. Vestibulum velit arcu, aliquam vel neque vel, feugiat tincidunt turpis. Donec semper augue at tempus auctor. Donec congue consectetur nunc a feugiat. Pellentesque urna diam, tincidunt eget massa vel, tincidunt ornare tellus. Nunc aliquet nulla eget orci finibus, eu dapibus nunc dictum. Ut in nulla placerat, consequat elit in, rhoncus purus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Cras eu vulputate nisi. In malesuada nisi ut lectus lacinia rutrum. Ut non arcu nec lorem ultricies condimentum. Praesent aliquet pulvinar purus, in tincidunt dui. Nam sit amet sem et turpis iaculis iaculis. Etiam in massa nec ipsum fringilla fringilla. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis sed aliquet massa, eu suscipit felis. Nullam eget gravida tellus, vitae maximus tortor. Aliquam vitae augue arcu. In accumsan tellus eros, sed placerat neque venenatis sed. Ut velit ligula, cursus pellentesque rhoncus sit amet, semper eget eros. Sed sed risus a leo elementum hendrerit. Donec id tortor finibus, cursus diam a, commodo ante. Nam ultricies massa nec nisl dignissim volutpat. Proin porttitor pretium rhoncus. Nunc viverra pretium urna, sit amet commodo arcu sollicitudin at. Ut rhoncus, ante non efficitur auctor, tortor est venenatis lectus, vel maximus augue nisi ac massa. Fusce in risus mauris. Vivamus ullamcorper dui odio. Pellentesque quis tortor eu libero congue porttitor. Vestibulum facilisis augue vitae odio porta, quis viverra dolor accumsan. Proin dignissim elit ac erat laoreet sagittis. Fusce mollis ornare augue non laoreet. Nam varius elit dolor, vitae egestas mi egestas congue. Maecenas sodales rhoncus magna, in cursus eros molestie quis. Vestibulum non enim vel arcu rutrum varius. Suspendisse porta sem erat, quis elementum quam consectetur eget. Sed lectus metus, bibendum lobortis nibh non, mattis egestas justo. Aliquam eget molestie leo. Nullam ultricies ac ipsum ut convallis. In hac habitasse platea dictumst. Suspendisse in interdum eros. Pellentesque ac elementum urna. Proin vulputate vel ligula quis ullamcorper. Curabitur aliquam, metus at vestibulum laoreet, urna massa sollicitudin justo, a efficitur risus orci a justo. Pellentesque id condimentum orci. Morbi est metus, tincidunt rutrum sodales sed, condimentum ac diam. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam facilisis tempus semper. Mauris vulputate nisl in elit semper, vel viverra mauris efficitur. Ut volutpat nisi eu ipsum lacinia molestie. In hac habitasse platea dictumst. Suspendisse ut posuere metus. Sed congue sodales suscipit. Aliquam a ullamcorper nisi. Maecenas vitae nibh porta, tempus libero ut, vehicula metus. Curabitur venenatis mauris ac placerat porta. Pellentesque nulla enim, aliquet eu sollicitudin ac, euismod ut elit. Sed nec urna finibus, maximus nibh eget, rutrum massa. Pellentesque et dui rutrum, ultrices risus nec, aliquet neque. Aliquam erat volutpat. Praesent semper porta tortor, nec euismod eros hendrerit et. Morbi vel vulputate sem. Ut varius quam quam, a mollis lectus fermentum sed. Nunc in volutpat ligula. Ut venenatis ligula vitae pharetra lobortis. Sed vitae eros non diam tincidunt rutrum quis a ipsum. Donec condimentum non est at pretium. Phasellus rhoncus non urna vel volutpat. Duis eleifend nunc id efficitur pharetra. Ut et pellentesque mi, eget pellentesque ex. Mauris volutpat libero lectus, sit amet pretium lectus ultrices ac. Etiam congue ex a nisl aliquet, sed condimentum metus iaculis. Donec non erat euismod, facilisis ligula pulvinar, placerat odio. Aenean eget orci fringilla, laoreet leo eu, semper enim. Aenean sed fermentum mi, ut feugiat quam. Morbi ex ipsum, malesuada ac vestibulum non, elementum sit amet ipsum. Pellentesque elementum urna quis nunc aliquet efficitur non vel est. Nunc ultricies fermentum quam sed luctus. Aliquam maximus mi vestibulum est ornare facilisis. Morbi ut aliquam dolor. Sed efficitur ligula quis odio pulvinar placerat. Vestibulum tellus neque, scelerisque eu posuere id, elementum congue felis. Proin in dolor venenatis tellus blandit consequat ac et mauris. Donec luctus, elit sit amet lacinia efficitur, dolor sapien porttitor nunc, id dignissim leo arcu vitae dolor. Aenean mattis bibendum nibh, vel faucibus neque maximus non. Vivamus mi eros, sodales et nulla quis, tristique tempus ante. Vestibulum sagittis molestie justo nec imperdiet. Sed fermentum ipsum vitae enim auctor porttitor. Aenean facilisis vestibulum tristique. Curabitur ac convallis enim, eu sagittis urna. Aenean rhoncus gravida eros. Ut mollis mi eget metus efficitur malesuada. Aenean lacus nisl, sodales at leo at, tempor pretium tortor. Vestibulum felis tellus, tincidunt a fermentum mattis, aliquam non sapien. Donec ut tortor erat. Maecenas malesuada at eros eget tincidunt. Nam maximus, est et euismod molestie, quam turpis malesuada arcu, sit amet fringilla mauris urna vitae orci. Donec tincidunt dolor risus, eu porta felis maximus at. Morbi placerat eget nibh tempus sodales. Nulla luctus urna et ipsum tempus, at mollis arcu rutrum. Nulla ultrices ligula in felis venenatis porttitor. Mauris vel velit felis. Sed consectetur varius semper. Nulla eu vehicula massa, sit amet euismod eros. Quisque dui tortor, posuere vel tempus non, pellentesque sit amet urna. Ut sit amet ultricies ex. Ut metus dolor, porttitor a lacus ut, volutpat porttitor velit. Suspendisse efficitur erat vel ex aliquam dignissim. Donec vulputate vel magna in convallis. Ut auctor augue neque, aliquet aliquam tellus vulputate nec. Nunc eu rhoncus odio. Phasellus sollicitudin tortor ut libero molestie, in blandit sem sodales. Mauris molestie condimentum pharetra. Duis in lacus scelerisque, pretium urna id, blandit est. Phasellus in ullamcorper leo, a sodales diam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae tincidunt nibh. Aliquam egestas interdum metus et egestas. Suspendisse lacinia a dolor vel cursus. Aliquam tincidunt mauris mauris, vitae posuere mi pharetra eget. Integer congue massa sed ligula condimentum, id sodales justo vulputate. Nunc orci erat, commodo sit amet nisi id, vestibulum tincidunt lectus. Quisque et magna vel elit venenatis porttitor quis facilisis nunc. Etiam sed elit ac lectus hendrerit dapibus vel nec risus. Sed ante mi, finibus eget cursus nec, accumsan sit amet nisl. Vestibulum vulputate leo eget metus tempor finibus. Ut iaculis finibus pretium. Suspendisse ornare dapibus nunc, eu aliquam lacus bibendum in. Aenean risus enim, dictum eget enim vel, pulvinar viverra erat. Aenean iaculis in nibh eu ornare. Sed fermentum orci sed accumsan scelerisque. Suspendisse maximus felis nec nisl consectetur dapibus. Ut eu augue blandit, rhoncus arcu vel, tincidunt turpis. Proin gravida, sapien vitae vulputate ornare, lorem justo lacinia libero, in fringilla nibh elit eget arcu. Quisque convallis dui at odio volutpat, id laoreet nibh faucibus. Vivamus a neque porttitor, ultrices tellus id, facilisis leo. Curabitur dignissim turpis sit amet erat efficitur, sed congue leo sollicitudin. Proin mattis rutrum nisl imperdiet ornare. Proin dignissim eleifend elit, at finibus justo pretium sed. Nulla fermentum, risus nec congue molestie, metus quam euismod est, gravida laoreet arcu ante iaculis nisi. Aenean commodo massa quis dignissim tincidunt. Etiam non congue quam. Vestibulum nunc ipsum, congue ac massa ut, condimentum euismod risus. Phasellus volutpat egestas ipsum, id fermentum quam malesuada id. Mauris vitae sagittis lacus, nec tincidunt lorem. Morbi id bibendum ligula, eget sagittis lacus. Aenean elementum euismod ornare. Curabitur rhoncus cursus libero, vel elementum lorem maximus a. In fringilla pharetra placerat. Ut condimentum ante sit amet posuere lacinia. Mauris aliquam quam magna, nec bibendum risus venenatis nec. Sed eu metus eleifend, venenatis tellus quis, facilisis massa. Mauris at ornare risus. Suspendisse justo sem, rutrum non ipsum vitae, eleifend varius nibh. Nullam rutrum eleifend sapien eu eleifend. Vestibulum velit arcu, aliquam vel neque vel, feugiat tincidunt turpis. Donec semper augue at tempus auctor. Donec congue consectetur nunc a feugiat. Pellentesque urna diam, tincidunt eget massa vel, tincidunt ornare tellus. Nunc aliquet nulla eget orci finibus, eu dapibus nunc dictum. Ut in nulla placerat, consequat elit in, rhoncus purus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Cras eu vulputate nisi. In malesuada nisi ut lectus lacinia rutrum. Ut non arcu nec lorem ultricies condimentum. Praesent aliquet pulvinar purus, in tincidunt dui. Nam sit amet sem et turpis iaculis iaculis. Etiam in massa nec ipsum fringilla fringilla. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis sed aliquet massa, eu suscipit felis. Nullam eget gravida tellus, vitae maximus tortor. Aliquam vitae augue arcu. In accumsan tellus eros, sed placerat neque venenatis sed. Ut velit ligula, cursus pellentesque rhoncus sit amet, semper eget eros. Sed sed risus a leo elementum hendrerit. Donec id tortor finibus, cursus diam a, commodo ante. Nam ultricies massa nec nisl dignissim volutpat. Proin porttitor pretium rhoncus. Nunc viverra pretium urna, sit amet commodo arcu sollicitudin at. Ut rhoncus, ante non efficitur auctor, tortor est venenatis lectus, vel maximus augue nisi ac massa. Fusce in risus mauris. Vivamus ullamcorper dui odio. Pellentesque quis tortor eu libero congue porttitor. Vestibulum facilisis augue vitae odio porta, quis viverra dolor accumsan. Proin dignissim elit ac erat laoreet sagittis. Fusce mollis ornare augue non laoreet. Nam varius elit dolor, vitae egestas mi egestas congue. Maecenas sodales rhoncus magna, in cursus eros molestie quis. Vestibulum non enim vel arcu rutrum varius. Suspendisse porta sem erat, quis elementum quam consectetur eget. Sed lectus metus, bibendum lobortis nibh non, mattis egestas justo. Aliquam eget molestie leo. Nullam ultricies ac ipsum ut convallis. In hac habitasse platea dictumst. Suspendisse in interdum eros. Pellentesque ac elementum urna. Proin vulputate vel ligula quis ullamcorper. Curabitur aliquam, metus at vestibulum laoreet, urna massa sollicitudin justo, a efficitur risus orci a justo. Pellentesque id condimentum orci. Morbi est metus, tincidunt rutrum sodales sed, condimentum ac diam. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam facilisis tempus semper. Mauris vulputate nisl in elit semper, vel viverra mauris efficitur. Ut volutpat nisi eu ipsum lacinia molestie. In hac habitasse platea dictumst. Suspendisse ut posuere metus. Sed congue sodales suscipit. Aliquam a ullamcorper nisi. Maecenas vitae nibh porta, tempus libero ut, vehicula metus. Curabitur venenatis mauris ac placerat porta. Pellentesque nulla enim, aliquet eu sollicitudin ac, euismod ut elit. Sed nec urna finibus, maximus nibh eget, rutrum massa. Pellentesque et dui rutrum, ultrices risus nec, aliquet neque. Aliquam erat volutpat. Praesent semper porta tortor, nec euismod eros hendrerit et. Morbi vel vulputate sem. Ut varius quam quam, a mollis lectus fermentum sed. Nunc in volutpat ligula. Ut venenatis ligula vitae pharetra lobortis. Sed vitae eros non diam tincidunt rutrum quis a ipsum. Donec condimentum non est at pretium. Phasellus rhoncus non urna vel volutpat. Duis eleifend nunc id efficitur pharetra. Ut et pellentesque mi, eget pellentesque ex. Mauris volutpat libero lectus, sit amet pretium lectus ultrices ac. Etiam congue ex a nisl aliquet, sed condimentum metus iaculis. Donec non erat euismod, facilisis ligula pulvinar, placerat odio. Aenean eget orci fringilla, laoreet leo eu, semper enim. Aenean sed fermentum mi, ut feugiat quam. Morbi ex ipsum, malesuada ac vestibulum non, elementum sit amet ipsum. Pellentesque elementum urna quis nunc aliquet efficitur non vel est. Nunc ultricies fermentum quam sed luctus. Aliquam maximus mi vestibulum est ornare facilisis. Morbi ut aliquam dolor. Sed efficitur ligula quis odio pulvinar placerat. Vestibulum tellus neque, scelerisque eu posuere id, elementum congue felis. Proin in dolor venenatis tellus blandit consequat ac et mauris. Donec luctus, elit sit amet lacinia efficitur, dolor sapien porttitor nunc, id dignissim leo arcu vitae dolor. Aenean mattis bibendum nibh, vel faucibus neque maximus non. Vivamus mi eros, sodales et nulla quis, tristique tempus ante. Vestibulum sagittis molestie justo nec imperdiet. Sed fermentum ipsum vitae enim auctor porttitor. Aenean facilisis vestibulum tristique. Curabitur ac convallis enim, eu sagittis urna. Aenean rhoncus gravida eros. Ut mollis mi eget metus efficitur malesuada. Aenean lacus nisl, sodales at leo at, tempor pretium tortor. Vestibulum felis tellus, tincidunt a fermentum mattis, aliquam non sapien. Donec ut tortor erat. Maecenas malesuada at eros eget tincidunt. Nam maximus, est et euismod molestie, quam turpis malesuada arcu, sit amet fringilla mauris urna vitae orci. Donec tincidunt dolor risus, eu porta felis maximus at. Morbi placerat eget nibh tempus sodales. Nulla luctus urna et ipsum tempus, at mollis arcu rutrum. Nulla ultrices ligula in felis venenatis porttitor. Mauris vel velit felis. Sed consectetur varius semper. Nulla eu vehicula massa, sit amet euismod eros. Quisque dui tortor, posuere vel tempus non, pellentesque sit amet urna. Ut sit amet ultricies ex. Ut metus dolor, porttitor a lacus ut, volutpat porttitor velit. Suspendisse efficitur erat vel ex aliquam dignissim. Donec vulputate vel magna in convallis. Ut auctor augue neque, aliquet aliquam tellus vulputate nec. Nunc eu rhoncus odio. Phasellus sollicitudin tortor ut libero molestie, in blandit sem sodales. Mauris molestie condimentum pharetra. Duis in lacus scelerisque, pretium urna id, blandit est. Phasellus in ullamcorper leo, a sodales diam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae tincidunt nibh. Aliquam egestas interdum metus et egestas. Suspendisse lacinia a dolor vel cursus. Aliquam tincidunt mauris mauris, vitae posuere mi pharetra eget. Integer congue massa sed ligula condimentum, id sodales justo vulputate. Nunc orci erat, commodo sit amet nisi id, vestibulum tincidunt lectus. Quisque et magna vel elit venenatis porttitor quis facilisis nunc. Etiam sed elit ac lectus hendrerit dapibus vel nec risus. Sed ante mi, finibus eget cursus nec, accumsan sit amet nisl. Vestibulum vulputate leo eget metus tempor finibus. Ut iaculis finibus pretium. Suspendisse ornare dapibus nunc, eu aliquam lacus bibendum in. Aenean risus enim, dictum eget enim vel, pulvinar viverra erat. Aenean iaculis in nibh eu ornare. Sed fermentum orci sed accumsan scelerisque. Suspendisse maximus felis nec nisl consectetur dapibus. Ut eu augue blandit, rhoncus arcu vel, tincidunt turpis. Proin gravida, sapien vitae vulputate ornare, lorem justo lacinia libero, in fringilla nibh elit eget arcu. Quisque convallis dui at odio volutpat, id laoreet nibh faucibus. Vivamus a neque porttitor, ultrices tellus id, facilisis leo. Curabitur dignissim turpis sit amet erat efficitur, sed congue leo sollicitudin. Proin mattis rutrum nisl imperdiet ornare. Proin dignissim eleifend elit, at finibus justo pretium sed. Nulla fermentum, risus nec congue molestie, metus quam euismod est, gravida laoreet arcu ante iaculis nisi. Aenean commodo massa quis dignissim tincidunt. Etiam non congue quam. Vestibulum nunc ipsum, congue ac massa ut, condimentum euismod risus. Phasellus volutpat egestas ipsum, id fermentum quam malesuada id. Mauris vitae sagittis lacus, nec tincidunt lorem. Morbi id bibendum ligula, eget sagittis lacus. Aenean elementum euismod ornare. Curabitur rhoncus cursus libero, vel elementum lorem maximus a. In fringilla pharetra placerat. Ut condimentum ante sit amet posuere lacinia. Mauris aliquam quam magna, nec bibendum risus venenatis nec. Sed eu metus eleifend, venenatis tellus quis, facilisis massa. Mauris at ornare risus. Suspendisse justo sem, rutrum non ipsum vitae, eleifend varius nibh. Nullam rutrum eleifend sapien eu eleifend. Vestibulum velit arcu, aliquam vel neque vel, feugiat tincidunt turpis. Donec semper augue at tempus auctor. Donec congue consectetur nunc a feugiat. Pellentesque urna diam, tincidunt eget massa vel, tincidunt ornare tellus. Nunc aliquet nulla eget orci finibus, eu dapibus nunc dictum. Ut in nulla placerat, consequat elit in, rhoncus purus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Cras eu vulputate nisi. In malesuada nisi ut lectus lacinia rutrum. Ut non arcu nec lorem ultricies condimentum. Praesent aliquet pulvinar purus, in tincidunt dui. Nam sit amet sem et turpis iaculis iaculis. Etiam in massa nec ipsum fringilla fringilla. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis sed aliquet massa, eu suscipit felis. Nullam eget gravida tellus, vitae maximus tortor. Aliquam vitae augue arcu. In accumsan tellus eros, sed placerat neque venenatis sed. Ut velit ligula, cursus pellentesque rhoncus sit amet, semper eget eros. Sed sed risus a leo elementum hendrerit. Donec id tortor finibus, cursus diam a, commodo ante. Nam ultricies massa nec nisl dignissim volutpat. Proin porttitor pretium rhoncus. Nunc viverra pretium urna, sit amet commodo arcu sollicitudin at. Ut rhoncus, ante non efficitur auctor, tortor est venenatis lectus, vel maximus augue nisi ac massa. Fusce in risus mauris. Vivamus ullamcorper dui odio. Pellentesque quis tortor eu libero congue porttitor. Vestibulum facilisis augue vitae odio porta, quis viverra dolor accumsan. Proin dignissim elit ac erat laoreet sagittis. Fusce mollis ornare augue non laoreet. Nam varius elit dolor, vitae egestas mi egestas congue. Maecenas sodales rhoncus magna, in cursus eros molestie quis. Vestibulum non enim vel arcu rutrum varius. Suspendisse porta sem erat, quis elementum quam consectetur eget. Sed lectus metus, bibendum lobortis nibh non, mattis egestas justo. Aliquam eget molestie leo. Nullam ultricies ac ipsum ut convallis. In hac habitasse platea dictumst. Suspendisse in interdum eros. Pellentesque ac elementum urna. Proin vulputate vel ligula quis ullamcorper. Curabitur aliquam, metus at vestibulum laoreet, urna massa sollicitudin justo, a efficitur risus orci a justo. Pellentesque id condimentum orci. Morbi est metus, tincidunt rutrum sodales sed, condimentum ac diam. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam facilisis tempus semper. Mauris vulputate nisl in elit semper, vel viverra mauris efficitur. Ut volutpat nisi eu ipsum lacinia molestie. In hac habitasse platea dictumst. Suspendisse ut posuere metus. Sed congue sodales suscipit. Aliquam a ullamcorper nisi. Maecenas vitae nibh porta, tempus libero ut, vehicula metus. Curabitur venenatis mauris ac placerat porta. Pellentesque nulla enim, aliquet eu sollicitudin ac, euismod ut elit. Sed nec urna finibus, maximus nibh eget, rutrum massa. Pellentesque et dui rutrum, ultrices risus nec, aliquet neque. Aliquam erat volutpat. Praesent semper porta tortor, nec euismod eros hendrerit et. Morbi vel vulputate sem. Ut varius quam quam, a mollis lectus fermentum sed. Nunc in volutpat ligula. Ut venenatis ligula vitae pharetra lobortis. Sed vitae eros non diam tincidunt rutrum quis a ipsum. Donec condimentum non est at pretium. Phasellus rhoncus non urna vel volutpat. Duis eleifend nunc id efficitur pharetra. Ut et pellentesque mi, eget pellentesque ex. Mauris volutpat libero lectus, sit amet pretium lectus ultrices ac. Etiam congue ex a nisl aliquet, sed condimentum metus iaculis. Donec non erat euismod, facilisis ligula pulvinar, placerat odio. Aenean eget orci fringilla, laoreet leo eu, semper enim. Aenean sed fermentum mi, ut feugiat quam. Morbi ex ipsum, malesuada ac vestibulum non, elementum sit amet ipsum. Pellentesque elementum urna quis nunc aliquet efficitur non vel est. Nunc ultricies fermentum quam sed luctus. Aliquam maximus mi vestibulum est ornare facilisis. Morbi ut aliquam dolor. Sed efficitur ligula quis odio pulvinar placerat. Vestibulum tellus neque, scelerisque eu posuere id, elementum congue felis. Proin in dolor venenatis tellus blandit consequat ac et mauris. Donec luctus, elit sit amet lacinia efficitur, dolor sapien porttitor nunc, id dignissim leo arcu vitae dolor. Aenean mattis bibendum nibh, vel faucibus neque maximus non. Vivamus mi eros, sodales et nulla quis, tristique tempus ante. Vestibulum sagittis molestie justo nec imperdiet. Sed fermentum ipsum vitae enim auctor porttitor. Aenean facilisis vestibulum tristique. Curabitur ac convallis enim, eu sagittis urna. Aenean rhoncus gravida eros. Ut mollis mi eget metus efficitur malesuada. Aenean lacus nisl, sodales at leo at, tempor pretium tortor. Vestibulum felis tellus, tincidunt a fermentum mattis, aliquam non sapien. Donec ut tortor erat. Maecenas malesuada at eros eget tincidunt. Nam maximus, est et euismod molestie, quam turpis malesuada arcu, sit amet fringilla mauris urna vitae orci. Donec tincidunt dolor risus, eu porta felis maximus at. Morbi placerat eget nibh tempus sodales. Nulla luctus urna et ipsum tempus, at mollis arcu rutrum. Nulla ultrices ligula in felis venenatis porttitor. Mauris vel velit felis. Sed consectetur varius semper. Nulla eu vehicula massa, sit amet euismod eros. Quisque dui tortor, posuere vel tempus non, pellentesque sit amet urna. Ut sit amet ultricies ex. Ut metus dolor, porttitor a lacus ut, volutpat porttitor velit. Suspendisse efficitur erat vel ex aliquam dignissim. Donec vulputate vel magna in convallis. Ut auctor augue neque, aliquet aliquam tellus vulputate nec. Nunc eu rhoncus odio. Phasellus sollicitudin tortor ut libero molestie, in blandit sem sodales. Mauris molestie condimentum pharetra. Duis in lacus scelerisque, pretium urna id, blandit est. Phasellus in ullamcorper leo, a sodales diam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae tincidunt nibh. Aliquam egestas interdum metus et egestas. Suspendisse lacinia a dolor vel cursus. Aliquam tincidunt mauris mauris, vitae posuere mi pharetra eget. Integer congue massa sed ligula condimentum, id sodales justo vulputate. Nunc orci erat, commodo sit amet nisi id, vestibulum tincidunt lectus. Quisque et magna vel elit venenatis porttitor quis facilisis nunc. Etiam sed elit ac lectus hendrerit dapibus vel nec risus. Sed ante mi, finibus eget cursus nec, accumsan sit amet nisl. Vestibulum vulputate leo eget metus tempor finibus. Ut iaculis finibus pretium. Suspendisse ornare dapibus nunc, eu aliquam lacus bibendum in. Aenean risus enim, dictum eget enim vel, pulvinar viverra erat. Aenean iaculis in nibh eu ornare. Sed fermentum orci sed accumsan scelerisque. Suspendisse maximus felis nec nisl consectetur dapibus. Ut eu augue blandit, rhoncus arcu vel, tincidunt turpis. Proin gravida, sapien vitae vulputate ornare, lorem justo lacinia libero, in fringilla nibh elit eget arcu. Quisque convallis dui at odio volutpat, id laoreet nibh faucibus. Vivamus a neque porttitor, ultrices tellus id, facilisis leo. Curabitur dignissim turpis sit amet erat efficitur, sed congue leo sollicitudin. Proin mattis rutrum nisl imperdiet ornare. Proin dignissim eleifend elit, at finibus justo pretium sed. Nulla fermentum, risus nec congue molestie, metus quam euismod est, gravida laoreet arcu ante iaculis nisi. Aenean commodo massa quis dignissim tincidunt. Etiam non congue quam. Vestibulum nunc ipsum, congue ac massa ut, condimentum euismod risus. Phasellus volutpat egestas ipsum, id fermentum quam malesuada id. Mauris vitae sagittis lacus, nec tincidunt lorem. Morbi id bibendum ligula, eget sagittis lacus. Aenean elementum euismod ornare. Curabitur rhoncus cursus libero, vel elementum lorem maximus a. In fringilla pharetra placerat. Ut condimentum ante sit amet posuere lacinia. Mauris aliquam quam magna, nec bibendum risus venenatis nec. Sed eu metus eleifend, venenatis tellus quis, facilisis massa. Mauris at ornare risus. Suspendisse justo sem, rutrum non ipsum vitae, eleifend varius nibh. Nullam rutrum eleifend sapien eu eleifend. Vestibulum velit arcu, aliquam vel neque vel, feugiat tincidunt turpis. Donec semper augue at tempus auctor. Donec congue consectetur nunc a feugiat. Pellentesque urna diam, tincidunt eget massa vel, tincidunt ornare tellus. Nunc aliquet nulla eget orci finibus, eu dapibus nunc dictum. Ut in nulla placerat, consequat elit in, rhoncus purus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Cras eu vulputate nisi. In malesuada nisi ut lectus lacinia rutrum. Ut non arcu nec lorem ultricies condimentum. Praesent aliquet pulvinar purus, in tincidunt dui. Nam sit amet sem et turpis iaculis iaculis. Etiam in massa nec ipsum fringilla fringilla. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis sed aliquet massa, eu suscipit felis. Nullam eget gravida tellus, vitae maximus tortor. Aliquam vitae augue arcu. In accumsan tellus eros, sed placerat neque venenatis sed. Ut velit ligula, cursus pellentesque rhoncus sit amet, semper eget eros. Sed sed risus a leo elementum hendrerit. Donec id tortor finibus, cursus diam a, commodo ante. Nam ultricies massa nec nisl dignissim volutpat. Proin porttitor pretium rhoncus. Nunc viverra pretium urna, sit amet commodo arcu sollicitudin at. Ut rhoncus, ante non efficitur auctor, tortor est venenatis lectus, vel maximus augue nisi ac massa. Fusce in risus mauris. Vivamus ullamcorper dui odio. Pellentesque quis tortor eu libero congue porttitor. Vestibulum facilisis augue vitae odio porta, quis viverra dolor accumsan. Proin dignissim elit ac erat laoreet sagittis. Fusce mollis ornare augue non laoreet. Nam varius elit dolor, vitae egestas mi egestas congue. Maecenas sodales rhoncus magna, in cursus eros molestie quis. Vestibulum non enim vel arcu rutrum varius. Suspendisse porta sem erat, quis elementum quam consectetur eget. Sed lectus metus, bibendum lobortis nibh non, mattis egestas justo. Aliquam eget molestie leo. Nullam ultricies ac ipsum ut convallis. In hac habitasse platea dictumst. Suspendisse in interdum eros. Pellentesque ac elementum urna. Proin vulputate vel ligula quis ullamcorper. Curabitur aliquam, metus at vestibulum laoreet, urna massa sollicitudin justo, a efficitur risus orci a justo. Pellentesque id condimentum orci. Morbi est metus, tincidunt rutrum sodales sed, condimentum ac diam. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam facilisis tempus semper. Mauris vulputate nisl in elit semper, vel viverra mauris efficitur. Ut volutpat nisi eu ipsum lacinia molestie. In hac habitasse platea dictumst. Suspendisse ut posuere metus. Sed congue sodales suscipit. Aliquam a ullamcorper nisi. Maecenas vitae nibh porta, tempus libero ut, vehicula metus. Curabitur venenatis mauris ac placerat porta. Pellentesque nulla enim, aliquet eu sollicitudin ac, euismod ut elit. Sed nec urna finibus, maximus nibh eget, rutrum massa. Pellentesque et dui rutrum, ultrices risus nec, aliquet neque. Aliquam erat volutpat. Praesent semper porta tortor, nec euismod eros hendrerit et. Morbi vel vulputate sem. Ut varius quam quam, a mollis lectus fermentum sed. Nunc in volutpat ligula. Ut venenatis ligula vitae pharetra lobortis. Sed vitae eros non diam tincidunt rutrum quis a ipsum. Donec condimentum non est at pretium. Phasellus rhoncus non urna vel volutpat. Duis eleifend nunc id efficitur pharetra. Ut et pellentesque mi, eget pellentesque ex. Mauris volutpat libero lectus, sit amet pretium lectus ultrices ac. Etiam congue ex a nisl aliquet, sed condimentum metus iaculis. Donec non erat euismod, facilisis ligula pulvinar, placerat odio. Aenean eget orci fringilla, laoreet leo eu, semper enim. Aenean sed fermentum mi, ut feugiat quam. Morbi ex ipsum, malesuada ac vestibulum non, elementum sit amet ipsum. Pellentesque elementum urna quis nunc aliquet efficitur non vel est. Nunc ultricies fermentum quam sed luctus. Aliquam maximus mi vestibulum est ornare facilisis. Morbi ut aliquam dolor. Sed efficitur ligula quis odio pulvinar placerat. Vestibulum tellus neque, scelerisque eu posuere id, elementum congue felis. Proin in dolor venenatis tellus blandit consequat ac et mauris. Donec luctus, elit sit amet lacinia efficitur, dolor sapien porttitor nunc, id dignissim leo arcu vitae dolor. Aenean mattis bibendum nibh, vel faucibus neque maximus non. Vivamus mi eros, sodales et nulla quis, tristique tempus ante. Vestibulum sagittis molestie justo nec imperdiet. Sed fermentum ipsum vitae enim auctor porttitor. Aenean facilisis vestibulum tristique. Curabitur ac convallis enim, eu sagittis urna. Aenean rhoncus gravida eros. Ut mollis mi eget metus efficitur malesuada. Aenean lacus nisl, sodales at leo at, tempor pretium tortor. Vestibulum felis tellus, tincidunt a fermentum mattis, aliquam non sapien. Donec ut tortor erat. Maecenas malesuada at eros eget tincidunt. Nam maximus, est et euismod molestie, quam turpis malesuada arcu, sit amet fringilla mauris urna vitae orci. Donec tincidunt dolor risus, eu porta felis maximus at. Morbi placerat eget nibh tempus sodales. Nulla luctus urna et ipsum tempus, at mollis arcu rutrum. Nulla ultrices ligula in felis venenatis porttitor. Mauris vel velit felis. Sed consectetur varius semper. Nulla eu vehicula massa, sit amet euismod eros. Quisque dui tortor, posuere vel tempus non, pellentesque sit amet urna. Ut sit amet ultricies ex. Ut metus dolor, porttitor a lacus ut, volutpat porttitor velit. Suspendisse efficitur erat vel ex aliquam dignissim. Donec vulputate vel magna in convallis. Ut auctor augue neque, aliquet aliquam tellus vulputate nec. Nunc eu rhoncus odio. Phasellus sollicitudin tortor ut libero molestie, in blandit sem sodales. Mauris molestie condimentum pharetra. Duis in lacus scelerisque, pretium urna id, blandit est. Phasellus in ullamcorper leo, a sodales diam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae tincidunt nibh. Aliquam egestas interdum metus et egestas. Suspendisse lacinia a dolor vel cursus. Aliquam tincidunt mauris mauris, vitae posuere mi pharetra eget. Integer congue massa sed ligula condimentum, id sodales justo vulputate. Nunc orci erat, commodo sit amet nisi id, vestibulum tincidunt lectus. Quisque et magna vel elit venenatis porttitor quis facilisis nunc. Etiam sed elit ac lectus hendrerit dapibus vel nec risus. Sed ante mi, finibus eget cursus nec, accumsan sit amet nisl. Vestibulum vulputate leo eget metus tempor finibus. Ut iaculis finibus pretium. Suspendisse ornare dapibus nunc, eu aliquam lacus bibendum in. Aenean risus enim, dictum eget enim vel, pulvinar viverra erat. Aenean iaculis in nibh eu ornare. Sed fermentum orci sed accumsan scelerisque. Suspendisse maximus felis nec nisl consectetur dapibus. Ut eu augue blandit, rhoncus arcu vel, tincidunt turpis. Proin gravida, sapien vitae vulputate ornare, lorem justo lacinia libero, in fringilla nibh elit eget arcu. Quisque convallis dui at odio volutpat, id laoreet nibh faucibus. Vivamus a neque porttitor, ultrices tellus id, facilisis leo. Curabitur dignissim turpis sit amet erat efficitur, sed congue leo sollicitudin. Proin mattis rutrum nisl imperdiet ornare. Proin dignissim eleifend elit, at finibus justo pretium sed. Nulla fermentum, risus nec congue molestie, metus quam euismod est, gravida laoreet arcu ante iaculis nisi. Aenean commodo massa quis dignissim tincidunt. Etiam non congue quam. Vestibulum nunc ipsum, congue ac massa ut, condimentum euismod risus. Phasellus volutpat egestas ipsum, id fermentum quam malesuada id. Mauris vitae sagittis lacus, nec tincidunt lorem. Morbi id bibendum ligula, eget sagittis lacus. Aenean elementum euismod ornare. Curabitur rhoncus cursus libero, vel elementum lorem maximus a. In fringilla pharetra placerat. Ut condimentum ante sit amet posuere lacinia. Mauris aliquam quam magna, nec bibendum risus venenatis nec. Sed eu metus eleifend, venenatis tellus quis, facilisis massa. Mauris at ornare risus. Suspendisse justo sem, rutrum non ipsum vitae, eleifend varius nibh. Nullam rutrum eleifend sapien eu eleifend. Vestibulum velit arcu, aliquam vel neque vel, feugiat tincidunt turpis. Donec semper augue at tempus auctor. Donec congue consectetur nunc a feugiat. Pellentesque urna diam, tincidunt eget massa vel, tincidunt ornare tellus. Nunc aliquet nulla eget orci finibus, eu dapibus nunc dictum. Ut in nulla placerat, consequat elit in, rhoncus purus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Cras eu vulputate nisi. In malesuada nisi ut lectus lacinia rutrum. Ut non arcu nec lorem ultricies condimentum. Praesent aliquet pulvinar purus, in tincidunt dui. Nam sit amet sem et turpis iaculis iaculis. Etiam in massa nec ipsum fringilla fringilla. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis sed aliquet massa, eu suscipit felis. Nullam eget gravida tellus, vitae maximus tortor. Aliquam vitae augue arcu. In accumsan tellus eros, sed placerat neque venenatis sed. Ut velit ligula, cursus pellentesque rhoncus sit amet, semper eget eros. Sed sed risus a leo elementum hendrerit. Donec id tortor finibus, cursus diam a, commodo ante. Nam ultricies massa nec nisl dignissim volutpat. Proin porttitor pretium rhoncus. Nunc viverra pretium urna, sit amet commodo arcu sollicitudin at. Ut rhoncus, ante non efficitur auctor, tortor est venenatis lectus, vel maximus augue nisi ac massa. Fusce in risus mauris. Vivamus ullamcorper dui odio. Pellentesque quis tortor eu libero congue porttitor. Vestibulum facilisis augue vitae odio porta, quis viverra dolor accumsan. Proin dignissim elit ac erat laoreet sagittis. Fusce mollis ornare augue non laoreet. Nam varius elit dolor, vitae egestas mi egestas congue. Maecenas sodales rhoncus magna, in cursus eros molestie quis. Vestibulum non enim vel arcu rutrum varius. Suspendisse porta sem erat, quis elementum quam consectetur eget. Sed lectus metus, bibendum lobortis nibh non, mattis egestas justo. Aliquam eget molestie leo. Nullam ultricies ac ipsum ut convallis. In hac habitasse platea dictumst. Suspendisse in interdum eros. Pellentesque ac elementum urna. Proin vulputate vel ligula quis ullamcorper. Curabitur aliquam, metus at vestibulum laoreet, urna massa sollicitudin justo, a efficitur risus orci a justo. Pellentesque id condimentum orci. Morbi est metus, tincidunt rutrum sodales sed, condimentum ac diam. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam facilisis tempus semper. Mauris vulputate nisl in elit semper, vel viverra mauris efficitur. Ut volutpat nisi eu ipsum lacinia molestie. In hac habitasse platea dictumst. Suspendisse ut posuere metus. Sed congue sodales suscipit. Aliquam a ullamcorper nisi. Maecenas vitae nibh porta, tempus libero ut, vehicula metus. Curabitur venenatis mauris ac placerat porta. Pellentesque nulla enim, aliquet eu sollicitudin ac, euismod ut elit. Sed nec urna finibus, maximus nibh eget, rutrum massa. Pellentesque et dui rutrum, ultrices risus nec, aliquet neque. Aliquam erat volutpat. Praesent semper porta tortor, nec euismod eros hendrerit et. Morbi vel vulputate sem. Ut varius quam quam, a mollis lectus fermentum sed. Nunc in volutpat ligula. Ut venenatis ligula vitae pharetra lobortis. Sed vitae eros non diam tincidunt rutrum quis a ipsum. Donec condimentum non est at pretium. Phasellus rhoncus non urna vel volutpat. Duis eleifend nunc id efficitur pharetra. Ut et pellentesque mi, eget pellentesque ex. Mauris volutpat libero lectus, sit amet pretium lectus ultrices ac. Etiam congue ex a nisl aliquet, sed condimentum metus iaculis. Donec non erat euismod, facilisis ligula pulvinar, placerat odio. Aenean eget orci fringilla, laoreet leo eu, semper enim. Aenean sed fermentum mi, ut feugiat quam. Morbi ex ipsum, malesuada ac vestibulum non, elementum sit amet ipsum. Pellentesque elementum urna quis nunc aliquet efficitur non vel est. Nunc ultricies fermentum quam sed luctus. Aliquam maximus mi vestibulum est ornare facilisis. Morbi ut aliquam dolor. Sed efficitur ligula quis odio pulvinar placerat. Vestibulum tellus neque, scelerisque eu posuere id, elementum congue felis. Proin in dolor venenatis tellus blandit consequat ac et mauris. Donec luctus, elit sit amet lacinia efficitur, dolor sapien porttitor nunc, id dignissim leo arcu vitae dolor. Aenean mattis bibendum nibh, vel faucibus neque maximus non. Vivamus mi eros, sodales et nulla quis, tristique tempus ante. Vestibulum sagittis molestie justo nec imperdiet. Sed fermentum ipsum vitae enim auctor porttitor. Aenean facilisis vestibulum tristique. Curabitur ac convallis enim, eu sagittis urna. Aenean rhoncus gravida eros. Ut mollis mi eget metus efficitur malesuada. Aenean lacus nisl, sodales at leo at, tempor pretium tortor. Vestibulum felis tellus, tincidunt a fermentum mattis, aliquam non sapien. Donec ut tortor erat. Maecenas malesuada at eros eget tincidunt. Nam maximus, est et euismod molestie, quam turpis malesuada arcu, sit amet fringilla mauris urna vitae orci. Donec tincidunt dolor risus, eu porta felis maximus at. Morbi placerat eget nibh tempus sodales. Nulla luctus urna et ipsum tempus, at mollis arcu rutrum. Nulla ultrices ligula in felis venenatis porttitor. Mauris vel velit felis. Sed consectetur varius semper. Nulla eu vehicula massa, sit amet euismod eros. Quisque dui tortor, posuere vel tempus non, pellentesque sit amet urna. Ut sit amet ultricies ex. Ut metus dolor, porttitor a lacus ut, volutpat porttitor velit. Suspendisse efficitur erat vel ex aliquam dignissim. Donec vulputate vel magna in convallis. Ut auctor augue neque, aliquet aliquam tellus vulputate nec. Nunc eu rhoncus odio. Phasellus sollicitudin tortor ut libero molestie, in blandit sem sodales. Mauris molestie condimentum pharetra. Duis in lacus scelerisque, pretium urna id, blandit est. Phasellus in ullamcorper leo, a sodales diam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae tincidunt nibh. Aliquam egestas interdum metus et egestas. Suspendisse lacinia a dolor vel cursus. Aliquam tincidunt mauris mauris, vitae posuere mi pharetra eget. Integer congue massa sed ligula condimentum, id sodales justo vulputate. Nunc orci erat, commodo sit amet nisi id, vestibulum tincidunt lectus. Quisque et magna vel elit venenatis porttitor quis facilisis nunc. Etiam sed elit ac lectus hendrerit dapibus vel nec risus. Sed ante mi, finibus eget cursus nec, accumsan sit amet nisl. Vestibulum vulputate leo eget metus tempor finibus. Ut iaculis finibus pretium. Suspendisse ornare dapibus nunc, eu aliquam lacus bibendum in. Aenean risus enim, dictum eget enim vel, pulvinar viverra erat. Aenean iaculis in nibh eu ornare. Sed fermentum orci sed accumsan scelerisque. Suspendisse maximus felis nec nisl consectetur dapibus. Ut eu augue blandit, rhoncus arcu vel, tincidunt turpis. Proin gravida, sapien vitae vulputate ornare, lorem justo lacinia libero, in fringilla nibh elit eget arcu. Quisque convallis dui at odio volutpat, id laoreet nibh faucibus. Vivamus a neque porttitor, ultrices tellus id, facilisis leo. Curabitur dignissim turpis sit amet erat efficitur, sed congue leo sollicitudin. Proin mattis rutrum nisl imperdiet ornare. Proin dignissim eleifend elit, at finibus justo pretium sed. Nulla fermentum, risus nec congue molestie, metus quam euismod est, gravida laoreet arcu ante iaculis nisi. Aenean commodo massa quis dignissim tincidunt. Etiam non congue quam. Vestibulum nunc ipsum, congue ac massa ut, condimentum euismod risus. Phasellus volutpat egestas ipsum, id fermentum quam malesuada id. Mauris vitae sagittis lacus, nec tincidunt lorem. Morbi id bibendum ligula, eget sagittis lacus. Aenean elementum euismod ornare. Curabitur rhoncus cursus libero, vel elementum lorem maximus a. In fringilla pharetra placerat. Ut condimentum ante sit amet posuere lacinia. Mauris aliquam quam magna, nec bibendum risus venenatis nec. Sed eu metus eleifend, venenatis tellus quis, facilisis massa. Mauris at ornare risus. Suspendisse justo sem, rutrum non ipsum vitae, eleifend varius nibh. Nullam rutrum eleifend sapien eu eleifend. Vestibulum velit arcu, aliquam vel neque vel, feugiat tincidunt turpis. Donec semper augue at tempus auctor. Donec congue consectetur nunc a feugiat. Pellentesque urna diam, tincidunt eget massa vel, tincidunt ornare tellus. Nunc aliquet nulla eget orci finibus, eu dapibus nunc dictum. Ut in nulla placerat, consequat elit in, rhoncus purus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Cras eu vulputate nisi. In malesuada nisi ut lectus lacinia rutrum. Ut non arcu nec lorem ultricies condimentum. Praesent aliquet pulvinar purus, in tincidunt dui. Nam sit amet sem et turpis iaculis iaculis. Etiam in massa nec ipsum fringilla fringilla. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis sed aliquet massa, eu suscipit felis. Nullam eget gravida tellus, vitae maximus tortor. Aliquam vitae augue arcu. In accumsan tellus eros, sed placerat neque venenatis sed. Ut velit ligula, cursus pellentesque rhoncus sit amet, semper eget eros. Sed sed risus a leo elementum hendrerit. Donec id tortor finibus, cursus diam a, commodo ante. Nam ultricies massa nec nisl dignissim volutpat. Proin porttitor pretium rhoncus. Nunc viverra pretium urna, sit amet commodo arcu sollicitudin at. Ut rhoncus, ante non efficitur auctor, tortor est venenatis lectus, vel maximus augue nisi ac massa. Fusce in risus mauris. Vivamus ullamcorper dui odio. Pellentesque quis tortor eu libero congue porttitor. Vestibulum facilisis augue vitae odio porta, quis viverra dolor accumsan. Proin dignissim elit ac erat laoreet sagittis. Fusce mollis ornare augue non laoreet. Nam varius elit dolor, vitae egestas mi egestas congue. Maecenas sodales rhoncus magna, in cursus eros molestie quis. Vestibulum non enim vel arcu rutrum varius. Suspendisse porta sem erat, quis elementum quam consectetur eget. Sed lectus metus, bibendum lobortis nibh non, mattis egestas justo. Aliquam eget molestie leo. Nullam ultricies ac ipsum ut convallis. In hac habitasse platea dictumst. Suspendisse in interdum eros. Pellentesque ac elementum urna. Proin vulputate vel ligula quis ullamcorper. Curabitur aliquam, metus at vestibulum laoreet, urna massa sollicitudin justo, a efficitur risus orci a justo. Pellentesque id condimentum orci. Morbi est metus, tincidunt rutrum sodales sed, condimentum ac diam. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam facilisis tempus semper. Mauris vulputate nisl in elit semper, vel viverra mauris efficitur. Ut volutpat nisi eu ipsum lacinia molestie. In hac habitasse platea dictumst. Suspendisse ut posuere metus. Sed congue sodales suscipit. Aliquam a ullamcorper nisi. Maecenas vitae nibh porta, tempus libero ut, vehicula metus. Curabitur venenatis mauris ac placerat porta. Pellentesque nulla enim, aliquet eu sollicitudin ac, euismod ut elit. Sed nec urna finibus, maximus nibh eget, rutrum massa. Pellentesque et dui rutrum, ultrices risus nec, aliquet neque. Aliquam erat volutpat. Praesent semper porta tortor, nec euismod eros hendrerit et. Morbi vel vulputate sem. Ut varius quam quam, a mollis lectus fermentum sed. Nunc in volutpat ligula. Ut venenatis ligula vitae pharetra lobortis. Sed vitae eros non diam tincidunt rutrum quis a ipsum. Donec condimentum non est at pretium. Phasellus rhoncus non urna vel volutpat. Duis eleifend nunc id efficitur pharetra. Ut et pellentesque mi, eget pellentesque ex. Mauris volutpat libero lectus, sit amet pretium lectus ultrices ac. Etiam congue ex a nisl aliquet, sed condimentum metus iaculis. Donec non erat euismod, facilisis ligula pulvinar, placerat odio. Aenean eget orci fringilla, laoreet leo eu, semper enim. Aenean sed fermentum mi, ut feugiat quam. Morbi ex ipsum, malesuada ac vestibulum non, elementum sit amet ipsum. Pellentesque elementum urna quis nunc aliquet efficitur non vel est. Nunc ultricies fermentum quam sed luctus. Aliquam maximus mi vestibulum est ornare facilisis. Morbi ut aliquam dolor. Sed efficitur ligula quis odio pulvinar placerat. Vestibulum tellus neque, scelerisque eu posuere id, elementum congue felis. Proin in dolor venenatis tellus blandit consequat ac et mauris. Donec luctus, elit sit amet lacinia efficitur, dolor sapien porttitor nunc, id dignissim leo arcu vitae dolor. Aenean mattis bibendum nibh, vel faucibus neque maximus non. Vivamus mi eros, sodales et nulla quis, tristique tempus ante. Vestibulum sagittis molestie justo nec imperdiet. Sed fermentum ipsum vitae enim auctor porttitor. Aenean facilisis vestibulum tristique. Curabitur ac convallis enim, eu sagittis urna. Aenean rhoncus gravida eros. Ut mollis mi eget metus efficitur malesuada. Aenean lacus nisl, sodales at leo at, tempor pretium tortor. Vestibulum felis tellus, tincidunt a fermentum mattis, aliquam non sapien. Donec ut tortor erat. Maecenas malesuada at eros eget tincidunt. Nam maximus, est et euismod molestie, quam turpis malesuada arcu, sit amet fringilla mauris urna vitae orci. Donec tincidunt dolor risus, eu porta felis maximus at. Morbi placerat eget nibh tempus sodales. Nulla luctus urna et ipsum tempus, at mollis arcu rutrum. Nulla ultrices ligula in felis venenatis porttitor. Mauris vel velit felis. Sed consectetur varius semper. Nulla eu vehicula massa, sit amet euismod eros. Quisque dui tortor, posuere vel tempus non, pellentesque sit amet urna. Ut sit amet ultricies ex. Ut metus dolor, porttitor a lacus ut, volutpat porttitor velit. Suspendisse efficitur erat vel ex aliquam dignissim. Donec vulputate vel magna in convallis. Ut auctor augue neque, aliquet aliquam tellus vulputate nec. Nunc eu rhoncus odio. Phasellus sollicitudin tortor ut libero molestie, in blandit sem sodales. Mauris molestie condimentum pharetra. Duis in lacus scelerisque, pretium urna id, blandit est. Phasellus in ullamcorper leo, a sodales diam. \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/HttpClientRedactionProcessorTests.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/HttpClientRedactionProcessorTests.cs new file mode 100644 index 0000000000..2f1630e00c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/HttpClientRedactionProcessorTests.cs @@ -0,0 +1,505 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETCOREAPP3_1_OR_GREATER + +using System; +using System.Diagnostics; +using System.Diagnostics.Tracing; +using System.Net.Http; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Http.Telemetry.Tracing.Internal; +using Microsoft.Extensions.Http.Telemetry.Tracing.Test.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Moq; +using Xunit; +using MSOptions = Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing.Test; + +public class HttpClientRedactionProcessorTests +{ + [Fact] + public void HttpClientRedactionProcessor_NullOptions_Throws() + { + var requestMetadataContext = new Mock().Object; + var redactor = new Mock().Object; + Assert.Throws(() => new HttpClientRedactionProcessor( + MSOptions.Options.Create(null!), redactor, requestMetadataContext)); + } + + [Theory] + [CombinatorialData] + public void HttpClientRedactionProcessor_GivenNullRequestUri_DoesNotSetHttpTargetAndLogsError(bool isLoggerPresent) + { + const int EventId = 2; + const string ActivityName = "test"; + + var redactor = GetHttpPathRedactor(); + var requestMetadataContext = new Mock().Object; + using var listener = new TestEventListener(HttpTracingEventSource.Instance); + var logger = isLoggerPresent ? new FakeLogger() : null; + + var options = MSOptions.Options.Create(new HttpClientTracingOptions()); + var processor = new HttpClientRedactionProcessor(options, redactor, requestMetadataContext, logger: logger); + + using Activity activity = new Activity(ActivityName); + + using HttpRequestMessage httpRequestMessage = new HttpRequestMessage(); + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + RequestRoute = "/api/routes/{routeId}/chats/{chatId}" + }); + + processor.Process(activity, httpRequestMessage); + + Assert.Null(activity.GetTagItem(Constants.AttributeHttpTarget)); + + ValidateEventSourceRecord(listener, EventId, EventLevel.Error, ActivityName); + + if (isLoggerPresent) + { + ValidateLoggerRecord(logger!, EventId, LogLevel.Error, ActivityName); + } + } + + [Fact] + public void HttpClientRedactionProcessor_UrlContains_ParametersInTagsToRedactList_RedactsInExportedUrl() + { + var builder = new ServiceCollection() + .AddFakeRedaction(options => options.RedactionFormat = "Redacted:{0}") + .AddHttpRouteProcessor() + .AddActivatedSingleton() + .BuildServiceProvider(); + + const string UriString = "http://test.com/api/routes/routeId123/chats/chatId123"; + var redactorProvider = (builder.GetService() as FakeRedactorProvider)!; + var options = new HttpClientTracingOptions(); + var httPathRedactor = builder.GetRequiredService(); + var requestMetadataContext = new Mock().Object; + options.RouteParameterDataClasses.Add("routeId", SimpleClassifications.PrivateData); + var processor = new HttpClientRedactionProcessor( + MSOptions.Options.Create(options), + httPathRedactor, + requestMetadataContext); + + using Activity activity = new Activity("test"); + using HttpRequestMessage httpRequestMessage = new HttpRequestMessage(); + httpRequestMessage.RequestUri = new Uri(UriString); + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + RequestRoute = "/api/routes/{routeId}/chats/{chatId}", + RequestName = "TestRequestName" + }); + + processor.Process(activity, httpRequestMessage); + + Assert.Equal("TestRequestName", activity.DisplayName); + Assert.Equal(SimpleClassifications.PrivateData, redactorProvider.Collector.LastRedactorRequested.DataClassification); + Assert.Equal("routeId123", redactorProvider.Collector.LastRedactedData.Original); + Assert.Equal($"http://test.com/api/routes/Redacted:routeId123/chats/{TelemetryConstants.Redacted}", activity.GetTagItem(Constants.AttributeHttpUrl)); + } + + [Theory] + [InlineData(HttpRouteParameterRedactionMode.Strict, + $"http://test.com/api/routes/Redacted:routeId123/chats/{TelemetryConstants.Redacted}", + $"api/routes/Redacted:routeId123/chats/{TelemetryConstants.Redacted}")] + [InlineData(HttpRouteParameterRedactionMode.Loose, + $"http://test.com/api/routes/Redacted:routeId123/chats/chatId123", + $"api/routes/Redacted:routeId123/chats/chatId123")] + [InlineData(HttpRouteParameterRedactionMode.None, + $"http://test.com/api/routes/routeId123/chats/chatId123", + $"/api/routes/routeId123/chats/chatId123")] + public void HttpClientRedactionProcessor_RequestNameMissing_SetsRedactedPathAsDisplayName( + HttpRouteParameterRedactionMode httpPathParameterRedactionMode, + string exptectedUrl, + string exptectedActivityName) + { + var builder = new ServiceCollection() + .AddFakeRedaction(options => options.RedactionFormat = "Redacted:{0}") + .Configure(o => o.RequestPathParameterRedactionMode = httpPathParameterRedactionMode) + .AddHttpRouteProcessor() + .AddActivatedSingleton() + .BuildServiceProvider(); + + const string UriString = "http://test.com/api/routes/routeId123/chats/chatId123"; + var httPathRedactor = builder.GetRequiredService(); + var redactorProvider = (builder.GetService() as FakeRedactorProvider)!; + var options = builder.GetRequiredService>().Value; + var requestMetadataContext = new Mock().Object; + options.RouteParameterDataClasses.Add("routeId", SimpleClassifications.PrivateData); + var processor = new HttpClientRedactionProcessor( + MSOptions.Options.Create(options), + httPathRedactor, + requestMetadataContext); + + using Activity activity = new Activity("test"); + using HttpRequestMessage httpRequestMessage = new HttpRequestMessage(); + httpRequestMessage.RequestUri = new Uri(UriString); + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + RequestRoute = "/api/routes/{routeId}/chats/{chatId}" + }); + + processor.Process(activity, httpRequestMessage); + + Assert.Equal(exptectedActivityName, activity.DisplayName); + Assert.Equal(exptectedUrl, activity.GetTagItem(Constants.AttributeHttpUrl)); + + if (httpPathParameterRedactionMode != HttpRouteParameterRedactionMode.None) + { + Assert.Equal(SimpleClassifications.PrivateData, redactorProvider.Collector.LastRedactorRequested.DataClassification); + Assert.Equal("routeId123", redactorProvider.Collector.LastRedactedData.Original); + } + else + { + Assert.Throws(() => redactorProvider.Collector.LastRedactorRequested.DataClassification); + } + } + + [Theory] + [InlineData(HttpRouteParameterRedactionMode.Strict)] + [InlineData(HttpRouteParameterRedactionMode.Loose)] + public void HttpClientRedactionProcessor_UrlContains_ParametersInTagsToRedactList_RequestRouteMissing_ExportsConstant( + HttpRouteParameterRedactionMode httpPathParameterRedactionMode) + { + var builder = new ServiceCollection() + .AddFakeRedaction(options => options.RedactionFormat = "Redacted:{0}") + .Configure(o => o.RequestPathParameterRedactionMode = httpPathParameterRedactionMode) + .AddHttpRouteProcessor() + .AddActivatedSingleton() + .BuildServiceProvider(); + + const string UriString = "http://test.com/api/routes/routeId123/chats/chatId123"; + var httPathRedactor = builder.GetRequiredService(); + var options = new HttpClientTracingOptions(); + options.RouteParameterDataClasses.Add("routeId", SimpleClassifications.PrivateData); + var httpRouteParser = builder.GetService(); + var httpRouteFormatter = builder.GetService(); + var requestMetadataContext = new Mock().Object; + var processor = new HttpClientRedactionProcessor( + MSOptions.Options.Create(options), + httPathRedactor, + requestMetadataContext); + + using Activity activity = new Activity("test"); + using HttpRequestMessage httpRequestMessage = new HttpRequestMessage(); + httpRequestMessage.RequestUri = new Uri(UriString); + + processor.Process(activity, httpRequestMessage); + + Assert.Equal(TelemetryConstants.Unknown, activity.DisplayName); + Assert.Equal($"http://test.com/{TelemetryConstants.Unknown}", activity.GetTagItem(Constants.AttributeHttpUrl)); + } + + [Theory] + [InlineData(HttpRouteParameterRedactionMode.Strict)] + [InlineData(HttpRouteParameterRedactionMode.Loose)] + public void HttpClientRedactionProcessor_UrlContains_AllParametersInTagsToRedactList_RedactsAllParamsInExportedUrl( + HttpRouteParameterRedactionMode httpPathParameterRedactionMode) + { + var builder = new ServiceCollection() + .AddFakeRedaction(options => options.RedactionFormat = "Redacted:{0}") + .Configure(o => o.RequestPathParameterRedactionMode = httpPathParameterRedactionMode) + .AddHttpRouteProcessor() + .AddActivatedSingleton() + .BuildServiceProvider(); + + const string UriString = "http://test.com/api/routes/routeId123/chats/chatId123/"; + var httPathRedactor = builder.GetRequiredService(); + var redactorProvider = (builder.GetService() as FakeRedactorProvider)!; + var options = new HttpClientTracingOptions(); + options.RouteParameterDataClasses.Add("routeId", SimpleClassifications.PrivateData); + options.RouteParameterDataClasses.Add("chatId", SimpleClassifications.PrivateData); + options.RouteParameterDataClasses.Add("userId", SimpleClassifications.PrivateData); + var requestMetadataContext = new Mock().Object; + var processor = new HttpClientRedactionProcessor( + MSOptions.Options.Create(options), + httPathRedactor, + requestMetadataContext); + + using Activity activity = new Activity("test"); + using HttpRequestMessage httpRequestMessage = new HttpRequestMessage(); + httpRequestMessage.RequestUri = new Uri(UriString); + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + RequestRoute = "/api/routes/{routeId}/chats/{chatId}" + }); + + processor.Process(activity, httpRequestMessage); + + Assert.Equal("api/routes/Redacted:routeId123/chats/Redacted:chatId123", activity.DisplayName); + Assert.Equal(2, redactorProvider.Collector.AllRedactedData.Count); + Assert.Equal("http://test.com/api/routes/Redacted:routeId123/chats/Redacted:chatId123", activity.GetTagItem(Constants.AttributeHttpUrl)); + } + + [Theory] + [InlineData(HttpRouteParameterRedactionMode.Strict, + $"http://test.com/api/routes/{TelemetryConstants.Redacted}/chats/{TelemetryConstants.Redacted}/messages", + $"api/routes/{TelemetryConstants.Redacted}/chats/{TelemetryConstants.Redacted}/messages")] + [InlineData(HttpRouteParameterRedactionMode.Loose, + "http://test.com/api/routes/routeId123/chats/chatId123/messages", + "api/routes/routeId123/chats/chatId123/messages")] + public void HttpClientRedactionProcessor_DoesNotRedactNonParameterStringsInUrl( + HttpRouteParameterRedactionMode httpPathParameterRedactionMode, + string exptectedUrl, + string exptectedActivityName) + { + var builder = new ServiceCollection() + .AddFakeRedaction(options => options.RedactionFormat = "Redacted:{0}") + .Configure(o => o.RequestPathParameterRedactionMode = httpPathParameterRedactionMode) + .AddHttpRouteProcessor() + .AddActivatedSingleton() + .BuildServiceProvider(); + + const string UriString = "http://test.com/api/routes/routeId123/chats/chatId123/messages"; + var httPathRedactor = builder.GetRequiredService(); + var redactorProvider = new FakeRedactorProvider(); + var options = new HttpClientTracingOptions(); + options.RouteParameterDataClasses.Add("routes", SimpleClassifications.PrivateData); + options.RouteParameterDataClasses.Add("chats", SimpleClassifications.PrivateData); + var requestMetadataContext = new Mock().Object; + var processor = new HttpClientRedactionProcessor( + MSOptions.Options.Create(options), + httPathRedactor, + requestMetadataContext); + + using Activity activity = new Activity("test"); + using HttpRequestMessage httpRequestMessage = new HttpRequestMessage(); + httpRequestMessage.RequestUri = new Uri(UriString); + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + RequestRoute = "/api/routes/{routeId}/chats/{chatId}/messages" + }); + + processor.Process(activity, httpRequestMessage); + + Assert.Equal(exptectedActivityName, activity.DisplayName); + Assert.Equal(0, redactorProvider.Collector.AllRedactorRequests.Count); + Assert.Equal(0, redactorProvider.Collector.AllRedactedData.Count); + Assert.Equal(exptectedUrl, activity.GetTagItem(Constants.AttributeHttpUrl)); + } + + [Fact] + public void HttpClientRedactionProcessor_UrlDoesNotContain_ParametersInTagsToRedactList_RedactsInExportedUrl() + { + var builder = new ServiceCollection() + .AddFakeRedaction(options => options.RedactionFormat = "Redacted:{0}") + .AddHttpRouteProcessor() + .AddActivatedSingleton() + .BuildServiceProvider(); + + const string UriString = "http://test.com/api/routes/routeId123/chats/chatId123"; + var httPathRedactor = builder.GetRequiredService(); + var redactorProvider = (builder.GetService() as FakeRedactorProvider)!; + var options = new HttpClientTracingOptions(); + options.RouteParameterDataClasses.Add("userId", SimpleClassifications.PrivateData); + var requestMetadataContext = new Mock().Object; + var processor = new HttpClientRedactionProcessor( + MSOptions.Options.Create(options), + httPathRedactor, + requestMetadataContext); + + using Activity activity = new Activity("test"); + using HttpRequestMessage httpRequestMessage = new HttpRequestMessage(); + httpRequestMessage.RequestUri = new Uri(UriString); + + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + RequestRoute = "/api/routes/{routeId}/chats/{chatId}" + }); + + processor.Process(activity, httpRequestMessage); + + Assert.Equal($"api/routes/{TelemetryConstants.Redacted}/chats/{TelemetryConstants.Redacted}", activity.DisplayName); + Assert.Equal(0, redactorProvider.Collector.AllRedactorRequests.Count); + Assert.Equal(0, redactorProvider.Collector.AllRedactedData.Count); + Assert.Equal($"http://test.com/api/routes/{TelemetryConstants.Redacted}/chats/{TelemetryConstants.Redacted}", activity.GetTagItem(Constants.AttributeHttpUrl)); + } + + [Fact] + public void HttpClientRedactionProcessor_NoRequestRouteSet_ReturnsConstant() + { + var builder = new ServiceCollection() + .AddFakeRedaction(options => options.RedactionFormat = "Redacted:{0}") + .AddHttpRouteProcessor() + .AddActivatedSingleton() + .BuildServiceProvider(); + + const string UriString = "http://test.com/api/routes/routeId123/chats/chatId123"; + var httPathRedactor = builder.GetRequiredService(); + var options = new HttpClientTracingOptions(); + options.RouteParameterDataClasses.Add("routeId", SimpleClassifications.PrivateData); + var requestMetadataContext = new Mock().Object; + var processor = new HttpClientRedactionProcessor( + MSOptions.Options.Create(options), + httPathRedactor, + requestMetadataContext); + + using Activity activity = new Activity("test1"); + using HttpRequestMessage httpRequestMessage = new HttpRequestMessage(); + httpRequestMessage.RequestUri = new Uri(UriString); + + processor.Process(activity, httpRequestMessage); + + Assert.Equal(TelemetryConstants.Unknown, activity.DisplayName); + Assert.Equal($"http://test.com/{TelemetryConstants.Unknown}", activity.GetTagItem(Constants.AttributeHttpUrl)); + } + + [Fact] + public void HttpClientRedactionProcessor_NoRequestRouteSet_RequestNameSet_UsesRequestName() + { + var builder = new ServiceCollection() + .AddFakeRedaction(options => options.RedactionFormat = "Redacted:{0}") + .AddHttpRouteProcessor() + .AddActivatedSingleton() + .BuildServiceProvider(); + + const string UriString = "http://test.com/api/routes/routeId123/chats/chatId123"; + var httPathRedactor = builder.GetRequiredService(); + var options = new HttpClientTracingOptions(); + options.RouteParameterDataClasses.Add("routeId", SimpleClassifications.PrivateData); + var requestMetadataContext = new Mock().Object; + var processor = new HttpClientRedactionProcessor( + MSOptions.Options.Create(options), + httPathRedactor, + requestMetadataContext); + + using Activity activity = new Activity("test1"); + using HttpRequestMessage httpRequestMessage = new HttpRequestMessage(); + httpRequestMessage.RequestUri = new Uri(UriString); + + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + RequestName = "TestRequest" + }); + + processor.Process(activity, httpRequestMessage); + + Assert.Equal("TestRequest", activity.DisplayName); + Assert.Equal($"http://test.com/TestRequest", activity.GetTagItem(Constants.AttributeHttpUrl)); + } + + [Fact] + public void HttpClientRedactionProcessor_EmptyRouteSet_ReturnsConstant() + { + var builder = new ServiceCollection() + .AddFakeRedaction(options => options.RedactionFormat = "Redacted:{0}") + .AddHttpRouteProcessor() + .AddActivatedSingleton() + .BuildServiceProvider(); + + const string UriString = "http://test.com/api/routes/routeId123/chats/chatId123"; + var httPathRedactor = builder.GetRequiredService(); + var options = new HttpClientTracingOptions(); + options.RouteParameterDataClasses.Add("routeId", SimpleClassifications.PrivateData); + var requestMetadataContext = new Mock().Object; + var processor = new HttpClientRedactionProcessor( + MSOptions.Options.Create(options), + httPathRedactor, + requestMetadataContext); + + using Activity activity = new Activity("test1"); + using HttpRequestMessage httpRequestMessage = new HttpRequestMessage(); + httpRequestMessage.RequestUri = new Uri(UriString); + + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + RequestRoute = string.Empty + }); + + processor.Process(activity, httpRequestMessage); + + Assert.Equal(TelemetryConstants.Redacted, activity.DisplayName); + Assert.Equal($"http://test.com/{TelemetryConstants.Redacted}", activity.GetTagItem(Constants.AttributeHttpUrl)); + } + + [Theory] + [InlineData(HttpRouteParameterRedactionMode.Strict, + $"http://test.com/api/routes/{TelemetryConstants.Redacted}/chats/{TelemetryConstants.Redacted}", + $"api/routes/{TelemetryConstants.Redacted}/chats/{TelemetryConstants.Redacted}")] + [InlineData(HttpRouteParameterRedactionMode.Loose, + "http://test.com/api/routes/routeId123/chats/chatId123", + "api/routes/routeId123/chats/chatId123")] + public void HttpClientRedactorProcessor_Given_Zero_Tags_To_Redact_Returns_Quickly( + HttpRouteParameterRedactionMode httpPathParameterRedactionMode, + string exptectedUrl, + string exptectedActivityName) + { + var builder = new ServiceCollection() + .AddFakeRedaction(options => options.RedactionFormat = "Redacted:{0}") + .AddHttpRouteProcessor() + .Configure(o => o.RequestPathParameterRedactionMode = httpPathParameterRedactionMode) + .AddActivatedSingleton() + .BuildServiceProvider(); + + const string UriString = "http://test.com/api/routes/routeId123/chats/chatId123"; + var httPathRedactor = builder.GetRequiredService(); + var redactorProvider = new FakeRedactorProvider(); + var options = new HttpClientTracingOptions(); + var requestMetadataContext = new Mock().Object; + var processor = new HttpClientRedactionProcessor( + MSOptions.Options.Create(options), + httPathRedactor, + requestMetadataContext); + + using Activity activity = new Activity("test"); + using HttpRequestMessage httpRequestMessage = new HttpRequestMessage(); + httpRequestMessage.RequestUri = new Uri(UriString); + + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + RequestRoute = "/api/routes/{routeId}/chats/{chatId}" + }); + + processor.Process(activity, httpRequestMessage); + + Assert.Equal(exptectedActivityName, activity.DisplayName); + Assert.Equal(0, redactorProvider.Collector.AllRedactorRequests.Count); + Assert.Equal(0, redactorProvider.Collector.AllRedactedData.Count); + Assert.Equal(exptectedUrl, activity.GetTagItem(Constants.AttributeHttpUrl)); + } + + private static void ValidateEventSourceRecord( + TestEventListener listener, int eventId, EventLevel level, string activityName) + { + EventWrittenEventArgs? lastEvent = listener.LastEvent; + + Assert.NotNull(lastEvent); + Assert.Equal(eventId, lastEvent!.EventId); + Assert.Equal(level, lastEvent!.Level); + Assert.Contains(activityName, lastEvent!.Payload!); + } + + private static void ValidateLoggerRecord( + FakeLogger logger, int eventId, LogLevel level, string activityName) + { + FakeLogCollector collector = logger.Collector; + Assert.Equal(2, collector.Count); + + FakeLogRecord record = collector.LatestRecord; + Assert.Equal(eventId, record.Id.Id); + Assert.Equal(level, record.Level); + Assert.Contains(activityName, record.Message); + } + + private static IHttpPathRedactor GetHttpPathRedactor() + { + var builder = new ServiceCollection() + .AddFakeRedaction(options => options.RedactionFormat = "Redacted:{0}") + .AddHttpRouteProcessor() + .AddActivatedSingleton() + .BuildServiceProvider(); + + return builder.GetRequiredService(); + } +} + +#endif diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/HttpClientTraceEnrichmentProcessorTests.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/HttpClientTraceEnrichmentProcessorTests.cs new file mode 100644 index 0000000000..74ba0e3fe3 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/HttpClientTraceEnrichmentProcessorTests.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETCOREAPP3_1_OR_GREATER + +using System; +using System.Diagnostics; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Http.Telemetry.Tracing.Test.Internal; +using Microsoft.Extensions.Telemetry; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing.Test; + +#pragma warning disable CS0618 // remove when IHttpClientTraceEnricher.Enrich(Activity, httpRequestMessage) API is deprecated. +public class HttpClientTraceEnrichmentProcessorTests +{ + [Fact] + public void HttpClientTraceEnrichmentProcessor_NoEnrichers_DoesNotThrow() + { + using var host = FakeHost.CreateBuilder(options => options.ValidateOnBuild = false) + .ConfigureWebHost(webBuilder => webBuilder + .ConfigureServices(services => services + .AddRouting() + .AddOpenTelemetry().WithTracing(builder => builder.AddHttpClientTracing()))) + .Build(); + + var traceEnrichmentProcessor = host.Services.GetRequiredService(); + Assert.NotNull(traceEnrichmentProcessor); + } + + [Fact] + public void HttpClientTraceEnrichmentProcessor_WithHttpRequestMessage_MultipleEnrichers() + { + Mock mockTraceEnricher1 = new Mock(); + Mock mockTraceEnricher2 = new Mock(); + + using var host = FakeHost.CreateBuilder() + .ConfigureWebHost(webBuilder => webBuilder + .UseTestServer() + .ConfigureServices(services => services + .AddRouting() + .AddOpenTelemetry().WithTracing(builder => builder + .AddHttpClientTracing() + .AddHttpClientTraceEnricher(mockTraceEnricher1.Object) + .AddHttpClientTraceEnricher(mockTraceEnricher2.Object) + .AddHttpClientTraceEnricher()))) + .Build(); + + var traceEnrichmentProcessor = host.Services.GetRequiredService(); + Assert.NotNull(traceEnrichmentProcessor); + + string uriString = "http://test.com/api/routes/routeId123/chats/chatId123"; + using Activity activity = new Activity("test"); + using HttpRequestMessage httpRequestMessage = new HttpRequestMessage(); + httpRequestMessage.RequestUri = new Uri(uriString); + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + RequestRoute = "/api/routes/{routeId}/chats/{chatId}" + }); + + traceEnrichmentProcessor.Enrich(activity, httpRequestMessage, null); + + mockTraceEnricher1.Verify(m => m.Enrich(activity, httpRequestMessage, null), Times.Once); + mockTraceEnricher2.Verify(m => m.Enrich(activity, httpRequestMessage, null), Times.Once); + } + + [Fact] + public void HttpClientTraceEnrichmentProcessor_WithHttpResponseMessage() + { + Mock mockTraceEnricher1 = new Mock(); + using var host = FakeHost.CreateBuilder() + .ConfigureWebHost(webBuilder => webBuilder + .UseTestServer() + .ConfigureServices(services => services + .AddRouting() + .AddOpenTelemetry().WithTracing(builder => builder + .AddHttpClientTracing() + .AddHttpClientTraceEnricher(mockTraceEnricher1.Object) + .AddHttpClientTraceEnricher()))) + .Build(); + + var traceEnrichmentProcessor = host.Services.GetRequiredService(); + Assert.NotNull(traceEnrichmentProcessor); + + using Activity activity = new Activity("test"); + using var httpResponseMessage = new HttpResponseMessage(System.Net.HttpStatusCode.OK); + + traceEnrichmentProcessor.Enrich(activity, httpResponseMessage.RequestMessage!, httpResponseMessage); + mockTraceEnricher1.Verify(m => m.Enrich(activity, httpResponseMessage.RequestMessage, httpResponseMessage), Times.Once); + } + + [Fact] + public async Task HttpClientTraceEnrichmentProcessor_NoHTTPRequest_DoesNotCallEnrich() + { + using ActivitySource activitySource = new ActivitySource(nameof(HttpClientTraceEnrichmentProcessor_NoHTTPRequest_DoesNotCallEnrich)); + using var testServer = TestHttpServer.RunServerOrThrow(ctx => ctx.Response.OutputStream.Close(), out var hostName, out var port); + var serverHost = $"{hostName}:{port}"; + + var mockTraceEnricher1 = new Mock(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddHttpClient() + .AddOpenTelemetry().WithTracing(builder => builder + .AddSource(nameof(HttpClientTraceEnrichmentProcessor_NoHTTPRequest_DoesNotCallEnrich)) + .AddHttpClientTracing() + .AddHttpClientTraceEnricher(mockTraceEnricher1.Object))) + .StartAsync(); + + using var activity = activitySource.StartActivity("Test"); + activity?.AddTag("internalKey", "internalValue"); + activity?.Stop(); + + mockTraceEnricher1.Verify(m => m.Enrich(activity!, It.IsAny(), It.IsAny()), Times.Never); + } +} + +#endif diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/HttpClientTracingExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/HttpClientTracingExtensionsTests.cs new file mode 100644 index 0000000000..1003adfd8b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/HttpClientTracingExtensionsTests.cs @@ -0,0 +1,330 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETCOREAPP3_1_OR_GREATER + +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Http.Telemetry.Tracing.Internal; +using Microsoft.Extensions.Http.Telemetry.Tracing.Test.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry; +using Microsoft.Extensions.Telemetry.Internal; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Microsoft.TestUtilities; +using Moq; +using OpenTelemetry; +using OpenTelemetry.Trace; +using Xunit; +using MSOptions = Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing.Test; + +public class HttpClientTracingExtensionsTests +{ + [Fact] + public void AddHttpClientTracingWithNullArgument_Throws() + { + var configRoot = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + var configSection = configRoot.GetSection("HttpClientTracingOptions"); + + Assert.Throws(() => + ((TracerProviderBuilder)null!).AddHttpClientTracing()); + + Assert.Throws(() => + ((TracerProviderBuilder)null!).AddHttpClientTracing(_ => { })); + + Assert.Throws(() => + ((TracerProviderBuilder)null!).AddHttpClientTracing(configSection)); + + var services = new ServiceCollection(); + Assert.Throws(() => + services.AddOpenTelemetry().WithTracing(builder => + builder.AddHttpClientTracing((Action)null!))); + + Assert.Throws(() => + services.AddOpenTelemetry().WithTracing(builder => + builder.AddHttpClientTracing((IConfigurationSection)null!))); + } + + [Fact] + public void AddHttpClientTraceEnricher_GivenNullArgument_Throws() + { + Assert.Throws(() => + ((TracerProviderBuilder)null!).AddHttpClientTraceEnricher()); + + Assert.Throws(() => + ((TracerProviderBuilder)null!).AddHttpClientTraceEnricher( + new TestHttpClientTraceEnricher(MSOptions.Options.Create(new HttpClientTracingOptions())))); + + Assert.Throws(() => + ((IServiceCollection)null!).AddHttpClientTraceEnricher()); + + Assert.Throws(() => + ((IServiceCollection)null!).AddHttpClientTraceEnricher( + new TestHttpClientTraceEnricher(MSOptions.Options.Create(new HttpClientTracingOptions())))); + } + + [Fact] + public async Task AddHttpClientTracing_WhenNoCustomHttpPathRedactor_RegistersDefaultHttpPathRedactor() + { + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddHttpClientTracing())) + .StartAsync(); + + var defaultRedactor = host.Services.GetService(); + Assert.NotNull(defaultRedactor); + Assert.IsType(defaultRedactor); + } + + [Fact] + public async Task AddHttpClientTracing_WithCustomHttpPathRedactorBeforeDefalut_RegistersCustomHttpPathRedactor() + { + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton() + .AddOpenTelemetry().WithTracing(builder => builder + .AddHttpClientTracing())) + .StartAsync(); + + var customRedactor = host.Services.GetService(); + Assert.NotNull(customRedactor); + Assert.IsType(customRedactor); + } + + [Fact] + public async Task AddHttpClientTracing_WithCustomHttpPathRedactorAfterDefalut_RegistersCustomHttpPathRedactor() + { + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddHttpClientTracing()).Services + .AddSingleton()) + .StartAsync(); + + var customRedactor = host.Services.GetService(); + Assert.NotNull(customRedactor); + Assert.IsType(customRedactor); + } + + [Theory] + [CombinatorialData] + public void AddHttpClientTracing_WithConfigSection(bool isLoggerPresent) + { + var configRoot = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + var configSection = configRoot.GetSection("HttpClientTracingOptions"); + + var mockTraceEnricher1 = new Mock(); + var mockTraceEnricher2 = new Mock(); + + using var host = FakeHost.CreateBuilder(options => options.FakeRedaction = false) + .ConfigureWebHost(webBuilder => + { + if (isLoggerPresent) + { + webBuilder.ConfigureLogging(builder => builder.AddFakeLogging()); + } + + webBuilder + .UseTestServer() + .ConfigureServices(services => services + .AddRouting() + .AddRedaction() + .AddOpenTelemetry().WithTracing(builder => builder + .AddHttpClientTracing(configSection) + .AddHttpClientTraceEnricher(mockTraceEnricher1.Object) + .AddHttpClientTraceEnricher(mockTraceEnricher2.Object))); + }) + .Build(); + + var enrichers = host.Services.GetServices(); + Assert.Equal(2, enrichers.Count()); + + var processor = host.Services.GetService(); + Assert.NotNull(processor); + + var logger = host.Services.GetService>(); + Assert.NotNull(logger); + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux)] + public async Task AddHttpClientTracing_WebRequestSetInCustomActivity() + { + const string UriString = "https://hopefully-no-such-domain/api/routes/routeId123/chats/chatId123"; + using var traceProcessor = new TestTraceProcessor(); + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddFakeRedaction(options => options.RedactionFormat = "RedactedData:{0}") + .AddHttpClient() + .AddOpenTelemetry().WithTracing(builder => builder + .AddHttpClientTracing(options => + { + options + .RouteParameterDataClasses + .Add("chatId", SimpleClassifications.PrivateData); + + options + .RouteParameterDataClasses + .Add("routeId", SimpleClassifications.PrivateData); + }) + .AddTestTraceProcessor(traceProcessor))) + .StartAsync(); + + var httpClientFactory = host.Services.GetRequiredService(); + using var httpClient = httpClientFactory.CreateClient(); + + using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, UriString); + httpRequestMessage.SetRequestMetadata(new RequestMetadata + { + RequestRoute = "api/routes/{routeId}/chats/{chatId}" + }); + try + { + await httpClient.SendAsync(httpRequestMessage); + } + catch (HttpRequestException) + { + // no op + } + + var activity = traceProcessor.FirstActivity!; + Assert.Equal("api/routes/RedactedData:routeId123/chats/RedactedData:chatId123", activity.DisplayName); + Assert.Equal("https://hopefully-no-such-domain/api/routes/RedactedData:routeId123/chats/RedactedData:chatId123", activity.GetTagItem(Constants.AttributeHttpUrl)); + Assert.Equal("api/routes/{routeId}/chats/{chatId}", activity.GetTagItem(Constants.AttributeHttpRoute)); + Assert.Null(activity.GetTagItem(Constants.AttributeHttpPath)); + Assert.Null(activity.GetTagItem(Constants.AttributeHttpTarget)); + } + + [Fact] + public void UrlRedactionProcessor_Throws_When_No_RedactorProvider() + { + Assert.Throws(() => + FakeHost.CreateBuilder(new FakeHostOptions { FakeRedaction = false, ValidateOnBuild = false }) + .ConfigureWebHost(webBuilder => webBuilder + .UseTestServer() + .ConfigureServices(services => services + .AddRouting() + .AddHttpClient() + .AddOpenTelemetry().WithTracing(builder => builder + .AddHttpClientTracing(options => options.RouteParameterDataClasses.Add("FWEJFNIWJ", SimpleClassifications.PrivateData)))) + .Configure(_ => { })) + .Build() + .Services + .GetRequiredService()); + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux)] + public async Task AddHttpClientTracing_SetRequestMetadataOnOutgoingRequestContext_ActivityEnrichedWithMetadata() + { + const string UriString = "https://hopefully-no-such-domain/api/routes/routeId123/chats/chatId123"; + using var traceProcessor = new TestTraceProcessor(); + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddFakeRedaction(options => options.RedactionFormat = "RedactedData:{0}") + .AddHttpClient() + .AddOpenTelemetry().WithTracing(builder => builder + .AddHttpClientTracing(options => + { + options + .RouteParameterDataClasses + .Add("chatId", SimpleClassifications.PrivateData); + + options + .RouteParameterDataClasses + .Add("routeId", SimpleClassifications.PrivateData); + }) + .AddTestTraceProcessor(traceProcessor))) + .StartAsync(); + + var httpClientFactory = host.Services.GetRequiredService(); + using var httpClient = httpClientFactory.CreateClient(); + + using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, UriString); + + var requestContext = host.Services.GetRequiredService(); + requestContext.RequestMetadata = new RequestMetadata + { + RequestRoute = "api/routes/{routeId}/chats/{chatId}" + }; + + try + { + await httpClient.SendAsync(httpRequestMessage); + } + catch (HttpRequestException) + { + // no op + } + + var activity = traceProcessor.FirstActivity!; + Assert.Equal("api/routes/RedactedData:routeId123/chats/RedactedData:chatId123", activity.DisplayName); + Assert.Equal("https://hopefully-no-such-domain/api/routes/RedactedData:routeId123/chats/RedactedData:chatId123", activity.GetTagItem(Constants.AttributeHttpUrl)); + Assert.Equal("api/routes/{routeId}/chats/{chatId}", activity.GetTagItem(Constants.AttributeHttpRoute)); + Assert.Null(activity.GetTagItem(Constants.AttributeHttpPath)); + Assert.Null(activity.GetTagItem(Constants.AttributeHttpTarget)); + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux, SkipReason = "See https://github.com/dotnet/r9/issues/289")] + public async Task AddHttpClientTracing_GetRequestMetadataFromDownstreamDependencyMetadataManager_ActivityEnrichedWithMetadata() + { + const string UriString = "https://hopefully-no-such-domain/api/test/url/1"; + + Mock downstreamDependencyMetadataManager = new(); + RequestMetadata requestMetadata = new() + { + RequestName = "TestUrl", + RequestRoute = "api/test/url/{id}" + }; + + downstreamDependencyMetadataManager + .Setup(m => m.GetRequestMetadata(It.IsAny())) + .Returns(requestMetadata) + .Verifiable(); + + using var traceProcessor = new TestTraceProcessor(); + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(downstreamDependencyMetadataManager.Object) + .AddHttpClient() + .AddOpenTelemetry().WithTracing(builder => builder + .AddHttpClientTracing() + .AddTestTraceProcessor(traceProcessor))) + .StartAsync(); + + var httpClientFactory = host.Services.GetRequiredService(); + using var httpClient = httpClientFactory.CreateClient(); + using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, UriString); + + try + { + await httpClient.SendAsync(httpRequestMessage); + } + catch (HttpRequestException) + { + // no op + } + + var activity = traceProcessor.FirstActivity!; + Assert.Equal(requestMetadata.RequestName, activity.DisplayName); + Assert.Equal(requestMetadata.RequestRoute, activity.GetTagItem(Constants.AttributeHttpRoute)); + + downstreamDependencyMetadataManager.Verify(); + } +} +#endif diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/HttpClientTracingOptionsValidationTests.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/HttpClientTracingOptionsValidationTests.cs new file mode 100644 index 0000000000..3fc2ea1d7c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/HttpClientTracingOptionsValidationTests.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing.Test; + +public class HttpClientTracingOptionsValidationTests +{ + [Theory] + [MemberData(nameof(ConfigureHttpClientTracingDelegates))] + public async Task HttpClientTracingOptions_RouteParameterDataClasses_ShouldNotBeNull( + Action configureHttpClientTracing) + { + using var host = FakeHost + .CreateBuilder() + .ConfigureServices((_, services) => configureHttpClientTracing(services)) + .Build(); + + var exception = await Assert.ThrowsAsync(() => host.StartAsync()); + Assert.Contains(nameof(HttpClientTracingOptions.RouteParameterDataClasses), exception.Message); + } + + public static IEnumerable ConfigureHttpClientTracingDelegates + { + get + { + yield return new object[] + { + (IServiceCollection services) => + { + services.AddOpenTelemetry().WithTracing(builder => builder.AddHttpClientTracing()); + services.Configure( + options => options.RouteParameterDataClasses = null!); + } + }; + + yield return new object[] + { + (IServiceCollection services) => + { + services.AddOpenTelemetry().WithTracing(builder => builder.AddHttpClientTracing( + options => options.RouteParameterDataClasses = null!)); + } + }; + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/FakeHttpWebResponse.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/FakeHttpWebResponse.cs new file mode 100644 index 0000000000..5761cc2886 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/FakeHttpWebResponse.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Runtime.Serialization; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing.Test.Internal; + +public class FakeHttpWebResponse : HttpWebResponse +{ + public FakeHttpWebResponse(string uri) +#pragma warning disable CS0618 // Type or member is obsolete + : base(GetSerializationInfo(uri), default) +#pragma warning restore CS0618 // Type or member is obsolete + { + } + + private static SerializationInfo GetSerializationInfo(string uri) + { +#pragma warning disable SYSLIB0050 + var serializationInfo = new SerializationInfo(typeof(HttpWebResponse), new FormatterConverter()); +#pragma warning restore SYSLIB0050 + serializationInfo.AddValue("m_HttpResponseHeaders", new WebHeaderCollection(), typeof(WebHeaderCollection)); + serializationInfo.AddValue("m_Uri", new Uri(uri), typeof(Uri)); + serializationInfo.AddValue("m_Certificate", null, typeof(System.Security.Cryptography.X509Certificates.X509Certificate)); + serializationInfo.AddValue("m_Version", new Version(), typeof(Version)); + serializationInfo.AddValue("m_StatusCode", (int)HttpStatusCode.OK); + serializationInfo.AddValue("m_ContentLength", 0); + serializationInfo.AddValue("m_Verb", "GET"); + serializationInfo.AddValue("m_StatusDescription", ""); + serializationInfo.AddValue("m_MediaType", ""); + return serializationInfo; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestEventListener.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestEventListener.cs new file mode 100644 index 0000000000..3b52c9f5d8 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestEventListener.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Tracing; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing.Test.Internal; + +internal sealed class TestEventListener : EventListener +{ + public TestEventListener(EventSource eventSource) + { + EnableEvents(eventSource, EventLevel.Error); + } + + public EventWrittenEventArgs? LastEvent { get; private set; } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + LastEvent = eventData; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestExtensions.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestExtensions.cs new file mode 100644 index 0000000000..89ce1b5806 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestExtensions.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using OpenTelemetry; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing.Test.Internal; + +internal static class TestExtensions +{ + public static TracerProviderBuilder AddTestTraceProcessor(this TracerProviderBuilder builder, BaseProcessor processor) + { + if (builder is IDeferredTracerProviderBuilder deferredTracerProvider) + { + deferredTracerProvider.Configure((_, builder) => builder.AddProcessor(processor)); + } + + return builder; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestHttpClientTraceEnricher.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestHttpClientTraceEnricher.cs new file mode 100644 index 0000000000..b8253ce0b7 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestHttpClientTraceEnricher.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETCOREAPP3_1_OR_GREATER + +using System.Diagnostics; +using System.Net.Http; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing.Test.Internal; + +#pragma warning disable SA1402 // File may only contain a single type + +internal sealed class TestHttpClientTraceEnricher : IHttpClientTraceEnricher +{ + public TestHttpClientTraceEnricher(IOptions _) + { + } + + public void Enrich(Activity activity, HttpRequestMessage? request, HttpResponseMessage? response) => Assert.NotNull(request); +} + +internal sealed class TestHttpClientResponseTraceEnricher : IHttpClientTraceEnricher +{ + public TestHttpClientResponseTraceEnricher(IOptions _) + { + // nop + } + + public void Enrich(Activity activity, HttpRequestMessage? request, HttpResponseMessage? response) + { + // nop + } +} + +internal sealed class TestHttpClientResponseTraceEnricher2 : IHttpClientTraceEnricher +{ + public TestHttpClientResponseTraceEnricher2(IOptions _) + { + } + + public void Enrich(Activity activity, HttpRequestMessage? request, HttpResponseMessage? response) + { + // nop + } +} +#endif diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestHttpPathRedactor.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestHttpPathRedactor.cs new file mode 100644 index 0000000000..e79e1c9a48 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestHttpPathRedactor.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Compliance.Classification; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing.Test.Internal; + +internal class TestHttpPathRedactor : IHttpPathRedactor +{ + public string Redact(string routeTemplate, string httpPath, IReadOnlyDictionary parametersToRedact, out int parameterCount) + { + parameterCount = 0; + + return string.Empty; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestHttpServer.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestHttpServer.cs new file mode 100644 index 0000000000..f91d579c11 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestHttpServer.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing.Test.Internal; + +// Originally taken from https://github.com/open-telemetry/opentelemetry-dotnet/blob/3d6be9cb5770f9f1e46478dccfec18e2ec05f828/test/OpenTelemetry.Tests/Shared/TestHttpServer.cs +internal class TestHttpServer +{ + public static IDisposable RunServerOrThrow(Action action, out string host, out int port) + { + host = "localhost"; + port = 0; + RunningServer? server = null; + + var retryCount = 5; + while (retryCount > 0) + { + try + { + using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + socket.Bind(new IPEndPoint(IPAddress.Any, 0)); + port = ((IPEndPoint)socket.LocalEndPoint!).Port; + } + + server = new RunningServer(action, host, port); + server.Start(); + break; + } + catch (HttpListenerException) + { + retryCount--; + } + } + + if (server is null) + { + throw new InvalidOperationException($"Retry count of {retryCount} was reached while trying to run the test server"); + } + + return server; + } + + private class RunningServer : IDisposable + { + private readonly HttpListener _listener; + private readonly Action _action; + private readonly AutoResetEvent _initialized = new(initialState: false); + private Task _httpListenerTask = Task.CompletedTask; + + public RunningServer(Action action, string host, int port) + { + _action = action; + _listener = new HttpListener(); + + _listener.Prefixes.Add($"http://{host}:{port}/"); + _listener.Start(); + } + + private async Task RunListenerTask() + { + while (true) + { + try + { + var ctxTask = _listener.GetContextAsync(); + + _initialized.Set(); + + _action(await ctxTask.ConfigureAwait(false)); + } + catch (ObjectDisposedException) + { + // Listener was closed before we got into GetContextAsync + break; + } + catch (HttpListenerException ex) when (ex.ErrorCode == 995) + { + // Listener was closed while we were in GetContextAsync. + break; + } + } + } + + public void Start() + { + _httpListenerTask = RunListenerTask(); + _initialized.WaitOne(); + } + + [SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "test helper")] + public void Dispose() + { + try + { + _listener.Close(); + _httpListenerTask.Wait(); + _initialized.Dispose(); + } + catch (ObjectDisposedException) + { + // swallow this exception just in case + } + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestTraceProcessor.cs b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestTraceProcessor.cs new file mode 100644 index 0000000000..f36e4a8ed8 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/Tracing/Internal/TestTraceProcessor.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using OpenTelemetry; + +namespace Microsoft.Extensions.Http.Telemetry.Tracing.Test.Internal; + +internal sealed class TestTraceProcessor : BaseProcessor +{ + public bool IsProcessorInvoked { get; set; } + + public Activity? FirstActivity { get; set; } + + public override void OnEnd(Activity activity) + { + FirstActivity = activity; + IsProcessorInvoked = true; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/appsettings.json b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/appsettings.json new file mode 100644 index 0000000000..80c1c4516a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Telemetry.Tests/appsettings.json @@ -0,0 +1,4 @@ +{ + "HttpClientTracingOptions": { + } +} diff --git a/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/AcceptanceTest.cs b/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/AcceptanceTest.cs new file mode 100644 index 0000000000..3964b17927 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/AcceptanceTest.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Xunit; + +namespace Microsoft.Extensions.Options.Contextual.Test; + +#pragma warning disable SA1402 // File may only contain a single type + +[OptionsContext] +internal partial class WeatherForecastContext // Note class must be partial +{ + public Guid UserId { get; set; } + public string? Country { get; set; } +} + +internal class WeatherForecastOptions +{ + public string TemperatureScale { get; set; } = "Celcius"; // Celcius or Farenheit + public int ForecastDays { get; set; } +} + +internal class CountryContextReceiver : IOptionsContextReceiver +{ + public string? Country { get; private set; } + + public void Receive(string key, T value) + { + if (key == nameof(Country)) + { + Country = value?.ToString(); + } + } +} + +internal class WeatherForecastService : IWeatherForecastService +{ + private readonly IContextualOptions _contextualOptions; + private readonly Random _rng = new(0); + + public WeatherForecastService(IContextualOptions contextualOptions) + { + _contextualOptions = contextualOptions; + } + + public async Task> GetForecast(WeatherForecastContext context, CancellationToken cancellationToken) + { + WeatherForecastOptions options = await _contextualOptions.GetAsync(context, cancellationToken).ConfigureAwait(false); + return Enumerable.Range(1, options.ForecastDays).Select(index => new WeatherForecast + { + Date = new DateTime(2000, 1, 1).AddDays(index), + Temperature = _rng.Next(-20, 55), + TemperatureScale = options.TemperatureScale, + }); + } +} + +internal interface IWeatherForecastService +{ + Task> GetForecast(WeatherForecastContext context, CancellationToken cancellationToken); +} + +internal class WeatherForecast +{ + public DateTime Date { get; set; } + public int Temperature { get; set; } + public string TemperatureScale { get; set; } = string.Empty; +} + +public class AcceptanceTest +{ + [Fact] + public async Task Foo() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .Configure(options => options.ForecastDays = 7) + .AddContextualOptions() + .Configure(ConfigureTemperatureScaleBasedOnCountry) + .AddSingleton()) + .Build(); + + var forecastService = host + .Services + .GetRequiredService(); + + Assert.Equal("Farenheit", (await forecastService.GetForecast(new WeatherForecastContext { Country = "US" }, CancellationToken.None)).First().TemperatureScale); + Assert.Equal("Celcius", (await forecastService.GetForecast(new WeatherForecastContext { Country = "CA" }, CancellationToken.None)).First().TemperatureScale); + + static void ConfigureTemperatureScaleBasedOnCountry(IOptionsContext context, WeatherForecastOptions options) + { + CountryContextReceiver receiver = new(); + context.PopulateReceiver(receiver); + if (receiver.Country == "US") + { + options.TemperatureScale = "Farenheit"; + } + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/ContextualOptionsFactoryTests.cs b/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/ContextualOptionsFactoryTests.cs new file mode 100644 index 0000000000..17ec93148e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/ContextualOptionsFactoryTests.cs @@ -0,0 +1,235 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Options.Contextual.Tests; + +public class ContextualOptionsFactoryTests +{ + [Fact] + public async Task ContextualOptionsFactoryDoesNothingWithNoOptionalDependenciesProvided() + { + var sut = new ContextualOptionsFactory>( + new OptionsFactory>(Enumerable.Empty>>(), Enumerable.Empty>>()), + Enumerable.Empty>>(), + Enumerable.Empty>>(), + Enumerable.Empty>>()); + + var result = await new ContextualOptions>(sut).GetAsync(Mock.Of(), default); + + Assert.Empty(result); + } + + [Fact] + public async Task DefaultValidatorsFailValidationForAnyInstanceName() + { + var sut = new ContextualOptionsFactory>( + new OptionsFactory>(Enumerable.Empty>>(), Enumerable.Empty>>()), + Enumerable.Empty>>(), + Enumerable.Empty>>(), + new[] { new ValidateContextualOptions>(null, _ => false, "epic fail") }); + + await Assert.ThrowsAsync(async () => await sut.CreateAsync(string.Empty, Mock.Of(), default)); + await Assert.ThrowsAsync(async () => await sut.CreateAsync("A Name", Mock.Of(), default)); + } + + [Fact] + public async Task NamedValidatorsFailValidationOnlyForNamedInstance() + { + var sut = new ContextualOptionsFactory>( + new OptionsFactory>(Enumerable.Empty>>(), Enumerable.Empty>>()), + Enumerable.Empty>>(), + Enumerable.Empty>>(), + new[] { new ValidateContextualOptions>("Foo", _ => false, "epic fail") }); + + await Assert.ThrowsAsync(async () => await sut.CreateAsync("Foo", Mock.Of(), default)); + Assert.Empty(await sut.CreateAsync("Bar", Mock.Of(), default)); + } + + [Fact] + public async Task PostConfigureRunsAfterLoad() + { + var loaders = new[] + { + new LoadContextualOptions>( + string.Empty, + (context, _) => new ValueTask>>(new ConfigureContextualOptions>((_, list) => list.Add("configure"), context))), + }; + + var sut = new ContextualOptionsFactory>( + new OptionsFactory>(Enumerable.Empty>>(), Enumerable.Empty>>()), + loaders, + new[] { new PostConfigureContextualOptions>(string.Empty, (_, list) => list.Add("post configure")) }, + Enumerable.Empty>>()); + + var result = await sut.CreateAsync(string.Empty, Mock.Of(), default); + + Assert.Equal(new[] { "configure", "post configure" }, result); + } + + [Fact] + public async Task NamedLoadersLoadOnlyNamedOptions() + { + var loaders = new[] + { + new LoadContextualOptions>( + "Foo", + (context, _) => new ValueTask>>(new ConfigureContextualOptions>((_, list) => list.Add("configure"), context))) + }; + + var sut = new ContextualOptions>(new ContextualOptionsFactory>( + new OptionsFactory>(Enumerable.Empty>>(), Enumerable.Empty>>()), + loaders, + Enumerable.Empty>>(), + Enumerable.Empty>>())); + + Assert.Equal(new[] { "configure" }, await sut.GetAsync("Foo", Mock.Of(), default)); + Assert.Empty(await sut.GetAsync("Bar", Mock.Of(), default)); + } + + [Fact] + public async Task DefaultLoadersLoadAllOptions() + { + var loaders = new[] + { + new LoadContextualOptions>( + null, + (context, _) => new ValueTask>>(new ConfigureContextualOptions>((_, list) => list.Add("configure"), context))), + }; + + var sut = new ContextualOptionsFactory>( + new OptionsFactory>(Enumerable.Empty>>(), Enumerable.Empty>>()), + loaders, + Enumerable.Empty>>(), + Enumerable.Empty>>()); + + Assert.Equal(new[] { "configure" }, await sut.CreateAsync("Foo", Mock.Of(), default)); + Assert.Equal(new[] { "configure" }, await sut.CreateAsync("Bar", Mock.Of(), default)); + } + + [Fact] + public async Task DefaultPostConfigureConfiguresAllOptions() + { + var sut = new ContextualOptionsFactory>( + new OptionsFactory>(Enumerable.Empty>>(), Enumerable.Empty>>()), + Enumerable.Empty>>(), + new[] { new PostConfigureContextualOptions>(null, (_, list) => list.Add("post configure")) }, + Enumerable.Empty>>()); + + Assert.Equal(new[] { "post configure" }, await sut.CreateAsync("Foo", Mock.Of(), default)); + Assert.Equal(new[] { "post configure" }, await sut.CreateAsync("Bar", Mock.Of(), default)); + } + + [Fact] + public async Task NamePostConfigureConfiguresOnlyNamedOptions() + { + var sut = new ContextualOptionsFactory>( + new OptionsFactory>(Enumerable.Empty>>(), Enumerable.Empty>>()), + Enumerable.Empty>>(), + new[] { new PostConfigureContextualOptions>("Foo", (_, list) => list.Add("post configure")) }, + Enumerable.Empty>>()); + + Assert.Equal(new[] { "post configure" }, await sut.CreateAsync("Foo", Mock.Of(), default)); + Assert.Empty(await sut.CreateAsync("Bar", Mock.Of(), default)); + } + + [Fact] + [SuppressMessage( + "Minor Code Smell", + "S3257:Declarations and initializations should be as concise as possible", + Justification = "This analyzer is broken. It's not actually redundant.")] + public async Task LoadsRunConcurrentlyWhileConfiguresRunSequentially() + { + using var semaphore = new SemaphoreSlim(0); + + var loaders = new LoadContextualOptions>[] + { + new(string.Empty, (context, _) => + { + semaphore.Release(); + return new ValueTask>>(new ConfigureContextualOptions>((_, list) => list.Add("1"), context)); + }), + new(string.Empty, async (context, cancellationToken) => + { + await semaphore.WaitAsync(3, cancellationToken); + return new ConfigureContextualOptions>((_, list) => list.Add("2"), context); + }), + new(string.Empty, (context, _) => + { + semaphore.Release(); + return new ValueTask>>(new ConfigureContextualOptions>((_, list) => list.Add("3"), context)); + }), + new(string.Empty, (context, _) => + { + semaphore.Release(); + return new ValueTask>>(new ConfigureContextualOptions>((_, list) => list.Add("4"), context)); + }), + }; + + var sut = new ContextualOptionsFactory>( + new OptionsFactory>(Enumerable.Empty>>(), Enumerable.Empty>>()), + loaders, + Enumerable.Empty>>(), + Enumerable.Empty>>()); + + Assert.Equal(new[] { "1", "2", "3", "4" }, await sut.CreateAsync(string.Empty, Mock.Of(), default)); + } + + [Fact] + [SuppressMessage( + "Minor Code Smell", + "S3257:Declarations and initializations should be as concise as possible", + Justification = "This analyzer is broken. It's not actually redundant.")] + public async Task CreateAsyncAggregatesAllExceptions() + { + var loaders = new LoadContextualOptions>[] + { + new(string.Empty, (context, _) => new ValueTask>>(Mock.Of>>(MockBehavior.Strict))), + new(string.Empty, (context, _) => throw new NotSupportedException()), + }; + + var sut = new ContextualOptionsFactory>( + new OptionsFactory>(Enumerable.Empty>>(), Enumerable.Empty>>()), + loaders, + Enumerable.Empty>>(), + Enumerable.Empty>>()); + + var exception = await Assert.ThrowsAsync(async () => await sut.CreateAsync(string.Empty, Mock.Of(), default)); + Assert.Equal(2, exception.InnerExceptions.Count); + } + + [Fact] + [SuppressMessage( + "Minor Code Smell", + "S3257:Declarations and initializations should be as concise as possible", + Justification = "This analyzer is broken. It's not actually redundant.")] + public async Task CreateAsyncCallsDisposeEvenAfterExceptions() + { + var disposeMock = new Mock>>(); + disposeMock.Setup(conf => conf.Dispose()).Throws(new ObjectDisposedException("foo")); + var loaders = new LoadContextualOptions>[] + { + new(string.Empty, (context, _) => new ValueTask>>(Mock.Of>>(MockBehavior.Strict))), + new(string.Empty, (context, _) => new ValueTask>>(disposeMock.Object)), + }; + + var sut = new ContextualOptionsFactory>( + new OptionsFactory>(Enumerable.Empty>>(), Enumerable.Empty>>()), + loaders, + Enumerable.Empty>>(), + Enumerable.Empty>>()); + + var exception = await Assert.ThrowsAsync(async () => await sut.CreateAsync(string.Empty, Mock.Of(), default)); + Assert.Equal(2, exception.InnerExceptions.Count); + disposeMock.Verify(conf => conf.Dispose(), Times.Once); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/ContextualOptionsServiceCollectionExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/ContextualOptionsServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000000..c59764ca6d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/ContextualOptionsServiceCollectionExtensionsTests.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Options.Contextual.Tests; + +public class ContextualOptionsServiceCollectionExtensionsTests +{ + [Fact] + public void AddContextualOptionsTest() + { + Assert.Throws(() => ((IServiceCollection)null!).AddContextualOptions()); + + using var provider = new ServiceCollection().AddContextualOptions().BuildServiceProvider(); + + Assert.IsType>(provider.GetRequiredService>()); + Assert.IsType>(provider.GetRequiredService>()); + Assert.IsType>(provider.GetRequiredService>()); + } + + [Fact] + public void ConfigureWithLoadTest() + { + Func>> loadOptions = + (_, _) => new ValueTask>(NullConfigureContextualOptions.GetInstance()); + + using var provider = new ServiceCollection().Configure(loadOptions).BuildServiceProvider(); + var loader = (LoadContextualOptions)provider.GetRequiredService>(); + Assert.Equal(loadOptions, loader.LoadAction); + Assert.Equal(string.Empty, loader.Name); + } + + [Fact] + public async Task ConfigureDirectTest() + { + Action configureOptions = (_, _) => { }; + using var provider = new ServiceCollection().Configure(configureOptions).BuildServiceProvider(); + var loader = (LoadContextualOptions)provider.GetRequiredService>(); + Assert.Equal(configureOptions, ((ConfigureContextualOptions)await loader.LoadAction(Mock.Of(), default)).ConfigureOptions); + Assert.Equal(string.Empty, loader.Name); + } + + [Fact] + public void PostConfigureAllTest() + { + Action configureOptions = (_, _) => { }; + using var provider = new ServiceCollection().PostConfigureAll(configureOptions).BuildServiceProvider(); + var postConfigure = (PostConfigureContextualOptions)provider.GetRequiredService>(); + + Assert.Equal(configureOptions, postConfigure.Action); + Assert.Null(postConfigure.Name); + } + + [Fact] + public void PostConfigureDefaultTest() + { + Action configureOptions = (_, _) => { }; + using var provider = new ServiceCollection().PostConfigure(configureOptions).BuildServiceProvider(); + var postConfigure = (PostConfigureContextualOptions)provider.GetRequiredService>(); + + Assert.Equal(configureOptions, postConfigure.Action); + Assert.Equal(string.Empty, postConfigure.Name); + } + + [Fact] + public void PostConfigureNamedTest() + { + Action configureOptions = (_, _) => { }; + using var provider = new ServiceCollection().PostConfigure("Foo", configureOptions).BuildServiceProvider(); + var postConfigure = (PostConfigureContextualOptions)provider.GetRequiredService>(); + + Assert.Equal(configureOptions, postConfigure.Action); + Assert.Equal("Foo", postConfigure.Name); + } + + [Fact] + public void ValidateDefaultTest() + { + Func validate = _ => true; + using var provider = new ServiceCollection().ValidateContextualOptions(validate, "epic fail").BuildServiceProvider(); + var validateOptions = (ValidateContextualOptions)provider.GetRequiredService>(); + + Assert.Equal(validate, validateOptions.Validation); + Assert.Equal(string.Empty, validateOptions.Name); + } + + [Fact] + public void ValidateNamedTest() + { + Func validate = _ => true; + using var provider = new ServiceCollection().ValidateContextualOptions("Foo", validate, "epic fail").BuildServiceProvider(); + var validateOptions = (ValidateContextualOptions)provider.GetRequiredService>(); + + Assert.Equal(validate, validateOptions.Validation); + Assert.Equal("Foo", validateOptions.Name); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/Microsoft.Extensions.Options.Contextual.Tests.csproj b/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/Microsoft.Extensions.Options.Contextual.Tests.csproj new file mode 100644 index 0000000000..02df72ca0e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Options.ContextualOptions.Tests/Microsoft.Extensions.Options.Contextual.Tests.csproj @@ -0,0 +1,20 @@ + + + Microsoft.Extensions.Options.Contextual + Unit tests for Microsoft.Extensions.Options.Contextual + + + + true + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Options.Validation.Tests/Microsoft.Extensions.Options.Validation.Tests.csproj b/test/Libraries/Microsoft.Extensions.Options.Validation.Tests/Microsoft.Extensions.Options.Validation.Tests.csproj new file mode 100644 index 0000000000..7adea74ea1 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Options.Validation.Tests/Microsoft.Extensions.Options.Validation.Tests.csproj @@ -0,0 +1,22 @@ + + + Microsoft.Extensions.Options.Validation + Tests for Microsoft.Extensions.Options.Validation + + + + true + + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Options.Validation.Tests/ValidateEnumeratedItemsAttributeTests.cs b/test/Libraries/Microsoft.Extensions.Options.Validation.Tests/ValidateEnumeratedItemsAttributeTests.cs new file mode 100644 index 0000000000..26f5cf70e3 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Options.Validation.Tests/ValidateEnumeratedItemsAttributeTests.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Options.Validation.Test; + +public class ValidateEnumeratedItemsAttributeTests +{ + [Fact] + public void Basic() + { + var a = new ValidateEnumeratedItemsAttribute(); + Assert.NotNull(a); + Assert.Null(a.Validator); + + a = new ValidateEnumeratedItemsAttribute(typeof(int)); + Assert.Equal(typeof(int), a.Validator); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Options.Validation.Tests/ValidateObjectMembersAttributeTest.cs b/test/Libraries/Microsoft.Extensions.Options.Validation.Tests/ValidateObjectMembersAttributeTest.cs new file mode 100644 index 0000000000..69208ed9b5 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Options.Validation.Tests/ValidateObjectMembersAttributeTest.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Options.Validation.Test; + +public class ValidateObjectMembersAttributeTest +{ + [Fact] + public void Basic() + { + var a = new ValidateObjectMembersAttribute(); + Assert.NotNull(a); + Assert.Null(a.Validator); + + a = new ValidateObjectMembersAttribute(typeof(int)); + Assert.Equal(typeof(int), a.Validator); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/FaultInjectionExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/FaultInjectionExtensionsTest.cs new file mode 100644 index 0000000000..a52a2c08c3 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/FaultInjectionExtensionsTest.cs @@ -0,0 +1,267 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Metering; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.FaultInjection.Test; + +public class FaultInjectionExtensionsTest +{ + private readonly IConfiguration _configurationWithPolicyOptions; + + public FaultInjectionExtensionsTest() + { + var builder = new ConfigurationBuilder().AddJsonFile("configs/appsettings.json"); + _configurationWithPolicyOptions = builder.Build(); + } + + [Fact] + public void AddFaultInjection_ShouldRegisterRequiredServices() + { + var services = new ServiceCollection(); + services + .AddLogging() + .RegisterMetering() + .AddFaultInjection(); + + using var serviceProvider = services.BuildServiceProvider(); + + var chaosPolicyConfigProvider = serviceProvider.GetService(); + Assert.IsAssignableFrom(chaosPolicyConfigProvider); + + var exceptionRegistry = serviceProvider.GetService(); + Assert.IsAssignableFrom(exceptionRegistry); + + var policyFactory = serviceProvider.GetService(); + Assert.IsAssignableFrom(policyFactory); + } + + [Fact] + public void AddFaultInjection_NullServices_ShouldThrow() + { + ServiceCollection? services = null!; + Assert.Throws(() => services.AddFaultInjection()); + } + + [Fact] + public void AddFaultInjection_WithConfigurationSection_ShouldRegisterRequiredServices() + { + var services = new ServiceCollection(); + services + .AddLogging() + .RegisterMetering() + .AddFaultInjection(_configurationWithPolicyOptions.GetSection("ChaosPolicyConfigurations")); + + using var serviceProvider = services.BuildServiceProvider(); + + var chaosPolicyConfigProvider = serviceProvider.GetService(); + Assert.IsAssignableFrom(chaosPolicyConfigProvider); + + var exceptionRegistry = serviceProvider.GetService(); + Assert.IsAssignableFrom(exceptionRegistry); + + var policyFactory = serviceProvider.GetService(); + Assert.IsAssignableFrom(policyFactory); + } + + [Fact] + public void AddFaultInjection_WithConfigurationSection_NullServices_ShouldThrow() + { + ServiceCollection? services = null!; + + Assert.Throws( + () => services.AddFaultInjection(_configurationWithPolicyOptions.GetSection("ChaosPolicyConfigurations"))); + } + + [Fact] + public void AddFaultInjection_NullConfigurationSection_ShouldThrow() + { + var services = new ServiceCollection(); + IConfigurationSection? configurationSection = null!; + + Assert.Throws( + () => services.AddFaultInjection(configurationSection)); + } + + [Fact] + public void AddFaultInjection_WithAction_ShouldInvokeActionAndRegisterRequiredServices() + { + var testExceptionKey1 = "TestException1"; + var testExceptionKey2 = "TestException2"; + var testException1 = new InjectedFaultException("TestException1"); + var testException2 = new InjectedFaultException("TestException2"); + + var services = new ServiceCollection(); + services + .AddLogging() + .RegisterMetering() + .AddFaultInjection(builder => + builder + .Configure(_configurationWithPolicyOptions.GetSection("ChaosPolicyConfigurations")) + .AddException(testExceptionKey1, testException1) + .AddException(testExceptionKey2, testException2)); + + using var serviceProvider = services.BuildServiceProvider(); + + var chaosPolicyConfigProviderOptions = serviceProvider.GetRequiredService>().Value; + Assert.IsAssignableFrom(chaosPolicyConfigProviderOptions); + Assert.NotNull(chaosPolicyConfigProviderOptions.ChaosPolicyOptionsGroups?["OptionsGroupTest"]); + + var faultInjectionExceptionOptions1 = serviceProvider.GetRequiredService>().Get(testExceptionKey1); + Assert.IsAssignableFrom(faultInjectionExceptionOptions1); + Assert.Equal(testException1, faultInjectionExceptionOptions1.Exception); + + var faultInjectionExceptionOptions2 = serviceProvider.GetRequiredService>().Get(testExceptionKey2); + Assert.IsAssignableFrom(faultInjectionExceptionOptions2); + Assert.Equal(testException2, faultInjectionExceptionOptions2.Exception); + + var chaosPolicyConfigProvider = serviceProvider.GetRequiredService(); + Assert.IsAssignableFrom(chaosPolicyConfigProvider); + + var exceptionRegistry = serviceProvider.GetRequiredService(); + Assert.IsAssignableFrom(exceptionRegistry); + + var policyFactory = serviceProvider.GetService(); + Assert.IsAssignableFrom(policyFactory); + } + + [Fact] + public void AddFaultInjection_NullAction_ShouldThrow() + { + var services = new ServiceCollection(); + + Action? action = null!; + Assert.Throws(() => services.AddFaultInjection(action)); + } + + [Fact] + public void AddFaultInjection_WithAction_NullServices_ShouldThrow() + { + ServiceCollection? services = null!; + + Assert.Throws( + () => services.AddFaultInjection(builder => { })); + } + + [Fact] + public void WithFaultInjection_NullContext_ShouldThrow() + { + Context? context = null!; + Assert.Throws(() => context.WithFaultInjection("Test")); + } + + [Fact] + public void WithFaultInjection_NullGroupName_ShouldThrow() + { + var context = new Context(); + string? testGroupName = null!; + + Assert.Throws(() => context.WithFaultInjection(testGroupName)); + } + + [Fact] + public void WithFaultInjection_GetFaultInjectionGroupName_IfRegistered_ShouldReturnGroupName() + { + var context = new Context(); + var groupName = "test"; + context.WithFaultInjection(groupName); + + var result = context.GetFaultInjectionGroupName(); + Assert.Equal(groupName, result); + } + + [Fact] + public void GetFaultInjectionGroupName_NullContext_ShouldThrow() + { + Context? context = null!; + Assert.Throws(() => context.GetFaultInjectionGroupName()); + } + + [Fact] + public void GetFaultInjectionGroupName_IfNotRegistered_ShouldReturnNull() + { + var context = new Context(); + var result = context.GetFaultInjectionGroupName(); + Assert.Null(result); + } + + [Fact] + public void GetFaultInjectionGroupName_EmptyWeightedAssignments_ShouldReturnNull() + { + var context = new Context(); + var weightAssignments = new FaultPolicyWeightAssignmentsOptions(); + context.WithFaultInjection(weightAssignments); + + var result = context.GetFaultInjectionGroupName(); + Assert.Null(result); + } + + [Fact] + public void WithFaultInjection_WeightedAssignments() + { + var context = new Context(); + var weightAssignments = new FaultPolicyWeightAssignmentsOptions(); + + weightAssignments.WeightAssignments.Add("TestA", 40); + weightAssignments.WeightAssignments.Add("TestB", 20); + weightAssignments.WeightAssignments.Add("TestC", 30); + weightAssignments.WeightAssignments.Add("TestD", 10); + context.WithFaultInjection(weightAssignments); + + // Check the ordering + var contextWeightAssignments = (Dictionary)context["ChaosPolicyOptionsGroupName"]; + var prev = 0.0; + foreach (var entry in contextWeightAssignments) + { + Assert.True(prev < entry.Value); + prev = entry.Value; + } + + for (int i = 0; i < 100; i++) + { + var result = context.GetFaultInjectionGroupName(); + Assert.True(result == "TestA" || result == "TestB" || result == "TestC" || result == "TestD"); + } + } + + [Fact] + public void WithFaultInjection_WeightedAssignments_NullContext_ShouldThrow() + { + Context? context = null!; + var weightAssignments = new FaultPolicyWeightAssignmentsOptions(); + + Assert.Throws(() => context.WithFaultInjection(weightAssignments)); + } + + [Fact] + public void WithFaultInjection_NullWeightedAssignments_ShouldThrow() + { + var context = new Context(); + FaultPolicyWeightAssignmentsOptions? weightAssignments = null!; + + Assert.Throws(() => context.WithFaultInjection(weightAssignments)); + } + + [Fact] + public void WithFaultInjection_WeightedAssignments_Mutation_Check() + { + var context = new Context(); + var weightAssignments = new FaultPolicyWeightAssignmentsOptions(); + + weightAssignments.WeightAssignments.Add("TestA", 10); + context.WithFaultInjection(weightAssignments); + + for (int i = 0; i < 100; i++) + { + var result = context.GetFaultInjectionGroupName(); + Assert.True(result == "TestA"); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/FaultInjectionOptionsBuilderTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/FaultInjectionOptionsBuilderTest.cs new file mode 100644 index 0000000000..e1f0f8087f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/FaultInjectionOptionsBuilderTest.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Resilience.FaultInjection.Test; + +public class FaultInjectionOptionsBuilderTest +{ + [Fact] + public void CanConstruct() + { + var services = new ServiceCollection(); + var faultInjectionOptionsBuilder = new FaultInjectionOptionsBuilder(services); + + Assert.NotNull(faultInjectionOptionsBuilder); + } + + [Fact] + public void Constructor_NullServices_ShouldThrow() + { + Assert.Throws(() => new FaultInjectionOptionsBuilder(null!)); + } + + [Fact] + public void Configure_WithConfigurationSection_ShouldConfigureChaosPolicyConfigProviderOptions() + { + var services = new ServiceCollection(); + var faultInjectionOptionsBuilder = new FaultInjectionOptionsBuilder(services); + + var builder = new ConfigurationBuilder().AddJsonFile("configs/appsettings.json"); + var configuration = builder.Build(); + + faultInjectionOptionsBuilder.Configure(configuration.GetSection("ChaosPolicyConfigurations")); + + using var provider = services.BuildServiceProvider(); + var result = provider.GetRequiredService>().Value; + Assert.IsAssignableFrom(result); + Assert.NotNull(result.ChaosPolicyOptionsGroups?["OptionsGroupTest"]); + } + + [Fact] + public void Configure_WithConfigurationSection_NullConfigurationSection_ShouldThrow() + { + var services = new ServiceCollection(); + var faultInjectionOptionsBuilder = new FaultInjectionOptionsBuilder(services); + + Assert.Throws( + () => faultInjectionOptionsBuilder.Configure((IConfigurationSection)null!)); + } + + [Fact] + public void Configure_WithAction_ShouldConfigureChaosPolicyConfigProviderOptions() + { + var services = new ServiceCollection(); + var faultInjectionOptionsBuilder = new FaultInjectionOptionsBuilder(services); + var testChaosPolicyOptionsGroups = new Dictionary(); + + faultInjectionOptionsBuilder.Configure(options => + { + options.ChaosPolicyOptionsGroups = testChaosPolicyOptionsGroups; + }); + + using var provider = services.BuildServiceProvider(); + var result = provider.GetRequiredService>().Value; + Assert.IsAssignableFrom(result); + Assert.Equal(testChaosPolicyOptionsGroups, result.ChaosPolicyOptionsGroups); + } + + [Fact] + public void Configure_WithAction_NullAction_ShouldThrow() + { + var services = new ServiceCollection(); + var faultInjectionOptionsBuilder = new FaultInjectionOptionsBuilder(services); + + Assert.Throws( + () => faultInjectionOptionsBuilder.Configure((Action)null!)); + } + + [Fact] + public void AddExceptionForFaultInjection_ShouldAddInstanceToExceptionRegistryOptions() + { + var testExceptionKey = "TestExceptionKey"; + var testExceptionInstance = new InjectedFaultException(); + var services = new ServiceCollection(); + var faultInjectionOptionsBuilder = new FaultInjectionOptionsBuilder(services); + faultInjectionOptionsBuilder.AddException(testExceptionKey, testExceptionInstance); + + using var provider = services.BuildServiceProvider(); + var faultInjectionExceptionOptions = provider.GetRequiredService>().Get(testExceptionKey); + + Assert.Equal(faultInjectionExceptionOptions.Exception, testExceptionInstance); + } + + [Fact] + public void AddExceptionForFaultInjection_NullExceptionInstance_ShouldThrow() + { + var testExceptionKey = "TestExceptionKey"; + Exception? testExceptionInstance = null!; + + var services = new ServiceCollection(); + var faultInjectionOptionsBuilder = new FaultInjectionOptionsBuilder(services); + + Assert.Throws(() => + faultInjectionOptionsBuilder.AddException(testExceptionKey, testExceptionInstance)); + } + + [Fact] + public void AddExceptionForFaultInjection_KeyNullOrWhiteSpace_ShouldThrow() + { + string? testExceptionKey = null!; + var testExceptionInstance = new InjectedFaultException(); + var services = new ServiceCollection(); + var faultInjectionOptionsBuilder = new FaultInjectionOptionsBuilder(services); + + Assert.Throws(() => + faultInjectionOptionsBuilder.AddException(testExceptionKey, testExceptionInstance)); + + testExceptionKey = ""; + Assert.Throws(() => + faultInjectionOptionsBuilder.AddException(testExceptionKey, testExceptionInstance)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/InjectedFaultExceptionTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/InjectedFaultExceptionTests.cs new file mode 100644 index 0000000000..37db1613ee --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/InjectedFaultExceptionTests.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using AutoFixture; +using Microsoft.Extensions.Resilience.FaultInjection; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.FaultInjection.Test; + +public class InjectedFaultExceptionTests +{ + [Fact] + public void Ctor_Empy() + { + var exception = new InjectedFaultException(); + + Assert.NotNull(exception); + } + + [Fact] + public void Ctor_WithMessage() + { + var message = new Fixture().Create(); + var exception = new InjectedFaultException(message); + + Assert.Equal(message, exception.Message); + } + + [Fact] + public void Ctor_WithMessageAndInnerException() + { + var message = new Fixture().Create(); + var innerException = new Fixture().Create(); + + var exception = new InjectedFaultException(message, innerException); + + Assert.Equal(message, exception.Message); + Assert.Equal(innerException, exception.InnerException); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/ChaosPolicyFactoryTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/ChaosPolicyFactoryTest.cs new file mode 100644 index 0000000000..b402acf044 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/ChaosPolicyFactoryTest.cs @@ -0,0 +1,270 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Telemetry.Metering; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.FaultInjection.Test.Internals; + +public class ChaosPolicyFactoryTest +{ + private readonly string _testOptionsGroupName = "TestGroupName"; + private readonly ChaosPolicyOptionsGroup _testChaosPolicyOptionsGroup; + private readonly IChaosPolicyFactory _testPolicyFactory; + private readonly string _testExceptionKey = "TestExceptionKey"; + private readonly InjectedFaultException _testException; + + public ChaosPolicyFactoryTest() + { + _testChaosPolicyOptionsGroup = new ChaosPolicyOptionsGroup + { + LatencyPolicyOptions = new LatencyPolicyOptions + { + Enabled = true, + FaultInjectionRate = 0.3 + }, + HttpResponseInjectionPolicyOptions = new HttpResponseInjectionPolicyOptions + { + Enabled = true, + FaultInjectionRate = 0.4 + }, + ExceptionPolicyOptions = new ExceptionPolicyOptions + { + Enabled = true, + FaultInjectionRate = 0.5, + ExceptionKey = _testExceptionKey + } + }; + _testException = new InjectedFaultException(); + + var services = new ServiceCollection(); + services + .AddLogging() + .RegisterMetering() + .AddFaultInjection(builder => builder.Configure( + options => + { + options.ChaosPolicyOptionsGroups.Add(_testOptionsGroupName, _testChaosPolicyOptionsGroup); + }) + .AddException(_testExceptionKey, _testException)); + + using var provider = services.BuildServiceProvider(); + _testPolicyFactory = provider.GetRequiredService(); + } + + [Fact] + public void CreateInjectLatencyPolicy_WithDelegateFunctions_ShouldReturnInstance() + { + var policy = _testPolicyFactory.CreateLatencyPolicy(); + Assert.NotNull(policy); + } + + [Fact] + public void CreateInjectExceptionPolicy_WithDelegateFunctions_ShouldReturnInstance() + { + var policy = _testPolicyFactory.CreateExceptionPolicy(); + Assert.NotNull(policy); + } + + [Fact] + public async Task GetEnabledAsync_WhenNoOptionsGroupNameFound_ShouldReturnFalse() + { + var context = new Context(); + + var result = await ((ChaosPolicyFactory)_testPolicyFactory).GetEnabledAsync(context, CancellationToken.None); + Assert.False(result); + } + + [Fact] + public async Task GetEnabledAsync_WhenNoOptionsGroupFound_ShouldReturnFalse() + { + var context = new Context(); + context.WithFaultInjection("RandomName"); + + var result = await ((ChaosPolicyFactory)_testPolicyFactory).GetEnabledAsync(context, CancellationToken.None); + Assert.False(result); + } + + [Fact] + public async Task GetEnabledAsync_ForLatencyPolicyOptions_ShouldReturnEnabled() + { + var context = new Context(); + context.WithFaultInjection(_testOptionsGroupName); + + var result = await ((ChaosPolicyFactory)_testPolicyFactory).GetEnabledAsync(context, CancellationToken.None); + Assert.Equal(_testChaosPolicyOptionsGroup!.LatencyPolicyOptions!.Enabled, result); + } + + [Fact] + public async Task GetEnabledAsync_ForLatencyPolicyOptions_WhenNoLatencyPolicyFoundInOptionsGroup_ShouldReturnFalse() + { + var testGroupName = "TestGroup"; + var tesOptionsGroupNoPolicyOptions = new ChaosPolicyOptionsGroup(); + var services = new ServiceCollection(); + services + .AddLogging() + .RegisterMetering() + .AddFaultInjection(builder => builder.Configure( + options => + { + options.ChaosPolicyOptionsGroups.Add(testGroupName, tesOptionsGroupNoPolicyOptions); + })); + + using var provider = services.BuildServiceProvider(); + var testPolicyFactory = provider.GetRequiredService(); + + var context = new Context(); + context.WithFaultInjection(testGroupName); + + var result = await ((ChaosPolicyFactory)testPolicyFactory).GetEnabledAsync(context, CancellationToken.None); + Assert.False(result); + } + + [Fact] + public async Task GetEnabledAsync_ForExceptionPolicyOptions_ShouldReturnEnabled() + { + var context = new Context(); + context.WithFaultInjection(_testOptionsGroupName); + + var result = await ((ChaosPolicyFactory)_testPolicyFactory).GetEnabledAsync(context, CancellationToken.None); + Assert.Equal(_testChaosPolicyOptionsGroup!.ExceptionPolicyOptions!.Enabled, result); + } + + [Fact] + public async Task GetEnabledAsync_ForExceptionPolicyOptions_WhenNoExceptionPolicyFoundInOptionsGroup_ShouldReturnFalse() + { + var testGroupName = "TestGroup"; + var tesOptionsGroupNoPolicyOptions = new ChaosPolicyOptionsGroup(); + var services = new ServiceCollection(); + services + .AddLogging() + .RegisterMetering() + .AddFaultInjection(builder => builder.Configure( + options => + { + options.ChaosPolicyOptionsGroups.Add(testGroupName, tesOptionsGroupNoPolicyOptions); + })); + + using var provider = services.BuildServiceProvider(); + var testPolicyFactory = provider.GetRequiredService(); + + var context = new Context(); + context.WithFaultInjection(testGroupName); + + var result = await ((ChaosPolicyFactory)testPolicyFactory).GetEnabledAsync(context, CancellationToken.None); + Assert.False(result); + } + + [Fact] + public async Task GetInjectionRateAsync_WhenNoOptionsGroupNameFound_ShouldReturnZero() + { + var context = new Context(); + + var result = await ((ChaosPolicyFactory)_testPolicyFactory).GetInjectionRateAsync(context, CancellationToken.None); + Assert.Equal(0.0, result); + } + + [Fact] + public async Task GetInjectionRateAsync_WhenNoOptionsGroupFound_ShouldReturnZero() + { + var context = new Context(); + context.WithFaultInjection("RandomName"); + + var result = await ((ChaosPolicyFactory)_testPolicyFactory).GetInjectionRateAsync(context, CancellationToken.None); + Assert.Equal(0.0, result); + } + + [Fact] + public async Task GetInjectionRateAsync_ForLatencyPolicyOptions_ShouldReturnInjectionRate() + { + var context = new Context(); + context.WithFaultInjection(_testOptionsGroupName); + + var result = await ((ChaosPolicyFactory)_testPolicyFactory).GetInjectionRateAsync(context, CancellationToken.None); + Assert.Equal(_testChaosPolicyOptionsGroup!.LatencyPolicyOptions!.FaultInjectionRate, result); + } + + [Fact] + public async Task GetInjectionRateAsync_ForLatencyPolicyOptions_WhenNoLatencyPolicyFoundInOptionsGroup_ShouldReturnZero() + { + var testGroupName = "TestGroup"; + var tesOptionsGroupNoPolicyOptions = new ChaosPolicyOptionsGroup(); + var services = new ServiceCollection(); + services + .AddLogging() + .RegisterMetering() + .AddFaultInjection(builder => builder.Configure( + options => + { + options.ChaosPolicyOptionsGroups.Add(testGroupName, tesOptionsGroupNoPolicyOptions); + })); + + using var provider = services.BuildServiceProvider(); + var testPolicyFactory = provider.GetRequiredService(); + + var context = new Context(); + context.WithFaultInjection(testGroupName); + + var result = await ((ChaosPolicyFactory)testPolicyFactory).GetInjectionRateAsync(context, CancellationToken.None); + Assert.Equal(0.0, result); + } + + [Fact] + public async Task GetInjectionRateAsync_ForExceptionPolicyOptions_ShouldReturnInjectionRate() + { + var context = new Context(); + context.WithFaultInjection(_testOptionsGroupName); + + var result = await ((ChaosPolicyFactory)_testPolicyFactory).GetInjectionRateAsync(context, CancellationToken.None); + Assert.Equal(_testChaosPolicyOptionsGroup!.ExceptionPolicyOptions!.FaultInjectionRate, result); + } + + [Fact] + public async Task GetInjectionRateAsync_ForExceptionPolicyOptions_WhenNoExceptionPolicyFoundInOptionsGroup_ShouldReturnZero() + { + var testGroupName = "TestGroup"; + var tesOptionsGroupNoPolicyOptions = new ChaosPolicyOptionsGroup(); + var services = new ServiceCollection(); + services + .AddLogging() + .RegisterMetering() + .AddFaultInjection(builder => builder.Configure( + options => + { + options.ChaosPolicyOptionsGroups.Add(testGroupName, tesOptionsGroupNoPolicyOptions); + })); + + using var provider = services.BuildServiceProvider(); + var testPolicyFactory = provider.GetRequiredService(); + + var context = new Context(); + context.WithFaultInjection(testGroupName); + + var result = await ((ChaosPolicyFactory)testPolicyFactory).GetInjectionRateAsync(context, CancellationToken.None); + Assert.Equal(0.0, result); + } + + [Fact] + public async Task GetLatencyAsync_ShouldReturnLatency() + { + var context = new Context(); + context.WithFaultInjection(_testOptionsGroupName); + + var result = await ((ChaosPolicyFactory)_testPolicyFactory).GetLatencyAsync(context, CancellationToken.None); + Assert.Equal(_testChaosPolicyOptionsGroup!.LatencyPolicyOptions!.Latency, result); + } + + [Fact] + public async Task GetExceptionAsync_ShouldReturnExceptionInstance() + { + var context = new Context(); + context.WithFaultInjection(_testOptionsGroupName); + + var result = await ((ChaosPolicyFactory)_testPolicyFactory).GetExceptionAsync(context, CancellationToken.None); + Assert.Equal(_testException, result); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/ExceptionRegistryTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/ExceptionRegistryTest.cs new file mode 100644 index 0000000000..52d8cdc4b9 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/ExceptionRegistryTest.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Resilience.FaultInjection.Test.Internals; + +public class ExceptionRegistryTest +{ + [Fact] + public void GetException_NullKey_ShouldThrow() + { + var services = new ServiceCollection(); + services.AddFaultInjection(); + + using var provider = services.BuildServiceProvider(); + var exceptionRegistry = provider.GetRequiredService(); + + Assert.Throws(() => exceptionRegistry.GetException(null!)); + } + + [Fact] + public void GetException_RegisteredKey_ShouldReturnInstance() + { + var testExceptionKey = "TestExceptionKey"; + var testExceptionInstance = new InjectedFaultException(); + var services = new ServiceCollection(); + services.AddFaultInjection(); + + var faultInjectionOptionsBuilder = new FaultInjectionOptionsBuilder(services); + faultInjectionOptionsBuilder.AddException(testExceptionKey, testExceptionInstance); + + using var provider = services.BuildServiceProvider(); + var exceptionRegistry = provider.GetRequiredService(); + + var result = exceptionRegistry.GetException(testExceptionKey); + Assert.Equal(testExceptionInstance, result); + } + + [Fact] + public void GetException_UnregisteredKey_ShouldReturnDefaultInstance() + { + var services = new ServiceCollection(); + services.AddFaultInjection(); + + using var provider = services.BuildServiceProvider(); + var exceptionRegistry = provider.GetRequiredService(); + + var result = exceptionRegistry.GetException("testingtesting"); + Assert.IsAssignableFrom(result); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/FaultInjectionOptionsProviderTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/FaultInjectionOptionsProviderTest.cs new file mode 100644 index 0000000000..33610bce69 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/FaultInjectionOptionsProviderTest.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Resilience.FaultInjection.Test.Internals; + +public class FaultInjectionOptionsProviderTest +{ + [Fact] + public void CanConstruct() + { + var options = new FaultInjectionOptions(); + var optionsMonitor = Mock.Of>(_ => _.CurrentValue == options); + + var chaosPolicyConfigProviderWithTelemetry = new FaultInjectionOptionsProvider(optionsMonitor); + Assert.NotNull(chaosPolicyConfigProviderWithTelemetry); + } + + [Fact] + public void Constructor_ReloadOnChangeTrue_OptionsAreUpdated() + { + var builder = new ConfigurationBuilder().AddJsonFile("configs/optionsOnChangeTestOriginal.json", false, true); + var config = builder.Build(); + + var services = new ServiceCollection(); + services + .AddFaultInjection(config.GetSection("ChaosPolicyConfigurations")); + + using var serviceProvider = services.BuildServiceProvider(); + var configurationProvider = serviceProvider.GetRequiredService(); + + // Trigger onChange callback of the options monitor + var original = File.ReadAllText("configs/optionsOnChangeTestOriginal.json"); + File.Copy("configs/optionsOnChangeTestNew.json", "configs/optionsOnChangeTestOriginal.json", true); + + // Wait for 5 seconds + Thread.Sleep(5000); + + // Verify that options is updated + var result = configurationProvider.TryGetChaosPolicyOptionsGroup("OptionsGroupTest", out var optionsGroup); + Assert.True(result); + Assert.False(optionsGroup!.LatencyPolicyOptions!.Enabled); + + // Test clean up + File.WriteAllText("configs/optionsOnChangeTestOriginal.json", original); + } + + [Fact] + public void GetChaosPolicyOptionsGroup_ValidOptionsGroupName_ShouldReturnInstance() + { + var testOptionsGroup = new ChaosPolicyOptionsGroup(); + var testOptionsGroupName = "TestOptionsGroup"; + var options = new FaultInjectionOptions + { + ChaosPolicyOptionsGroups = new Dictionary + { + { testOptionsGroupName, testOptionsGroup } + } + }; + + var optionsMonitor = Mock.Of>(_ => _.CurrentValue == options); + var chaosPolicyConfigProvider = new FaultInjectionOptionsProvider(optionsMonitor); + + var result = chaosPolicyConfigProvider.TryGetChaosPolicyOptionsGroup(testOptionsGroupName, out var optionsGroup); + Assert.True(result); + Assert.Equal(optionsGroup, testOptionsGroup); + } + + [Fact] + public void GetChaosPolicyOptionsGroup_InvalidOptionsGroupName_ShouldReturnNull() + { + var options = new FaultInjectionOptions(); + var optionsMonitor = Mock.Of>(_ => _.CurrentValue == options); + var chaosPolicyConfigProvider = new FaultInjectionOptionsProvider(optionsMonitor); + + var result = chaosPolicyConfigProvider.TryGetChaosPolicyOptionsGroup("RandomName", out var optionsGroup); + Assert.False(result); + Assert.Null(optionsGroup); + } + + [Fact] + public void GetChaosPolicyOptionsGroup_NullOptionsGroupName_ShouldThrow() + { + var options = new FaultInjectionOptions(); + var optionsMonitor = Mock.Of>(_ => _.CurrentValue == options); + var chaosPolicyConfigProvider = new FaultInjectionOptionsProvider(optionsMonitor); + + string? optionsGroupName = null; + Assert.Throws(() => chaosPolicyConfigProvider.TryGetChaosPolicyOptionsGroup(optionsGroupName!, out var optionsGroup)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/FaultInjectionTelemetryHandlerTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/FaultInjectionTelemetryHandlerTests.cs new file mode 100644 index 0000000000..78cb7beee2 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/FaultInjectionTelemetryHandlerTests.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metering; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Resilience.FaultInjection.Test.Internals; + +public class FaultInjectionTelemetryHandlerTests +{ + private const string MetricName = @"R9\Resilience\FaultInjection\InjectedFaults"; + + [Fact] + public void LogAndMeter() + { + var logger = Mock.Of>(); + + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + var counter = meter.CreateCounter(MetricName); + var metricCounter = new FaultInjectionMetricCounter(counter); + + const string GroupName = "TestClient"; + const string FaultType = "Type"; + const string InjectedValue = "Value"; + + FaultInjectionTelemetryHandler.LogAndMeter(logger, metricCounter, GroupName, FaultType, InjectedValue); + + var latest = metricCollector.GetCounterValues(MetricName)!.LatestWritten; + + Assert.NotNull(latest); + Assert.Equal(1, latest.Value); + Assert.Equal(GroupName, latest.GetDimension(FaultInjectionEventMeterDimensions.FaultInjectionGroupName)); + Assert.Equal(FaultType, latest.GetDimension(FaultInjectionEventMeterDimensions.FaultType)); + Assert.Equal(InjectedValue, latest.GetDimension(FaultInjectionEventMeterDimensions.InjectedValue)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/WeightAssignmentHelperTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/WeightAssignmentHelperTest.cs new file mode 100644 index 0000000000..cb00550c79 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Internals/WeightAssignmentHelperTest.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Extensions.Resilience.FaultInjection.Test.Internals; + +public class WeightAssignmentHelperTest +{ + [Fact] + public void GetWeightSum_ShouldReturnWeightSum() + { + var weights = new Dictionary + { + { "TestA", 3 }, + { "TestB", 7 } + }; + + var result = WeightAssignmentHelper.GetWeightSum(weights); + Assert.Equal(10, result); + } + + [Fact] + public void GenerateRandom_NumberWithinRange() + { + var result = WeightAssignmentHelper.GenerateRandom(10); + + Assert.True(result <= 10); + Assert.True(result >= 0); + } + + [Fact] + public void GenerateRandom_Mutation_Check() + { + var result = WeightAssignmentHelper.GenerateRandom(0); + + Assert.Equal(0, result); + } + + [Fact] + public void IsUnderMax_ValueUnderMax_ShouldReturnTrue() + { + var result = WeightAssignmentHelper.IsUnderMax(2, 5); + + Assert.True(result); + } + + [Fact] + public void IsUnderMax_ValueOverMax_ShouldReturnFalse() + { + var result = WeightAssignmentHelper.IsUnderMax(6, 5); + + Assert.False(result); + } + + [Fact] + public void IsUnderMax_ValueEqualsMax_ShouldReturnTrue() + { + var result = WeightAssignmentHelper.IsUnderMax(2, 2); + + Assert.True(result); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/ChaosPolicyOptionsGroupTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/ChaosPolicyOptionsGroupTest.cs new file mode 100644 index 0000000000..0e9146900e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/ChaosPolicyOptionsGroupTest.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Resilience.FaultInjection.Test.Options; + +public class ChaosPolicyOptionsGroupTest +{ + [Fact] + public void CanConstruct() + { + var optionsGroup = new ChaosPolicyOptionsGroup(); + Assert.NotNull(optionsGroup); + } + + [Fact] + public void CanSetAndGetLatencyPolicyOptions() + { + var testLatencyOptions = new LatencyPolicyOptions(); + var optionsGroup = new ChaosPolicyOptionsGroup + { + LatencyPolicyOptions = testLatencyOptions + }; + Assert.Equal(optionsGroup.LatencyPolicyOptions, testLatencyOptions); + } + + [Fact] + public void CanSetAndGetHttpResponseInjectionPolicyOptions() + { + var testHttpOptions = new HttpResponseInjectionPolicyOptions(); + var optionsGroup = new ChaosPolicyOptionsGroup + { + HttpResponseInjectionPolicyOptions = testHttpOptions + }; + Assert.Equal(optionsGroup.HttpResponseInjectionPolicyOptions, testHttpOptions); + } + + [Fact] + public void CanSetAndGetExceptionPolicyOptions() + { + var testExceptionOptions = new ExceptionPolicyOptions(); + var optionsGroup = new ChaosPolicyOptionsGroup + { + ExceptionPolicyOptions = testExceptionOptions + }; + Assert.Equal(optionsGroup.ExceptionPolicyOptions, testExceptionOptions); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/ExceptionPolicyOptionTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/ExceptionPolicyOptionTest.cs new file mode 100644 index 0000000000..96c7ce8df4 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/ExceptionPolicyOptionTest.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Resilience.FaultInjection.Test.Options; + +public class ExceptionPolicyOptionTest +{ + [Fact] + public void CanConstruct() + { + var instance = new ExceptionPolicyOptions(); + Assert.NotNull(instance); + } + + [Fact] + public void CanSetAndGetEnabled() + { + var testValue = true; + var instance = new ExceptionPolicyOptions + { + Enabled = testValue + }; + Assert.Equal(testValue, instance.Enabled); + } + + [Fact] + public void CanSetAndGetInjectionRate() + { + var testValue = 0.7; + var instance = new ExceptionPolicyOptions + { + FaultInjectionRate = testValue + }; + Assert.Equal(testValue, instance.FaultInjectionRate); + } + + [Fact] + public void CanSetAndGetExceptionToInject() + { + var testValue = "SocketException"; + var instance = new ExceptionPolicyOptions + { + ExceptionKey = testValue + }; + Assert.Equal(testValue, instance.ExceptionKey); + } + + [Fact] + public void InstanceHasDefaultValues() + { + var instance = new ExceptionPolicyOptions(); + Assert.Equal(ChaosPolicyOptionsBase.DefaultEnabled, instance.Enabled); + Assert.Equal(ChaosPolicyOptionsBase.DefaultInjectionRate, instance.FaultInjectionRate); + Assert.Equal(ExceptionPolicyOptions.DefaultExceptionKey, instance.ExceptionKey); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/FaultInjectionOptionsTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/FaultInjectionOptionsTest.cs new file mode 100644 index 0000000000..4c6d974ad4 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/FaultInjectionOptionsTest.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Extensions.Resilience.FaultInjection.Test.Options; + +public class FaultInjectionOptionsTest +{ + [Fact] + public void CanConstruct() + { + var instance = new FaultInjectionOptions(); + Assert.NotNull(instance); + } + + [Fact] + public void CanGetAndSetChaosPolicyOptionsGroups() + { + var chaosPolicyOptionsGroups = new Dictionary(); + var instance = new FaultInjectionOptions + { + ChaosPolicyOptionsGroups = chaosPolicyOptionsGroups + }; + + Assert.Equal(chaosPolicyOptionsGroups, instance.ChaosPolicyOptionsGroups); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/HttpResponseInjectionPolicyOptionTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/HttpResponseInjectionPolicyOptionTest.cs new file mode 100644 index 0000000000..b6a1c3dd8f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/HttpResponseInjectionPolicyOptionTest.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using Xunit; + +namespace Microsoft.Extensions.Resilience.FaultInjection.Test.Options; + +public class HttpResponseInjectionPolicyOptionTest +{ + [Fact] + public void CanConstruct() + { + var instance = new HttpResponseInjectionPolicyOptions(); + Assert.NotNull(instance); + } + + [Fact] + public void CanSetAndGetEnabled() + { + const bool TestValue = true; + var instance = new HttpResponseInjectionPolicyOptions + { + Enabled = TestValue + }; + Assert.Equal(TestValue, instance.Enabled); + } + + [Fact] + public void CanSetAndGetInjectionRate() + { + const double TestValue = 0.7; + var instance = new HttpResponseInjectionPolicyOptions + { + FaultInjectionRate = TestValue + }; + Assert.Equal(TestValue, instance.FaultInjectionRate); + } + + [Fact] + public void CanSetAndGetStatusCode() + { + const HttpStatusCode TestValue = HttpStatusCode.BadRequest; + var instance = new HttpResponseInjectionPolicyOptions + { + StatusCode = TestValue + }; + Assert.Equal(TestValue, instance.StatusCode); + } + + [Fact] + public void InstanceHasDefaultValues() + { + var instance = new HttpResponseInjectionPolicyOptions(); + Assert.Equal(ChaosPolicyOptionsBase.DefaultEnabled, instance.Enabled); + Assert.Equal(ChaosPolicyOptionsBase.DefaultInjectionRate, instance.FaultInjectionRate); + Assert.Equal(HttpResponseInjectionPolicyOptions.DefaultStatusCode, instance.StatusCode); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/LatencyPolicyOptionTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/LatencyPolicyOptionTest.cs new file mode 100644 index 0000000000..cd13250803 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/LatencyPolicyOptionTest.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.Extensions.Resilience.FaultInjection.Test.Options; + +public class LatencyPolicyOptionTest +{ + [Fact] + public void CanConstruct() + { + var instance = new LatencyPolicyOptions(); + Assert.NotNull(instance); + } + + [Fact] + public void CanSetAndGetEnabled() + { + const bool TestValue = true; + var instance = new LatencyPolicyOptions + { + Enabled = TestValue + }; + Assert.Equal(TestValue, instance.Enabled); + } + + [Fact] + public void CanSetAndGetInjectionRate() + { + const double TestValue = 0.7; + var instance = new LatencyPolicyOptions + { + FaultInjectionRate = TestValue + }; + Assert.Equal(TestValue, instance.FaultInjectionRate); + } + + [Fact] + public void CanSetAndGetLatency() + { + var testValue = TimeSpan.FromSeconds(40); + var instance = new LatencyPolicyOptions + { + Latency = testValue + }; + Assert.Equal(testValue, instance.Latency); + } + + [Fact] + public void InstanceHasDefaultValues() + { + var instance = new LatencyPolicyOptions(); + Assert.Equal(ChaosPolicyOptionsBase.DefaultEnabled, instance.Enabled); + Assert.Equal(ChaosPolicyOptionsBase.DefaultInjectionRate, instance.FaultInjectionRate); + Assert.Equal(LatencyPolicyOptions.DefaultLatency, instance.Latency); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/OptionsValidationTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/OptionsValidationTests.cs new file mode 100644 index 0000000000..c6e2c5e6f0 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/FaultInjection/Options/OptionsValidationTests.cs @@ -0,0 +1,133 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Resilience.FaultInjection.Test.Options; + +public class OptionsValidationTests +{ + private readonly IConfiguration _configurationWithPolicyOptions; + + public OptionsValidationTests() + { + var builder = new ConfigurationBuilder().AddJsonFile("configs/appsettings.json"); + _configurationWithPolicyOptions = builder.Build(); + } + + [Fact] + public void ChaosPolicyOptionsValidator_HttpResponseInjectionPolicyOptions_InjectionRateOutOfRange_ShouldReturnFailure() + { + var options = new FaultInjectionOptions(); + _configurationWithPolicyOptions + .GetSection("ChaosPolicyOptionsGroupsNegativeTest1") + .Bind(options); + + var exception = Assert.Throws(() => Validate(options)); + Assert.Equal("The field FaultInjectionRate must be between 0 and 1.", string.Join("", exception!.Failures)); + } + + [Fact] + public void ChaosPolicyOptionsValidator_ExceptionPolicyOptions_InjectionRateOutOfRange_ShouldReturnFailure() + { + var options = new FaultInjectionOptions(); + _configurationWithPolicyOptions + .GetSection("ChaosPolicyOptionsGroupsNegativeTest2") + .Bind(options); + + var exception = Assert.Throws(() => Validate(options)); + Assert.Equal("The field FaultInjectionRate must be between 0 and 1.", string.Join("", exception!.Failures)); + } + + [Fact] + public void ChaosPolicyOptionsValidator_LatencyPolicyOptions_InjectionRateOutOfRange_ShouldReturnFailure() + { + var options = new FaultInjectionOptions(); + _configurationWithPolicyOptions + .GetSection("ChaosPolicyOptionsGroupsNegativeTest3") + .Bind(options); + + var exception = Assert.Throws(() => Validate(options)); + Assert.Equal("The field FaultInjectionRate must be between 0 and 1.", string.Join("", exception!.Failures)); + } + + [Fact] + public void ChaosPolicyOptionsValidator_LatencyPolicyOptions_LatencyOutOfRange_ShouldReturnFailure() + { + var options = new FaultInjectionOptions(); + _configurationWithPolicyOptions + .GetSection("ChaosPolicyOptionsGroupsNegativeTest4") + .Bind(options); + + var exception = Assert.Throws(() => Validate(options)); + Assert.Equal("The field Latency must be <= to 00:10:00.", string.Join("", exception!.Failures)); + } + + [Fact] + public void ChaosPolicyOptionsValidator_HttpResponseInjectionPolicyOptions_StatusCodeOutOfRange_ShouldReturnFailure() + { + var options = new FaultInjectionOptions(); + _configurationWithPolicyOptions + .GetSection("ChaosPolicyOptionsGroupsNegativeTest5") + .Bind(options); + + var exception = Assert.Throws(() => Validate(options)); + Assert.Equal("The field StatusCode is invalid.", string.Join("", exception!.Failures)); + } + + [Fact] + public void ChaosPolicyOptionsValidator_NoOptionsGroupField_ShouldBeAllowed() + { + var options = new FaultInjectionOptions(); + _configurationWithPolicyOptions + .GetSection("ChaosPolicyOptionsGroupsTestNoOptionsGroup") + .Bind(options); + + Validate(options); + Assert.Empty(options.ChaosPolicyOptionsGroups); + } + + [Fact] + public void ChaosPolicyOptionsValidator_MultipleErrors() + { + var options = new FaultInjectionOptions(); + _configurationWithPolicyOptions + .GetSection("ChaosPolicyOptionsGroupsNegativeTestMultipleErrors") + .Bind(options); + + var exception = Assert.Throws(() => Validate(options)); + Assert.Equal( + "The field Latency must be <= to 00:10:00.; " + + "The field FaultInjectionRate must be between 0 and 1.", string.Join("; ", exception!.Failures)); + } + + [Fact] + public void GenerateFailureMessages_NullErrorMessages_ShouldReturnUnknownError() + { + var results = new List + { + new ValidationResult(null!) + }; + var messages = FaultInjectionOptionsValidator.GenerateFailureMessages(results); + + Assert.Equal("Unknown Error", string.Join("", messages)); + } + + [Fact] + public void GenerateFailureMessages_EmptyValidationResults_ShouldReturnEmptyCollection() + { + var messages = FaultInjectionOptionsValidator.GenerateFailureMessages(new List()); + + Assert.Empty(messages); + } + + private static void Validate(FaultInjectionOptions options) + { + var optionsValidator = new FaultInjectionOptionsValidator(); + optionsValidator.Validate(Microsoft.Extensions.Options.Options.DefaultName, options); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Microsoft.Extensions.Resilience.Tests.csproj b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Microsoft.Extensions.Resilience.Tests.csproj new file mode 100644 index 0000000000..f05cef1848 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Microsoft.Extensions.Resilience.Tests.csproj @@ -0,0 +1,37 @@ + + + Microsoft.Extensions.Resilience.Test + Unit tests for Microsoft.Extensions.Resilience + + + + true + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/GlobalSuppressions.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/GlobalSuppressions.cs new file mode 100644 index 0000000000..45a3bb509d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/GlobalSuppressions.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Major Code Smell", "S104:Files should not have too many lines of code", Justification = "Combined two factories hence increasing the APIs to be tested")] diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/AsyncHedgingPolicyTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/AsyncHedgingPolicyTests.cs new file mode 100644 index 0000000000..b25ce01fdd --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/AsyncHedgingPolicyTests.cs @@ -0,0 +1,545 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Data; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Hedging; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Extensions.Resilience.Polly.Test.Helpers; +using Microsoft.Extensions.Time.Testing; +using Polly; +using Polly.Timeout; +using Polly.Utilities; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Hedging; + +[Collection(nameof(ResiliencePollyFakeClockTestsCollection))] +public sealed class AsyncHedgingPolicyTests : IDisposable +{ + private static readonly TimeSpan _longTimeout = TimeSpan.FromDays(31); + private static readonly TimeSpan _assertTimeout = TimeSpan.FromSeconds(30); + + private static readonly List>> _exceptionTasks = + new() + { + (_, _) => GetExceptionAfterDelayAsync(new InvalidCastException(), 10), + (_, _) => GetExceptionAfterDelayAsync(new InvalidOperationException(), 5), + (_, _) => GetExceptionAfterDelayAsync(new ArgumentException(), 1) + }; + + private readonly Context _context; + private readonly CancellationTokenSource _cts; + private readonly FakeTimeProvider _timeProvider; + + public AsyncHedgingPolicyTests() + { + _context = new Context(); + _cts = new CancellationTokenSource(); + _timeProvider = new FakeTimeProvider(); + SystemClock.SleepAsync = _timeProvider.DelayAndAdvanceAsync; + } + + public void Dispose() + { + _cts.Dispose(); + SystemClock.Reset(); + } + + [Fact] + public void Constructor_ShouldCreatePolicy() + { + var policyBuilder = Policy.HandleResult(_ => false); + var hedgingPolicy = new AsyncHedgingPolicy( + policyBuilder, + HedgingTestUtilities.HedgedTasksHandler.FunctionsProvider, + HedgingTestUtilities.HedgedTasksHandler.Functions.Count + 1, + HedgingTestUtilities.DefaultHedgingDelayGenerator, + HedgingTestUtilities.EmptyOnHedgingTask); + + Assert.NotNull(hedgingPolicy); + } + + [Fact] + public void ExecuteAsync_ZeroHedgingDelay_EnsureAllTasksSpawnedAtOnce() + { + int executions = 0; + using var allExecutionsReached = new ManualResetEvent(false); + var hedgingPolicy = new AsyncHedgingPolicy( + Policy.HandleResult(result => false), + HedgingTestUtilities.HedgedTasksHandler.GetCustomTaskProvider(Execute), + 3, + _ => TimeSpan.Zero, + HedgingTestUtilities.EmptyOnHedgingTask); + + _ = hedgingPolicy.ExecuteAsync(Execute, _cts.Token); + + Assert.True(allExecutionsReached.WaitOne(_assertTimeout)); + + async Task Execute(CancellationToken token) + { + if (Interlocked.Increment(ref executions) == 3) + { + allExecutionsReached.Set(); + } + + await _timeProvider.Delay(_longTimeout, token); + return "dummy"; + } + } + + [Fact] + public void ExecuteAsync_InfiniteHedgingDelay_EnsureNoConcurrentExecutions() + { + bool executing = false; + int executions = 0; + using var allExecutions = new ManualResetEvent(true); + var hedgingPolicy = new AsyncHedgingPolicy( + Policy.HandleResult(result => true), + HedgingTestUtilities.HedgedTasksHandler.GetCustomTaskProvider(Execute), + 3, + _ => TimeSpan.FromMilliseconds(-1), + HedgingTestUtilities.EmptyOnHedgingTask); + + var pending = hedgingPolicy.ExecuteAsync(Execute, _cts.Token); + + Assert.True(allExecutions.WaitOne(_assertTimeout)); + + async Task Execute(CancellationToken token) + { + if (Interlocked.Increment(ref executions) == 3) + { + allExecutions.Set(); + } + + if (executing) + { + throw new InvalidOperationException("Concurrent execution detected!"); + } + + await SystemClock.SleepAsync(TimeSpan.FromHours(1), token); + + return "dummy"; + } + } + + [Fact] + public async Task ExecuteAsync_InfiniteHedgingDelay_EnsureSecondRetrySuccesfull() + { + var hedgingPolicy = new AsyncHedgingPolicy( + Policy.HandleResult(result => true).Or(), + HedgingTestUtilities.HedgedTasksHandler.GetCustomTaskProvider(Execute), + 2, + _ => TimeSpan.FromMilliseconds(-1), + HedgingTestUtilities.EmptyOnHedgingTask); + + var task = hedgingPolicy.ExecuteAsync( + async (token) => + { + await _timeProvider.Delay(TimeSpan.FromSeconds(10), token); + throw new TimeoutRejectedException(); + }, + _cts.Token); + + // let the task do some work first + Assert.False(task.Wait(10)); + + // advance the time + _timeProvider.Advance(TimeSpan.FromSeconds(15)); + + Assert.Equal("success", await task); + + static Task Execute(CancellationToken token) => Task.FromResult("success"); + } + + [Fact] + public async Task ExecuteAsync_PrimaryTaskIsFailure_ShouldReturnDifferentNextOne() + { + var policyBuilder = Policy.HandleResult(result => result == HedgingTestUtilities.PrimaryStringTasks.FastTaskResult); + + var hedgingPolicy = new AsyncHedgingPolicy( + policyBuilder, + HedgingTestUtilities.HedgedTasksHandler.FunctionsProvider, + HedgingTestUtilities.HedgedTasksHandler.Functions.Count + 1, + HedgingTestUtilities.DefaultHedgingDelayGenerator, + HedgingTestUtilities.EmptyOnHedgingTask); + + var result = await hedgingPolicy.ExecuteAsync(() => HedgingTestUtilities.PrimaryStringTasks.FastTask(_cts.Token)); + + Assert.NotNull(result); + Assert.NotEqual(HedgingTestUtilities.PrimaryStringTasks.FastTaskResult, result); + + Assert.Contains(result, new[] { "Oranges", "Apples" }); + } + + [Fact] + public async Task ExecuteAsync_ProviderReturnsNullTaskWhenPreviousTasksNotCompleted_ShouldReturn() + { + var policyBuilder = Policy.HandleResult(result => false); + + var hedgingPolicy = new AsyncHedgingPolicy( + policyBuilder, + (HedgingTaskProviderArguments htpa, out Task? result) => + { + if (htpa.AttemptNumber != 1) + { + return HedgingTestUtilities.HedgedTasksHandler.FunctionsProvider(htpa, out result); + } + + result = null; + return false; + }, + HedgingTestUtilities.HedgedTasksHandler.Functions.Count + 1, + HedgingTestUtilities.DefaultHedgingDelayGenerator, + HedgingTestUtilities.EmptyOnHedgingTask); + + var result = await hedgingPolicy.ExecuteAsync( + () => + HedgingTestUtilities.PrimaryStringTasks.SlowTask(_context, _cts.Token)); + + Assert.NotNull(result); + Assert.Equal("I am so slow!", result); + } + + [Fact] + public async Task ExecuteAsync_ProviderReturnsNullTaskWhenPreviousTaskAlreadyCompleted_ShouldNotThrow() + { + var policyBuilder = Policy.HandleResult(result => false); + + var hedgingPolicy = new AsyncHedgingPolicy( + policyBuilder, + (HedgingTaskProviderArguments htpa, out Task? result) => + { + if (htpa.AttemptNumber != 4) + { + return HedgingTestUtilities.HedgedTasksHandler.FunctionsProvider(htpa, out result); + } + + result = null; + return false; + }, + HedgingTestUtilities.HedgedTasksHandler.Functions.Count + 1, + HedgingTestUtilities.DefaultHedgingDelayGenerator, + HedgingTestUtilities.EmptyOnHedgingTask); + + var result = await hedgingPolicy.ExecuteAsync( + () => + HedgingTestUtilities.PrimaryStringTasks.InstantTask()); + + Assert.NotNull(result); + Assert.Equal(HedgingTestUtilities.PrimaryStringTasks.InstantTaskResult, result); + } + + [Fact] + public async Task ExecuteAsync_AllExceptionsHandledWhenTaskThrows_ShouldThrowAnyException() + { + var policyBuilder = Policy + .HandleResult(_ => false) + .Or() + .Or() + .Or() + .Or(); + + var hedgingPolicy = GetHedgingPolicyWithExceptions(policyBuilder); + var ex = await Assert.ThrowsAnyAsync( + () => hedgingPolicy.ExecuteAsync( + () => GetExceptionAfterDelayAsync(new BadImageFormatException(), 20)).AdvanceTimeUntilFinished(_timeProvider)); + + Assert.True(ex is InvalidCastException || + ex is ArgumentException || + ex is InvalidOperationException || + ex is BadImageFormatException, + "The exception must be of type one of the thrown"); + } + + [Fact] + public async Task ExecuteAsync_AllExceptionsHandledWhenProviderThrows_ShouldThrowLastException() + { + var policyBuilder = Policy + .HandleResult(_ => false) + .Or() + .Or(); + + var exceptionTasks = new List>> + { + (_, _) => throw new BadImageFormatException(), + (_, _) => throw new BadImageFormatException(), + (_, _) => throw new ArgumentException("Expected exception.") + }; + + var hedgingPolicy = GetHedgingPolicyWithExceptions(policyBuilder, exceptionTasks); + var exception = await Assert.ThrowsAsync( + () => hedgingPolicy.ExecuteAsync( + () => throw new BadImageFormatException())); + Assert.Equal("Expected exception.", exception.Message); + } + + [Fact] + public async Task ExecuteAsync_AllExceptionsHandledButOne_ShouldThrowTheNonHandledOne() + { + var policyBuilder = Policy + .HandleResult(_ => false) + .Or() + .Or() + .Or(); + + var hedgingPolicy = + GetHedgingPolicyWithExceptions( + policyBuilder, + new List>> + { + (_, _)=> throw new InvalidCastException(), + (_, _)=> throw new ArgumentException(), + (_, _)=> throw new ReadOnlyException(), + }, + generator: _ => TimeSpan.Zero); + + await Assert.ThrowsAsync(() => hedgingPolicy.ExecuteAsync(() => throw new BadImageFormatException())); + } + + [Fact] + public async Task ExecuteAsync_AllExceptionsHandledButFirst_ShouldThrowTheFirstOne() + { + var policyBuilder = Policy + .HandleResult(_ => false) + .Or() + .Or() + .Or(); + + var hedgingPolicy = GetHedgingPolicyWithExceptions(policyBuilder); + await Assert.ThrowsAsync( + () => hedgingPolicy.ExecuteAsync( + () => + GetExceptionAfterDelayAsync(new BadImageFormatException()))); + } + + [Fact] + public async Task ExecuteAsync_AllResultsAndExceptionssHandledButOneException_ShouldThrowUnhandledException() + { + using var expectedResponse = new HttpResponseMessage(HttpStatusCode.BadRequest); + var policyBuilder = Policy + .HandleResult(response => true) + .Or() + .Or(); + + var exceptionTasks = new List>> + { + (_, _) => throw new InvalidCastException(), + (_, _) => throw new InvalidOperationException(), + (_, _) => throw new ArgumentException() + }; + + var hedgingPolicy = GetHedgingPolicyWithExceptions(policyBuilder, exceptionTasks); + await Assert.ThrowsAsync( + () => hedgingPolicy.ExecuteAsync( + async () => + { + await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(100), + CancellationToken.None); + return expectedResponse; + })); + } + + [InlineData(false)] + [InlineData(true)] + [Theory] + public async Task ExecuteAsync_AllResultsHandledButNoExceptionHandled_ShouldThrowFirstException(bool delay) + { + using var expectedResponse = new HttpResponseMessage(HttpStatusCode.BadRequest); + var policyBuilder = Policy.HandleResult(response => true); + + var executeTime = delay ? (int)HedgingTestUtilities.DefaultHedgingDelay.TotalSeconds * 2 : 0; + + var exceptionTasks = new List>> + { + (_, t) => GetExceptionTaskAsync(new InvalidCastException(), TimeSpan.FromDays(1), t), + (_, t) => GetExceptionTaskAsync(new BadImageFormatException(), TimeSpan.FromDays(3), t), + (_, t) => GetExceptionTaskAsync(new BadImageFormatException(), TimeSpan.FromDays(3), t), + }; + + var hedgingPolicy = GetHedgingPolicyWithExceptions(policyBuilder, exceptionTasks); + + await Assert.ThrowsAsync(() => + hedgingPolicy.ExecuteAsync(() => Task.FromResult(expectedResponse)).AdvanceTimeUntilFinished(_timeProvider, TimeSpan.FromHours(1), TimeSpan.FromDays(2))); + + async Task GetExceptionTaskAsync(Exception ex, TimeSpan delay, CancellationToken cancellationToken) + { + await _timeProvider.Delay(delay, cancellationToken); + + throw ex; + } + } + + [Fact] + public async Task ExecuteAsync_CancellationRequested_ShouldThrowOperationCancelledException() + { + var policyBuilder = Policy.HandleResult(_ => false); + var hedgedTaskProvider = new HedgedTaskProvider((HedgingTaskProviderArguments _, out Task? _) => throw new NotSupportedException()); + var hedgingPolicy = new AsyncHedgingPolicy( + policyBuilder, + hedgedTaskProvider, + 1, + HedgingTestUtilities.DefaultHedgingDelayGenerator, + HedgingTestUtilities.EmptyOnHedgingTask); + + _cts.Cancel(); + + var error = await Assert.ThrowsAsync(() => hedgingPolicy.ExecuteAsync(_ => Task.FromResult("dummy-result"), _cts.Token)); + + // hedging implementation creates linked token once it starts executing + // in this case we want to check the original canceled token was respected + Assert.Equal(_cts.Token, error.CancellationToken); + } + + [Fact] + public void ExecuteAsync_EnsureHedgingDelayGeneratorRespected() + { + using var attemptsReached = new ManualResetEvent(false); + var calls = 0; + var argsList = new List(); + var hedgingPolicy = new AsyncHedgingPolicy( + Policy.HandleResult(_ => true), + HedgingTestUtilities.HedgedTasksHandler.GetCustomTaskProvider(Execute), + 3, + GetHedgingDelay, + HedgingTestUtilities.EmptyOnHedgingTask); + + _ = hedgingPolicy.ExecuteAsync(Execute, _cts.Token); + + Assert.True(attemptsReached.WaitOne(TimeSpan.FromMinutes(1))); + +#pragma warning disable S4158 // Empty collections should not be accessed or iterated + Assert.Equal(0, argsList[0].AttemptNumber); + Assert.Equal(1, argsList[1].AttemptNumber); +#pragma warning restore S4158 // Empty collections should not be accessed or iterated + + TimeSpan GetHedgingDelay(HedgingDelayArguments args) + { + argsList.Add(args); + if (Interlocked.Increment(ref calls) == 2) + { + attemptsReached.Set(); + } + + return TimeSpan.FromSeconds(1); + } + + async Task Execute(CancellationToken token) + { + await _timeProvider.Delay(TimeSpan.FromDays(1), token); + return "dummy"; + } + } + + [Fact(Skip = "Flaky")] + public async Task ExecuteAsync_NegativeHedgingDelay_EnsureRespected() + { + var called = false; + var hedgingPolicy = new AsyncHedgingPolicy( + Policy.HandleResult(_ => true), + HedgingTestUtilities.HedgedTasksHandler.GetCustomTaskProvider(Execute), + 3, + (_) => + { + called = true; + return TimeSpan.FromSeconds(-5); + }, + HedgingTestUtilities.EmptyOnHedgingTask); + + await hedgingPolicy.ExecuteAsync(Execute, _cts.Token).AdvanceTimeUntilFinished(_timeProvider); + + Assert.True(called); + + async Task Execute(CancellationToken token) + { + await _timeProvider.Delay(TimeSpan.FromDays(1), token); + return "dummy"; + } + } + + [Fact] + public async Task ExecuteAsync_ExceptionCaptured_EnsureStackTrace() + { + // arrange + var policyBuilder = Policy.Handle(_ => true).OrResult(v => true); + var hedgingPolicy = new AsyncHedgingPolicy( + policyBuilder, + NextTask, + 3, + (_) => + { + return TimeSpan.FromSeconds(-5); + }, + HedgingTestUtilities.EmptyOnHedgingTask); + + // act + var error = await Assert.ThrowsAsync(() => hedgingPolicy.ExecuteAsync(PrimaryTaskThatThrowsError)); + + // assert + Assert.Equal("Forced Error", error.Message); + Assert.Contains(nameof(PrimaryTaskThatThrowsError), error.StackTrace); + + Task PrimaryTaskThatThrowsError() => throw new InvalidOperationException("Forced Error"); + + bool NextTask(HedgingTaskProviderArguments arguments, out Task task) + { + task = null!; + return false; + } + } + + [Fact] + public async Task ExecuteAsync_OnHedgingDelayThrows_EnsureFirstResultDisposed() + { + using var firstResult = new DisposableResult(); + + var hedgingPolicy = new AsyncHedgingPolicy( + Policy.HandleResult(_ => true), + HedgingTestUtilities.HedgedTasksHandler.GetCustomTaskProvider(Execute), + 3, + (_) => TimeSpan.FromSeconds(1), + (_, _, _, _) => throw new InvalidOperationException("on hedging fail")); + + var error = await Assert.ThrowsAsync(() => hedgingPolicy.ExecuteAsync(Execute, _cts.Token)); + Assert.Equal("on hedging fail", error.Message); + + Assert.True(firstResult.IsDisposed); + + Task Execute(CancellationToken token) => Task.FromResult(firstResult); + } + + private static async Task GetExceptionAfterDelayAsync(Exception ex, int delayInSeconds = 0) + { + if (delayInSeconds != 0) + { + await SystemClock.SleepAsync(TimeSpan.FromSeconds(delayInSeconds), CancellationToken.None); + } + + throw ex; + } + + private static AsyncHedgingPolicy GetHedgingPolicyWithExceptions( + PolicyBuilder policyBuilder, + List>>? exceptionTasks = null, + Func? generator = null) + { + bool HedgedTaskProvider(HedgingTaskProviderArguments args, out Task? result) + { + result = exceptionTasks![args.AttemptNumber - 1].Invoke(args.Context, args.CancellationToken); + return true; + } + + exceptionTasks ??= _exceptionTasks; + return new AsyncHedgingPolicy( + policyBuilder, + HedgedTaskProvider, + _exceptionTasks.Count + 1, + generator ?? HedgingTestUtilities.DefaultHedgingDelayGenerator, + HedgingTestUtilities.EmptyOnHedgingTask); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/AsyncHedgingPolicyTestsNonGeneric.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/AsyncHedgingPolicyTestsNonGeneric.cs new file mode 100644 index 0000000000..e03692743a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/AsyncHedgingPolicyTestsNonGeneric.cs @@ -0,0 +1,401 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Hedging; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Extensions.Time.Testing; +using Polly; +using Polly.Utilities; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Hedging; + +[Collection(nameof(ResiliencePollyFakeClockTestsCollection))] +public sealed class AsyncHedgingPolicyTestsNonGeneric : IDisposable +{ + private static readonly TimeSpan _longTimeout = TimeSpan.FromDays(365); + private static readonly TimeSpan _assertTimeout = TimeSpan.FromSeconds(30); + + private static readonly List>> _exceptionTasks = + new() + { + (_, _) => GetExceptionAfterDelayAsync(new InvalidCastException(), 10), + (_, _) => GetExceptionAfterDelayAsync(new InvalidOperationException(), 5), + (_, _) => GetExceptionAfterDelayAsync(new ArgumentException(), 1) + }; + + private readonly Context _context; + private readonly CancellationTokenSource _cts; + private readonly FakeTimeProvider _timeProvider; + + public AsyncHedgingPolicyTestsNonGeneric() + { + _context = new Context(); + _cts = new CancellationTokenSource(); + _timeProvider = new FakeTimeProvider(); + SystemClock.SleepAsync = _timeProvider.DelayAndAdvanceAsync; + } + + public void Dispose() + { + _cts.Dispose(); + SystemClock.Reset(); + } + + [Fact] + public void Constructor_ShouldCreatePolicy() + { + var policyBuilder = Policy.Handle(); + var hedgingPolicy = new AsyncHedgingPolicy( + policyBuilder, + HedgingTestUtilities.HedgedTasksHandler.FunctionsProvider, + HedgingTestUtilities.HedgedTasksHandler.Functions.Count + 1, + HedgingTestUtilities.DefaultHedgingDelayGenerator, + HedgingTestUtilities.EmptyOnHedgingTask); + + Assert.NotNull(hedgingPolicy); + } + + [Fact] + public void ExecuteAsync_ZeroHedgingDelay_EnsureAllTasksSpawnedAtOnce() + { + int executions = 0; + using var allExecutionsReached = new ManualResetEvent(false); + var hedgingPolicy = new AsyncHedgingPolicy( + Policy.Handle(), + HedgingTestUtilities.HedgedTasksHandler.GetCustomTaskProvider(Execute), + 3, + _ => TimeSpan.Zero, + HedgingTestUtilities.EmptyOnHedgingTask); + + _ = hedgingPolicy.ExecuteAsync(Execute, _cts.Token); + + Assert.True(allExecutionsReached.WaitOne(_assertTimeout)); + + async Task Execute(CancellationToken token) + { + if (Interlocked.Increment(ref executions) == 3) + { + allExecutionsReached.Set(); + } + + await _timeProvider.Delay(_longTimeout, token); + return EmptyStruct.Instance; + } + } + + [Fact] + public void ExecuteAsync_InfiniteHedgingDelay_EnsureNoConcurrentExecutions() + { + bool executing = false; + int executions = 0; + using var allExecutions = new ManualResetEvent(true); + var hedgingPolicy = new AsyncHedgingPolicy( + Policy.Handle(), + HedgingTestUtilities.HedgedTasksHandler.GetCustomTaskProvider(Execute), + 3, + _ => TimeSpan.FromMilliseconds(-1), + HedgingTestUtilities.EmptyOnHedgingTask); + + var pending = hedgingPolicy.ExecuteAsync(Execute, _cts.Token); + + Assert.True(allExecutions.WaitOne(_assertTimeout)); + + async Task Execute(CancellationToken token) + { + if (Interlocked.Increment(ref executions) == 3) + { + allExecutions.Set(); + } + + if (executing) + { + throw new InvalidOperationException("Concurrent execution detected!"); + } + + await SystemClock.SleepAsync(TimeSpan.FromHours(1), token); + + return EmptyStruct.Instance; + } + } + + [Fact] + public async Task ExecuteAsync_ProviderReturnsNullTaskWhenPreviousTasksNotCompleted_ShouldReturn() + { + var policyBuilder = Policy.Handle(); + + var hedgingPolicy = new AsyncHedgingPolicy( + policyBuilder, + (HedgingTaskProviderArguments htpa, out Task? result) => + { + if (htpa.AttemptNumber != 1) + { + return HedgingTestUtilities.HedgedTasksHandler.FunctionsProvider(htpa, out result); + } + + result = null; + return false; + }, + HedgingTestUtilities.HedgedTasksHandler.Functions.Count + 1, + HedgingTestUtilities.DefaultHedgingDelayGenerator, + HedgingTestUtilities.EmptyOnHedgingTask); + + var result = await hedgingPolicy.ExecuteAsync( + () => + HedgingTestUtilities.PrimaryStringTasks.SlowTask(_context, _cts.Token)); + + Assert.NotNull(result); + Assert.Equal("I am so slow!", result); + } + + [Fact] + public async Task ExecuteAsync_ProviderReturnsNullTaskWhenPreviousTaskAlreadyCompleted_ShouldNotThrow() + { + var policyBuilder = Policy.Handle(); + + var hedgingPolicy = new AsyncHedgingPolicy( + policyBuilder, + (HedgingTaskProviderArguments htpa, out Task? result) => + { + if (htpa.AttemptNumber != 4) + { + return HedgingTestUtilities.HedgedTasksHandler.FunctionsProvider(htpa, out result); + } + + result = null; + return false; + }, + HedgingTestUtilities.HedgedTasksHandler.Functions.Count + 1, + HedgingTestUtilities.DefaultHedgingDelayGenerator, + HedgingTestUtilities.EmptyOnHedgingTask); + + var result = await hedgingPolicy.ExecuteAsync( + () => + HedgingTestUtilities.PrimaryStringTasks.InstantTask()); + + Assert.NotNull(result); + Assert.Equal(HedgingTestUtilities.PrimaryStringTasks.InstantTaskResult, result); + } + + [Fact] + public async Task ExecuteAsync_AllExceptionsHandledWhenTaskThrows_ShouldThrowAnyException() + { + var policyBuilder = Policy.Handle() + .Or() + .Or() + .Or() + .Or(); + + var hedgingPolicy = AsyncHedgingPolicyTestsNonGeneric.GetHedgingPolicyWithExceptions(policyBuilder); + var ex = await Assert.ThrowsAnyAsync( + () => hedgingPolicy.ExecuteAsync(() => + GetExceptionAfterDelayAsync(new BadImageFormatException(), 20)).AdvanceTimeUntilFinished(_timeProvider)); + + Assert.True(ex is InvalidCastException || + ex is ArgumentException || + ex is InvalidOperationException || + ex is BadImageFormatException, + "The exception must be of type one of the thrown"); + } + + [Fact] + public async Task ExecuteAsync_AllExceptionsHandledWhenProviderThrows_ShouldThrowLastException() + { + var policyBuilder = Policy.Handle() + .Or(); + + var exceptionTasks = new List>> + { + (_, _) => throw new BadImageFormatException(), + (_, _) => throw new BadImageFormatException(), + (_, _) => throw new ArgumentException("Expected exception.") + }; + + var hedgingPolicy = GetHedgingPolicyWithExceptions(policyBuilder, exceptionTasks); + var exception = await Assert.ThrowsAsync( + () => hedgingPolicy.ExecuteAsync( + () => throw new BadImageFormatException())); + Assert.Equal("Expected exception.", exception.Message); + } + + [Fact] + public async Task ExecuteAsync_AllExceptionsHandledButOne_ShouldThrowTheNonHandledOne() + { + var policyBuilder = Policy + .Handle() + .Or() + .Or(); + + var hedgingPolicy = + GetHedgingPolicyWithExceptions( + policyBuilder, + new List>> + { + (_, _)=> throw new InvalidCastException(), + (_, _)=> throw new ArgumentException(), + (_, _)=> throw new ReadOnlyException(), + }, + generator: _ => TimeSpan.Zero); + + await Assert.ThrowsAsync(() => hedgingPolicy.ExecuteAsync(() => throw new BadImageFormatException())); + } + + [Fact] + public async Task ExecuteAsync_AllExceptionsHandledButFirst_ShouldThrowTheFirstOne() + { + var policyBuilder = Policy.Handle() + .Or() + .Or(); + + var hedgingPolicy = GetHedgingPolicyWithExceptions(policyBuilder); + await Assert.ThrowsAsync( + () => hedgingPolicy.ExecuteAsync( + () => + GetExceptionAfterDelayAsync(new BadImageFormatException()))); + } + + [InlineData(false)] + [InlineData(true)] + [Theory] + public async Task ExecuteAsync_NoExceptionHandled_ShouldThrowFirstException(bool delay) + { + var policyBuilder = Policy.Handle(e => e is ReadOnlyException); + + var executeTime = delay ? (int)HedgingTestUtilities.DefaultHedgingDelay.TotalSeconds * 2 : 0; + + var exceptionTasks = new List>> + { + (_, t) => GetExceptionTaskAsync(new InvalidCastException(), TimeSpan.FromDays(1), t), + (_, t) => GetExceptionTaskAsync(new BadImageFormatException(), TimeSpan.FromDays(3), t), + (_, t) => GetExceptionTaskAsync(new BadImageFormatException(), TimeSpan.FromDays(3), t), + }; + + var hedgingPolicy = GetHedgingPolicyWithExceptions(policyBuilder, exceptionTasks); + + await Assert.ThrowsAsync(() => hedgingPolicy + .ExecuteAsync(() => throw new ReadOnlyException()) + .AdvanceTimeUntilFinished(_timeProvider, TimeSpan.FromHours(1), TimeSpan.FromDays(2))); + + async Task GetExceptionTaskAsync(Exception ex, TimeSpan delay, CancellationToken cancellationToken) + { + await _timeProvider.Delay(delay, cancellationToken); + + throw ex; + } + } + + [Fact] + public async Task ExecuteAsync_CancellationRequested_ShouldThrowOperationCancelledException() + { + var policyBuilder = Policy.Handle(); + var hedgedTaskProvider = new HedgedTaskProvider((HedgingTaskProviderArguments _, out Task? _) => throw new NotSupportedException()); + var hedgingPolicy = new AsyncHedgingPolicy( + policyBuilder, + hedgedTaskProvider, + 1, + HedgingTestUtilities.DefaultHedgingDelayGenerator, + HedgingTestUtilities.EmptyOnHedgingTask); + + _cts.Cancel(); + + var error = await Assert.ThrowsAsync(() => hedgingPolicy.ExecuteAsync(_ => Task.FromResult("dummy-result"), _cts.Token)); + + // hedging implementation creates linked token once it starts executing + // in this case we want to check the original canceled token was respected + Assert.Equal(_cts.Token, error.CancellationToken); + } + + [Fact] + public void ExecuteAsync_EnsureHedgingDelayGeneratorRespected() + { + using var attemptsReached = new ManualResetEvent(false); + var calls = 0; + var hedgingPolicy = new AsyncHedgingPolicy( + Policy.Handle(), + HedgingTestUtilities.HedgedTasksHandler.GetCustomTaskProvider(Execute), + 3, + GetHedgingDelay, + HedgingTestUtilities.EmptyOnHedgingTask); + + _ = hedgingPolicy.ExecuteAsync(Execute, _cts.Token); + + Assert.True(attemptsReached.WaitOne(TimeSpan.FromMinutes(1))); + + TimeSpan GetHedgingDelay(HedgingDelayArguments args) + { + if (Interlocked.Increment(ref calls) == 2) + { + attemptsReached.Set(); + } + + return TimeSpan.FromSeconds(1); + } + + async Task Execute(CancellationToken token) + { + await _timeProvider.Delay(TimeSpan.FromDays(1), token); + return EmptyStruct.Instance; + } + } + + [Fact] + public async Task ExecuteAsync_NegativeHedgingDelay_EnsureRespected() + { + var called = false; + var hedgingPolicy = new AsyncHedgingPolicy( + Policy.Handle(), + HedgingTestUtilities.HedgedTasksHandler.GetCustomTaskProvider(Execute), + 3, + (_) => + { + called = true; + return TimeSpan.FromSeconds(-5); + }, + HedgingTestUtilities.EmptyOnHedgingTask); + + await hedgingPolicy.ExecuteAsync(Execute, _cts.Token).AdvanceTimeUntilFinished(_timeProvider); + + Assert.True(called); + + async Task Execute(CancellationToken token) + { + await _timeProvider.Delay(TimeSpan.FromDays(1), token); + return EmptyStruct.Instance; + } + } + + private static async Task GetExceptionAfterDelayAsync(Exception ex, int delayInSeconds = 0) + { + if (delayInSeconds != 0) + { + await SystemClock.SleepAsync(TimeSpan.FromSeconds(delayInSeconds), CancellationToken.None); + } + + throw ex; + } + + private static AsyncHedgingPolicy GetHedgingPolicyWithExceptions( + PolicyBuilder policyBuilder, + List>>? exceptionTasks = null, + Func? generator = null) + { + bool HedgedTaskProvider(HedgingTaskProviderArguments args, out Task? result) + { + result = exceptionTasks![args.AttemptNumber - 1].Invoke(args.Context, args.CancellationToken); + return true; + } + + exceptionTasks ??= _exceptionTasks; + return new AsyncHedgingPolicy( + policyBuilder, + HedgedTaskProvider, + _exceptionTasks.Count + 1, + generator ?? HedgingTestUtilities.DefaultHedgingDelayGenerator, + HedgingTestUtilities.EmptyOnHedgingTask); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/AsyncHedgingSyntaxTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/AsyncHedgingSyntaxTests.cs new file mode 100644 index 0000000000..b946690423 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/AsyncHedgingSyntaxTests.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Hedging; +using Microsoft.Extensions.Time.Testing; +using Polly; +using Polly.Utilities; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Hedging; + +[Collection(nameof(ResiliencePollyFakeClockTestsCollection))] +public sealed class AsyncHedgingSyntaxTests : IDisposable +{ + private readonly Context _context; + private readonly CancellationTokenSource _cts; + private readonly FakeTimeProvider _timeProvider; + + public AsyncHedgingSyntaxTests() + { + _context = new Context(); + _cts = new CancellationTokenSource(); + _timeProvider = new FakeTimeProvider(); + SystemClock.SleepAsync = _timeProvider.DelayAndAdvanceAsync; + } + + public void Dispose() + { + _cts.Dispose(); + SystemClock.Reset(); + } + + [Fact] + public async Task AsyncHedgingPolicy_AllRequiredArgs_ShouldCreatePolicy() + { + var hedgingPolicy = Policy + .Handle() + .OrResult(_ => false) + .AsyncHedgingPolicy( + HedgingTestUtilities.HedgedTasksHandler.FunctionsProvider, + 1 + HedgingTestUtilities.HedgedTasksHandler.Functions.Count, + HedgingTestUtilities.DefaultHedgingDelayGenerator, + HedgingTestUtilities.EmptyOnHedgingTask); + + Assert.NotNull(hedgingPolicy); + + var result = await hedgingPolicy.ExecuteAsync( + () => + HedgingTestUtilities.PrimaryStringTasks.FastTask(_cts.Token)); + + Assert.Contains(result, + new[] + { + "Oranges", "Pears", "Apples", + HedgingTestUtilities.PrimaryStringTasks.FastTaskResult + }); + } + + [Fact] + public async Task AsyncHedgingPolicy_AllRequiredArgs_ShouldCreatePolicy_EmptyStruct() + { + var hedgingPolicy = Policy + .Handle() + .OrResult(_ => false) + .AsyncHedgingPolicy( + HedgingTestUtilities.HedgedTasksHandler.FunctionsProvider, + 1 + HedgingTestUtilities.HedgedTasksHandler.Functions.Count, + HedgingTestUtilities.DefaultHedgingDelayGenerator, + HedgingTestUtilities.EmptyOnHedgingTask); + + Assert.NotNull(hedgingPolicy); + + var result = await hedgingPolicy.ExecuteAsync( + () => + HedgingTestUtilities.PrimaryStringTasks.GenericFastTask(EmptyStruct.Instance, _cts.Token)); + + Assert.Equal(result, EmptyStruct.Instance); + } + + [InlineData(true, true, false)] + [InlineData(true, false, false)] + [InlineData(false, false, false)] + [InlineData(false, true, true)] + [Theory] + public void WrapProvider_Ok(bool nullTask, bool result, bool expectedResult) + { + var provider = AsyncHedgingSyntax.WrapProvider(Provider); + + Assert.Equal(expectedResult, provider(default, out var wrappedResult)); + if (expectedResult) + { + Assert.NotNull(wrappedResult); + } + else + { + Assert.Null(wrappedResult); + } + + bool Provider(HedgingTaskProviderArguments args, out Task? task) + { + task = nullTask ? null : Task.CompletedTask; + return result; + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/FakeTimeProviderExtensions.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/FakeTimeProviderExtensions.cs new file mode 100644 index 0000000000..7d7628bf01 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/FakeTimeProviderExtensions.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Time.Testing; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Hedging; +internal static class FakeTimeProviderExtensions +{ + public static async Task DelayAndAdvanceAsync(this FakeTimeProvider timeProvider, TimeSpan delay, CancellationToken cancellationToken) + { + var delayTask = timeProvider.Delay(delay, cancellationToken); + + timeProvider.Advance(HedgingTestUtilities.DefaultHedgingDelay); + + await delayTask; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/HedgingEngineTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/HedgingEngineTest.cs new file mode 100644 index 0000000000..bf84e070b7 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/HedgingEngineTest.cs @@ -0,0 +1,305 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Hedging; +using Microsoft.Extensions.Resilience.Polly.Test.Helpers; +using Microsoft.Extensions.Time.Testing; +using Polly; +using Polly.Utilities; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Hedging; + +[Collection(nameof(ResiliencePollyFakeClockTestsCollection))] +public sealed class HedgingEngineTest : IDisposable +{ + private readonly HedgingEngineOptions _options; + private readonly CancellationTokenSource _cts; + private readonly Context _context; + private readonly FakeTimeProvider _timeProvider; + + public HedgingEngineTest() + { + _cts = new CancellationTokenSource(); + _context = new Context(); + _options = new HedgingEngineOptions( + 1 + HedgingTestUtilities.HedgedTasksHandler.Functions.Count, + HedgingTestUtilities.DefaultHedgingDelayGenerator, + ExceptionPredicates.None, + ResultPredicates.None, + HedgingTestUtilities.EmptyOnHedgingTask); + + _timeProvider = new FakeTimeProvider(); + SystemClock.SleepAsync = _timeProvider.DelayAndAdvanceAsync; + } + + public void Dispose() + { + _cts?.Dispose(); + SystemClock.Reset(); + } + + [Fact] + public async Task ExecuteAsync_ShouldReturnAnyPossibleResult() + { + var result = await HedgingEngine.ExecuteAsync( + HedgingTestUtilities.PrimaryStringTasks.SlowTask, + _context, + HedgingTestUtilities.HedgedTasksHandler.FunctionsProvider, + _options, + false, + _cts.Token); + + Assert.NotNull(result); + Assert.Contains(result, + new[] + { + "Oranges", "Pears", "Apples", + HedgingTestUtilities.PrimaryStringTasks.SlowTaskResult + }); + } + + [Fact] + public async Task ExecuteAsync_PrimaryTaskTooSlow_EnsurePrimaryTaskResultDisposed() + { + var slowTaskCancelled = false; + var slowTaskDelay = TimeSpan.FromDays(1); + using var firstResult = new DisposableResult(); + + var options = new HedgingEngineOptions( + 2, + HedgingTestUtilities.DefaultHedgingDelayGenerator, + ExceptionPredicates.None, + ResultPredicates.None, + (_, _, _, _) => Task.CompletedTask); + + var result = await HedgingEngine.ExecuteAsync( + SlowTask, + _context, + FastTask, + options, + false, + _cts.Token); + + // the next line should be instantaneous, however let's give it some buffer to finish in rare scenarios (system overload on build machine) + Assert.True(firstResult.OnDisposed.Task.Wait(TimeSpan.FromSeconds(5)), "Timeout while waiting for the disposal of the first result."); + Assert.True(firstResult.IsDisposed); + Assert.True(slowTaskCancelled); + + async Task SlowTask(Context context, CancellationToken cancellationToken) + { + try + { + // occasionally, SystemClock.SleepAsync call won't throw and just returns instead + // so we need to handle both cases + await SystemClock.SleepAsync(slowTaskDelay, cancellationToken); + + slowTaskCancelled = cancellationToken.IsCancellationRequested; + } + catch (OperationCanceledException) + { + slowTaskCancelled = true; + } + + return firstResult; + } + + bool FastTask(HedgingTaskProviderArguments arguments, out Task result) + { + result = Task.FromResult(new DisposableResult()); + return true; + } + } + + [Fact] + public async Task ExecuteAsync_PrimaryTaskTooSlow_EnsureSecondaryResultWithoutCancelledCancellationToken() + { + // arrange + var originalMessage = new DummyRequestMessage(); + + var options = CreateOptions(); + + // act + var result = await HedgingEngine.ExecuteAsync( + (c, token) => SlowTask(originalMessage, c, token), + _context, + FastTask, + options, + false, + _cts.Token); + + _timeProvider.Advance(TimeSpan.FromDays(2)); + + // assert + Assert.NotEqual(result, originalMessage); + Assert.NotEqual(result.CancellationToken, originalMessage.CancellationToken); + Assert.False(result.CancellationToken.IsCancellationRequested); + Assert.True(originalMessage.CancellationToken.IsCancellationRequested); + + async Task SlowTask(DummyRequestMessage message, Context context, CancellationToken cancellationToken) + { + context["request"] = message; + + message.StoreCancellationToken(cancellationToken); + + await SystemClock.SleepAsync(TimeSpan.FromDays(1), cancellationToken); + + return message; + } + + bool FastTask(HedgingTaskProviderArguments arguments, out Task task) + { + var message = (arguments.Context["request"] as DummyRequestMessage)!.Clone(); + + message.StoreCancellationToken(arguments.CancellationToken); + + task = Task.FromResult(message); + return true; + } + } + + [Fact] + public async Task ExecuteAsync_EnsureCancellationTokenLinkingBroken() + { + // arrange + var originalMessage = new DummyRequestMessage(); + + var options = CreateOptions(); + + // act + var result = await HedgingEngine.ExecuteAsync( + (c, token) => SlowTask(originalMessage, c, token), + _context, + FastTask, + options, + false, + _cts.Token); + + _timeProvider.Advance(TimeSpan.FromDays(2)); + + Assert.NotEqual(_cts.Token, originalMessage.CancellationToken); + Assert.NotEqual(_cts.Token, result.CancellationToken); + + Assert.False(result.CancellationToken.IsCancellationRequested); + _cts.Cancel(); + Assert.False(result.CancellationToken.IsCancellationRequested); + + async Task SlowTask(DummyRequestMessage message, Context context, CancellationToken cancellationToken) + { + context["request"] = message; + + message.StoreCancellationToken(cancellationToken); + + await SystemClock.SleepAsync(TimeSpan.FromDays(1), cancellationToken); + + return message; + } + + bool FastTask(HedgingTaskProviderArguments arguments, out Task task) + { + var message = (arguments.Context["request"] as DummyRequestMessage)!.Clone(); + + message.StoreCancellationToken(arguments.CancellationToken); + + task = Task.FromResult(message); + return true; + } + } + + [Fact] + public async Task ExecuteAsync_EnsureBackroundWorkInSuccesfullCallNotCancelled() + { + List backroundTasks = new List(); + + var options = CreateOptions(); + + // act + var result = await HedgingEngine.ExecuteAsync( + (_, token) => SlowTask(token), + _context, + FastTask, + options, + false, + _cts.Token); + + _timeProvider.Advance(TimeSpan.FromDays(2)); + + await Assert.ThrowsAsync(() => backroundTasks[0]); + + // background task is still pending + Assert.False(backroundTasks[1].IsCompleted); + + _cts.Cancel(); + + // background task is still pending + Assert.False(backroundTasks[1].IsCompleted); + + async Task SlowTask(CancellationToken cancellationToken) + { + backroundTasks.Add(BackroundWork(cancellationToken)); + + await SystemClock.SleepAsync(TimeSpan.FromDays(1), cancellationToken); + + return true; + } + + bool FastTask(HedgingTaskProviderArguments arguments, out Task task) + { + backroundTasks.Add(BackroundWork(arguments.CancellationToken)); + + task = Task.FromResult(true); + return true; + } + + async Task BackroundWork(CancellationToken cancellationToken) => await Task.Delay(TimeSpan.FromDays(24), cancellationToken); + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public void CancellationPair_Create_Ok(bool canBeCancelled) + { + using var source = new CancellationTokenSource(); + var token = canBeCancelled ? source.Token : CancellationToken.None; + + var pair = HedgingEngine.CancellationPair.Create(token); + + if (canBeCancelled) + { + Assert.NotNull(pair.Registration); + } + else + { + Assert.Null(pair.Registration); + } + } + + private static HedgingEngineOptions CreateOptions() + { + return new HedgingEngineOptions( + 2, + HedgingTestUtilities.DefaultHedgingDelayGenerator, + ExceptionPredicates.None, + ResultPredicates.None, + (_, _, _, _) => Task.CompletedTask); + } + + private sealed class DummyRequestMessage + { + public CancellationToken CancellationToken { get; private set; } + + public void StoreCancellationToken(CancellationToken cancellationToken) + { + CancellationToken = cancellationToken; + } + + public DummyRequestMessage Clone() => new() + { + CancellationToken = CancellationToken + }; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/HedgingTestUtilities.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/HedgingTestUtilities.cs new file mode 100644 index 0000000000..783744bc42 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/HedgingTestUtilities.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Options; +using Polly; +using Polly.Utilities; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Hedging; + +#pragma warning disable CS0618 // access obsoleted members + +public static class HedgingTestUtilities +{ + public static TimeSpan DefaultHedgingDelay { get; } = TimeSpan.FromSeconds(1); + + public static Func DefaultHedgingDelayGenerator { get; } = (_) => DefaultHedgingDelay; + + public static Func, Context, int, CancellationToken, Task> EmptyOnHedgingTask { get; } = + (_, _, _, _) => Task.CompletedTask; + + public static class HedgedTasksHandler + { + public static HedgedTaskProvider FunctionsProvider { get; } = + new((HedgingTaskProviderArguments hedgingTaskProviderArguments, out Task? result) => + { + if (hedgingTaskProviderArguments.AttemptNumber <= Functions!.Count) + { + var function = Functions![hedgingTaskProviderArguments.AttemptNumber - 1]; + result = function(hedgingTaskProviderArguments.Context, hedgingTaskProviderArguments.CancellationToken)!; + return true; + } + + result = null; + return false; + }); + + public static HedgedTaskProvider FunctionsProviderNonGeneric { get; } = + new((HedgingTaskProviderArguments hedgingTaskProviderArguments, out Task? result) => + { + if (hedgingTaskProviderArguments.AttemptNumber <= Functions!.Count) + { + var function = Functions![hedgingTaskProviderArguments.AttemptNumber - 1]; + result = function(hedgingTaskProviderArguments.Context, hedgingTaskProviderArguments.CancellationToken)!; + return true; + } + + result = null; + return false; + }); + + public static HedgedTaskProvider FunctionsProviderNonGenericReturnsFalse { get; } = +#pragma warning disable S3257 // Declarations and initializations should be as concise as possible + new((HedgingTaskProviderArguments hedgingTaskProviderArguments, out Task? result) => +#pragma warning restore S3257 // Declarations and initializations should be as concise as possible + { + result = null; + return false; + }); + + public static List?>> Functions { get; } = + new() + { + GetApples, + GetOranges, + GetPears + }; + + private static async Task GetApples(Context context, CancellationToken token) + { + await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(10 * 1000), token); +#pragma warning disable S4056 // Overloads with a "CultureInfo" or an "IFormatProvider" parameter should be used + return (T)Convert.ChangeType("Apples", typeof(T)); +#pragma warning restore S4056 // Overloads with a "CultureInfo" or an "IFormatProvider" parameter should be used + } + + private static async Task GetPears(Context context, CancellationToken token) + { + await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(3 * 1000), token); +#pragma warning disable S4056 // Overloads with a "CultureInfo" or an "IFormatProvider" parameter should be used + return (T)Convert.ChangeType("Pears", typeof(T)); +#pragma warning restore S4056 // Overloads with a "CultureInfo" or an "IFormatProvider" parameter should be used + } + + private static async Task GetOranges(Context context, CancellationToken token) + { + await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(2 * 1000), token); +#pragma warning disable S4056 // Overloads with a "CultureInfo" or an "IFormatProvider" parameter should be used + return (T)Convert.ChangeType("Oranges", typeof(T)); +#pragma warning restore S4056 // Overloads with a "CultureInfo" or an "IFormatProvider" parameter should be used + } + + public static HedgedTaskProvider GetCustomTaskProvider(Func> task) + => new((HedgingTaskProviderArguments args, out Task? result) => + { + result = task(args.CancellationToken); + return true; + }); + + public static int MaxHedgedTasks { get; } = Functions.Count + 1; + } + + public static class PrimaryStringTasks + { + public const string InstantTaskResult = "Instant"; + + public const string FastTaskResult = "I am fast!"; + + public const string SlowTaskResult = "I am so slow!"; + + public static Task InstantTask() + { + return Task.FromResult(InstantTaskResult); + } + + public static async Task FastTask(CancellationToken token) + { + await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(10), token); + return FastTaskResult; + } + + public static async Task SlowTask(Context _, CancellationToken token) + { + await SystemClock.SleepAsync(TimeSpan.FromDays(1), token); + return SlowTaskResult; + } + + public static async Task GenericFastTask(T result, CancellationToken token) + { + await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(10), token); + return result; + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/TaskHelper.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/TaskHelper.cs new file mode 100644 index 0000000000..f67b6db6c7 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Hedging/TaskHelper.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Time.Testing; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Hedging; + +public static class TaskHelper +{ + public static async Task AdvanceTimeUntilFinished(this Task task, FakeTimeProvider timeProvider, TimeSpan? delta = null, TimeSpan? maxAdvance = null) + { + var advanceTask = CreateAdvanceTask(task, timeProvider, delta, maxAdvance); + +#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks + await task; +#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks + await advanceTask; + } + + public static async Task AdvanceTimeUntilFinished(this Task task, FakeTimeProvider timeProvider, TimeSpan? delta = null, TimeSpan? maxAdvance = null) + { + delta ??= TimeSpan.FromDays(1); + var advanceTask = CreateAdvanceTask(task, timeProvider, delta, maxAdvance); + +#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks + var result = await task; +#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks + await advanceTask; + + return result; + } + + private static Task CreateAdvanceTask(Task task, FakeTimeProvider timeProvider, TimeSpan? delta, TimeSpan? maxAdvance) + { + var totalAdvanced = 0d; + delta ??= TimeSpan.FromDays(1); + + return Task.Run(async () => + { + while (!task.IsCompleted) + { + timeProvider.Advance(delta.Value); + totalAdvanced += delta.Value.TotalMilliseconds; + + if (maxAdvance != null && totalAdvanced > maxAdvance.Value.TotalMilliseconds) + { + break; + } + + await Task.Delay(1); + } + }); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/HedgingTaskProviderArgumentsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/HedgingTaskProviderArgumentsTests.cs new file mode 100644 index 0000000000..4d1c235a92 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/HedgingTaskProviderArgumentsTests.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test; + +public class HedgingTaskProviderArgumentsTests +{ + [Fact] + public void Constructor_NoParameters_ShouldInitialize() + { +#pragma warning disable SA1129 // Do not use default value type constructor + var instance = new HedgingTaskProviderArguments(); +#pragma warning restore SA1129 // Do not use default value type constructor + + Assert.Null(instance.Context); + Assert.Equal(0, instance.AttemptNumber); + Assert.Equal(CancellationToken.None, instance.CancellationToken); + } + + [Fact] + public void Constructor_WithParameters_ShouldInitializeProperties() + { + var context = new Context(); +#pragma warning disable SA1129 // Do not use default value type constructor + var cancellationToken = new CancellationToken(); +#pragma warning restore SA1129 // Do not use default value type constructor + var instance = new HedgingTaskProviderArguments( + context, + 2, + CancellationToken.None); + + Assert.Equal(context, instance.Context); + Assert.Equal(2, instance.AttemptNumber); + Assert.Equal(cancellationToken, instance.CancellationToken); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Helpers/AssertionFailure.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Helpers/AssertionFailure.cs new file mode 100644 index 0000000000..4f7ef68ac3 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Helpers/AssertionFailure.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Helpers; + +internal sealed class AssertionFailure +{ + public AssertionFailure(int expected, int actual, string measure) + { + if (string.IsNullOrWhiteSpace(measure)) + { + throw new ArgumentNullException(nameof(measure)); + } + + Expected = expected; + Actual = actual; + Measure = measure; + } + + public int Expected { get; } + + public int Actual { get; } + + public string Measure { get; } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Helpers/CustomObject.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Helpers/CustomObject.cs new file mode 100644 index 0000000000..8f43c5af7d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Helpers/CustomObject.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Helpers; + +public sealed class CustomObject : IDisposable +{ + public string? Content { get; private set; } + + public CustomObject(string content) + { + Content = content; + } + + public void Dispose() + { + Content = null; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Helpers/DisposableResult.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Helpers/DisposableResult.cs new file mode 100644 index 0000000000..8cdd45a181 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Helpers/DisposableResult.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Helpers; + +internal sealed class DisposableResult : IDisposable +{ + public readonly TaskCompletionSource OnDisposed = new(); + + public bool IsDisposed { get; private set; } + + public void Dispose() + { + IsDisposed = true; + + OnDisposed.TrySetResult(true); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Helpers/FailureResultContextHelper.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Helpers/FailureResultContextHelper.cs new file mode 100644 index 0000000000..cb4e0ec872 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Helpers/FailureResultContextHelper.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.Internal; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Helpers; + +public static class FailureResultContextHelper +{ + public static Func GetFailureResultContextProvider(IServiceProvider serviceProvider) + { + var options = serviceProvider.GetRequiredService>>(); + + return value => options.Value.GetContextFromResult(value); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/ContextExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/ContextExtensionsTests.cs new file mode 100644 index 0000000000..264380e25d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/ContextExtensionsTests.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Resilience.Internal; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Internals; +public class ContextExtensionsTests +{ + [Fact] + public void GetPolicyPipelineName_WhenNullPolicyKeyAndNullPolicyWrapKey_ShouldReturnEmpty() + { + var context = new Context(); + var name = context.GetPolicyPipelineName(); + Assert.Equal(string.Empty, name); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/FailureReasonResolverTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/FailureReasonResolverTest.cs new file mode 100644 index 0000000000..2fcd4913b9 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/FailureReasonResolverTest.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Net.Http; +using Microsoft.Extensions.Resilience.Internal; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test; + +public class FailureReasonResolverTest +{ + [Fact] + public void GetFailureReason_WhenExceptionIsThrown_ShouldReturnException() + { + const string ExpectedMessage = "Cannot access year 2020. This part of the memory is under quarantine."; + var result = new DelegateResult(new InvalidOperationException(ExpectedMessage)); + var failureReason = FailureReasonResolver.GetFailureReason(result); + Assert.Equal($"Error: {ExpectedMessage}", failureReason); + } + + [Fact] + public void GetFailureReason_GetFailureMessageFromException() + { + const string ExpectedMessage = "Cannot access year 2020. This part of the memory is under quarantine."; + var exception = new InvalidOperationException(ExpectedMessage); + var failureReason = FailureReasonResolver.GetFailureFromException(exception); + Assert.Equal($"Error: {ExpectedMessage}", failureReason); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("ʕ•́ᴥ•̀ʔ")] + [InlineData("We wish you a Merry Christmas")] + public void GetFailureReason_NullResult_ShouldReturnUndefined(string resultContent) + { + var result = new DelegateResult(resultContent); + var failureReason = FailureReasonResolver.GetFailureReason(result); + Assert.Equal("Undefined", failureReason); + } + + [Theory] + [InlineData(HttpStatusCode.BadRequest, "Status code: BadRequest")] + [InlineData(HttpStatusCode.InternalServerError, "Status code: InternalServerError")] + public void GetFailureReason_WhenErrorStatus_ShouldReturnException(HttpStatusCode code, string expectedFailure) + { + using var responseMessage = new HttpResponseMessage { StatusCode = code }; + var httpResponseDelegate = new DelegateResult(responseMessage); + + var failureReason = FailureReasonResolver.GetFailureReason(httpResponseDelegate); + Assert.Equal(expectedFailure, failureReason); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/PipelineIdTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/PipelineIdTests.cs new file mode 100644 index 0000000000..b2870137df --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/PipelineIdTests.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Resilience.Internal; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Internals; + +public class PipelineIdTests +{ + [InlineData("pipeline", "key", "String-pipeline-key")] + [InlineData("pipeline", null, "String-pipeline")] + [InlineData("pipeline", "", "String-pipeline")] + [Theory] + public void PolicyPipelineKey_Typed_Ok(string pipeline, string key, string expectedResult) + { + Assert.Equal(expectedResult, PipelineId.Create(pipeline, key).PolicyPipelineKey); + } + + [InlineData("pipeline", "key", "pipeline-key")] + [InlineData("pipeline", null, "pipeline")] + [InlineData("pipeline", "", "pipeline")] + [Theory] + public void PolicyPipelineKey_NonTyped_Ok(string pipeline, string key, string expectedResult) + { + Assert.Equal(expectedResult, PipelineId.Create(pipeline, key).PolicyPipelineKey); + } + + [Fact] + public void Create_Ok() + { + Assert.Throws(() => PipelineId.Create(null!, "key")); + Assert.Throws(() => PipelineId.Create(string.Empty, "key")); + Assert.Throws(() => PipelineId.Create(null!, "key")); + Assert.Throws(() => PipelineId.Create(string.Empty, "key")); + + var id = PipelineId.Create("dummy", "key"); + + Assert.Equal("dummy", id.PipelineName); + Assert.Equal("key", id.PipelineKey); + + id = PipelineId.Create("dummy", "key"); + + Assert.Equal("dummy", id.PipelineName); + Assert.Equal("key", id.PipelineKey); + Assert.Equal("String", id.ResultType); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/PolicyFactoryTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/PolicyFactoryTests.cs new file mode 100644 index 0000000000..a23042c64e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/PolicyFactoryTests.cs @@ -0,0 +1,1487 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Resilience.Hedging; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Extensions.Resilience.Polly.Test.Hedging; +using Microsoft.Extensions.Resilience.Polly.Test.Helpers; +using Microsoft.Extensions.Resilience.Polly.Test.Options; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Microsoft.Extensions.Time.Testing; +using Moq; +using Polly; +using Polly.Bulkhead; +using Polly.CircuitBreaker; +using Polly.Timeout; +using Polly.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Extensions.Resilience.Polly.Test; + +#pragma warning disable CS0618 // Type or member is obsolete +[Collection(nameof(ResiliencePollyFakeClockTestsCollection))] +public sealed class PolicyFactoryTests : IDisposable +{ + private const string DefaultStringResponse = "We wish you a merry Xmas!"; + private const string DefaultFallbackReturnResponse = "42"; + private const string DummyPolicyName = "Name"; + private static readonly Func> _defaultFallbackAction = _ => Task.FromResult(DefaultFallbackReturnResponse); + private static readonly HedgedTaskProvider _defaultHedgedTaskProvider = (HedgingTaskProviderArguments _, out Task? result) => + { + result = Task.FromResult("test"); + return true; + }; + + private static readonly HedgedTaskProvider _defaultHedgedTaskProviderNonGeneric = (HedgingTaskProviderArguments _, out Task? result) => + { + result = Task.FromResult("test"); + return true; + }; + + private readonly IPolicyFactory _policyFactory; + private readonly FakeLogger _loggerMock; + private readonly Mock _policyMeter; + + private readonly RetryPolicyOptions _retryPolicyOptions = new() { BaseDelay = TimeSpan.FromMilliseconds(10) }; + private readonly RetryPolicyOptions _retryPolicyOptionsNonGeneric = new() { BaseDelay = TimeSpan.FromMilliseconds(10) }; + private readonly CircuitBreakerPolicyOptions _defaultCircuitBreakerPolicyOptions = new(); + private readonly CircuitBreakerPolicyOptions _defaultCircuitBreakerPolicyOptionsNonGeneric = new(); + private readonly FallbackPolicyOptions _defaultFallbackPolicyOptions = new(); + private readonly FallbackPolicyOptions _defaultFallbackPolicyOptionsNonGeneric = new(); + private readonly HedgingPolicyOptions _defaulHedgingPolicyOptions = new(); + private readonly HedgingPolicyOptions _defaulHedgingPolicyOptionsNonGeneric = new(); + private readonly TimeSpan _cohesionTimeLimit = TimeSpan.FromMilliseconds(1000); // Consider increasing CohesionTimeLimit if bulkhead specs fail transiently in slower build environments. + private readonly AutoResetEvent _statusChangedEvent = new(false); + private readonly TimeSpan _shimTimeSpan = TimeSpan.FromMilliseconds(50); // How frequently to retry the assertions. + + private readonly ITestOutputHelper _output; + private readonly FakeTimeProvider _timeProvider; + + public PolicyFactoryTests(ITestOutputHelper output) + { + _loggerMock = new FakeLogger(); + _policyMeter = new Mock(MockBehavior.Strict); + + var services = new ServiceCollection(); + _policyFactory = new PolicyFactory(_loggerMock, _policyMeter.Object); + _output = output; + _timeProvider = new FakeTimeProvider(); + SystemClock.SleepAsync = _timeProvider.DelayAndAdvanceAsync; + SystemClock.UtcNow = () => _timeProvider.GetUtcNow().UtcDateTime; + } + + public void Dispose() + { + _policyMeter.VerifyAll(); + _statusChangedEvent.Dispose(); + SystemClock.Reset(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CreateCircuitBreakerPolicy_WhenExecutedWithException_ShouldBreakThenReset(bool shouldHandleException) + { + var options = new CircuitBreakerPolicyOptions + { + FailureThreshold = 0.1, + MinimumThroughput = 2, + BreakDuration = TimeSpan.FromMilliseconds(501), + ShouldHandleException = _ => shouldHandleException + }; + + var policy = (AsyncCircuitBreakerPolicy)_policyFactory.CreateCircuitBreakerPolicy("DefaultCircuitBreakerPolicy", options); + Assert.NotNull(policy); + + if (shouldHandleException) + { + SetupMetering("DefaultCircuitBreakerPolicy", PolicyEvents.CircuitBreakerOnBreakPolicyEvent); + SetupMetering("DefaultCircuitBreakerPolicy", PolicyEvents.CircuitBreakerOnResetPolicyEvent); + } + + for (int i = 0; i < 10; i++) + { + try + { + await policy.ExecuteAsync(TaskWithException); + } + catch (InvalidOperationException) + { + // Nothing + } + catch (BrokenCircuitException) + { + // Ensure that OnBreak is triggered + break; + } + } + + // Make sure OnReset is triggered + policy.Reset(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CreateCircuitBreakerPolicy_NonGeneric_WhenExecutedWithException_ShouldBreakThenReset(bool shouldHandleException) + { + var options = new CircuitBreakerPolicyOptions + { + FailureThreshold = 0.1, + MinimumThroughput = 2, + BreakDuration = TimeSpan.FromMilliseconds(501), + ShouldHandleException = _ => shouldHandleException + }; + + var policy = (AsyncCircuitBreakerPolicy)_policyFactory.CreateCircuitBreakerPolicy("DefaultCircuitBreakerPolicy", options); + Assert.NotNull(policy); + + if (shouldHandleException) + { + SetupMetering("DefaultCircuitBreakerPolicy", PolicyEvents.CircuitBreakerOnResetPolicyEvent); + SetupMetering("DefaultCircuitBreakerPolicy", PolicyEvents.CircuitBreakerOnBreakPolicyEvent); + } + + for (int i = 0; i < 10; i++) + { + try + { + await policy.ExecuteAsync(TaskWithExceptionNonGeneric); + } + catch (InvalidOperationException) + { + // Nothing + } + catch (BrokenCircuitException) + { + // Ensure that OnBreak is triggered + break; + } + } + + // Make sure OnReset is triggered + policy.Reset(); + } + + [Fact] + public async Task CreateCircuitBreakerPolicy_EnsureExplicitPolicyNameRespected() + { + var policyName = "some-name"; + var options = new CircuitBreakerPolicyOptions + { + FailureThreshold = 0.1, + MinimumThroughput = 2, + BreakDuration = TimeSpan.FromMilliseconds(501), + ShouldHandleException = _ => true + }; + + var policy = (AsyncCircuitBreakerPolicy)_policyFactory.CreateCircuitBreakerPolicy(policyName, options); + + SetupMetering(policyName, PolicyEvents.CircuitBreakerOnBreakPolicyEvent); + SetupMetering(policyName, PolicyEvents.CircuitBreakerOnResetPolicyEvent); + + for (int i = 0; i < 10; i++) + { + try + { + await policy.ExecuteAsync(TaskWithException); + } + catch (InvalidOperationException) + { + // Nothing + } + catch (BrokenCircuitException) + { + // Ensure that OnBreak is triggered + break; + } + } + + // Make sure OnReset is triggered + policy.Reset(); + _policyMeter.VerifyAll(); + } + + [Fact] + public async Task CreateCircuitBreakerPolicy_WhenResetEvent_UsesTheCorrectOne() + { + var policyName = "some-name"; + var options = new CircuitBreakerPolicyOptions + { + FailureThreshold = 0.1, + MinimumThroughput = 2, + BreakDuration = TimeSpan.FromMilliseconds(501), + ShouldHandleException = _ => true + }; + + SetupMetering(policyName, PolicyEvents.CircuitBreakerOnBreakPolicyEvent); + SetupMetering(policyName, PolicyEvents.CircuitBreakerOnResetPolicyEvent); + + var policy = (AsyncCircuitBreakerPolicy)_policyFactory.CreateCircuitBreakerPolicy(policyName, options); + Assert.NotNull(policy); + + for (int i = 0; i < 10; i++) + { + try + { + await policy.ExecuteAsync(TaskWithException); + } + catch (InvalidOperationException) + { + // Nothing + } + catch (BrokenCircuitException) + { + // Ensure that OnBreak is triggered + break; + } + } + + policy.Reset(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CreateCircuitBreakerPolicy_EnsureOnHalfOpenReported(bool generic) + { + var policyName = "some-name"; + var options = new CircuitBreakerPolicyOptions + { + FailureThreshold = 0.9, + MinimumThroughput = 2, + BreakDuration = TimeSpan.FromMilliseconds(501), + ShouldHandleException = _ => true + }; + + if (generic) + { + SetupMetering(policyName, PolicyEvents.CircuitBreakerOnBreakPolicyEvent); + SetupMetering(policyName, PolicyEvents.CircuitBreakerOnHalfOpenPolicyEvent); + SetupMetering(policyName, PolicyEvents.CircuitBreakerOnResetPolicyEvent); + } + else + { + SetupMetering(policyName, PolicyEvents.CircuitBreakerOnBreakPolicyEvent); + SetupMetering(policyName, PolicyEvents.CircuitBreakerOnHalfOpenPolicyEvent); + SetupMetering(policyName, PolicyEvents.CircuitBreakerOnResetPolicyEvent); + } + + var policy = generic ? + _policyFactory.CreateCircuitBreakerPolicy(policyName, options) : + _policyFactory.CreateCircuitBreakerPolicy(policyName, (CircuitBreakerPolicyOptions)options).AsAsyncPolicy(); + + for (int i = 0; i < 10; i++) + { + try + { + await policy.ExecuteAsync(TaskWithException); + } + catch (InvalidOperationException) + { + // Nothing + } + catch (BrokenCircuitException) + { + // Ensure that OnBreak is triggered + break; + } + } + + _timeProvider.Advance(TimeSpan.FromSeconds(1)); + + await policy.ExecuteAsync(() => Task.FromResult("dummy")); + + var record = _loggerMock.Collector.GetSnapshot().Single(v => v.Id == 7); + record.Message.Should().Be("Circuit breaker policy: some-name. Half-Open has been triggered."); + } + + [Fact] + public async Task CreateCircuitBreakerPolicy_NonGeneric_WhenResetEvent_UsesTheCorrectOne() + { + var options = new CircuitBreakerPolicyOptions + { + FailureThreshold = 0.1, + MinimumThroughput = 2, + BreakDuration = TimeSpan.FromMilliseconds(501), + ShouldHandleException = _ => true + }; + + SetupMetering("DefaultCircuitBreakerPolicy", PolicyEvents.CircuitBreakerOnBreakPolicyEvent); + SetupMetering("DefaultCircuitBreakerPolicy", PolicyEvents.CircuitBreakerOnResetPolicyEvent); + + var policy = (AsyncCircuitBreakerPolicy)_policyFactory.CreateCircuitBreakerPolicy("DefaultCircuitBreakerPolicy", options); + Assert.NotNull(policy); + + for (int i = 0; i < 10; i++) + { + try + { + await policy.ExecuteAsync(TaskWithException); + } + catch (InvalidOperationException) + { + // Nothing + } + catch (BrokenCircuitException) + { + // Ensure that OnBreak is triggered + break; + } + } + + policy.Reset(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CreateCircuitBreakerPolicy_WhenExecutedWithErrorResponse_ShouldHandleResponse(bool shouldHandleResponse) + { + _defaultCircuitBreakerPolicyOptions.ShouldHandleResultAsError = _ => shouldHandleResponse; + _defaultCircuitBreakerPolicyOptions.ShouldHandleException = _ => false; + var policy = _policyFactory.CreateCircuitBreakerPolicy(DummyPolicyName, _defaultCircuitBreakerPolicyOptions); + Assert.NotNull(policy); + + var response = await policy.ExecuteAsync(TaskWithResponse); + Assert.Equal(DefaultStringResponse, response); + } + + [Fact] + public async Task CreateCircuitBreakerPolicy_NonGeneric_WhenExecutedWithErrorResponse_ShouldHandleResponse() + { + _defaultCircuitBreakerPolicyOptionsNonGeneric.ShouldHandleException = _ => false; + var policy = _policyFactory.CreateCircuitBreakerPolicy("policy-name", _defaultCircuitBreakerPolicyOptionsNonGeneric); + Assert.NotNull(policy); + + await policy.ExecuteAsync(TaskWithResponse); + } + + [Fact] + public void CreateCircuitBreakerPolicy_NullConfiguration_ShouldThrow() + { + Assert.Throws(() => _policyFactory.CreateCircuitBreakerPolicy(null!, null!)); + + Assert.Throws(() => _policyFactory.CreateCircuitBreakerPolicy(null!, _defaultCircuitBreakerPolicyOptions)); + + Assert.Throws(() => _policyFactory.CreateCircuitBreakerPolicy("name", null!)); + } + + [Fact] + public void CreateCircuitBreakerPolicy_NullConfiguration_ShouldThrow_NonGeneric() + { + Assert.Throws(() => + _policyFactory.CreateCircuitBreakerPolicy("policy-name", null!)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CreateRetryPolicy_WhenExecutedWithException_ShouldHandleException(bool shouldHandleException) + { + _retryPolicyOptions.ShouldHandleException = _ => shouldHandleException; + + var policy = _policyFactory.CreateRetryPolicy("DefaultRetryPolicy", _retryPolicyOptions); + Assert.NotNull(policy); + + if (shouldHandleException) + { + SetupMetering("DefaultRetryPolicy", PolicyEvents.RetryPolicyEvent); + } + + await Assert.ThrowsAsync(async () => + await policy.ExecuteAsync(TaskWithException)); + + if (shouldHandleException) + { + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogRetry", _loggerMock.LatestRecord.Id.Name); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CreateRetryPolicy_NonGeneric_WhenExecutedWithException_ShouldHandleException(bool shouldHandleException) + { + _retryPolicyOptionsNonGeneric.ShouldHandleException = _ => shouldHandleException; + + var policy = _policyFactory.CreateRetryPolicy(DummyPolicyName, _retryPolicyOptionsNonGeneric); + Assert.NotNull(policy); + + if (shouldHandleException) + { + SetupMetering(DummyPolicyName, PolicyEvents.RetryPolicyEvent); + } + + await Assert.ThrowsAsync(async () => await policy.ExecuteAsync(TaskWithException)); + + if (shouldHandleException) + { + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogRetry", _loggerMock.LatestRecord.Id.Name); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CreateRetryPolicy_WhenExecutedWithErrorResponse_ShouldHandleResponse(bool shouldHandleResponse) + { + _retryPolicyOptions.ShouldHandleResultAsError = _ => shouldHandleResponse; + _retryPolicyOptions.ShouldHandleException = _ => false; + + var policy = _policyFactory.CreateRetryPolicy(DummyPolicyName, _retryPolicyOptions); + Assert.NotNull(policy); + + if (shouldHandleResponse) + { + SetupMetering(DummyPolicyName, PolicyEvents.RetryPolicyEvent); + } + + var response = await policy.ExecuteAsync(TaskWithResponse); + Assert.Equal(DefaultStringResponse, response); + + if (shouldHandleResponse) + { + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogRetry", _loggerMock.LatestRecord.Id.Name); + } + } + + [Fact] + public async Task CreateFallbackPolicy_NonGeneric_WhenExecutedWithNotHandledErrorResponse_ShouldNotHandleResponse() + { + _defaultFallbackPolicyOptionsNonGeneric.ShouldHandleException = _ => false; + + var policy = _policyFactory.CreateFallbackPolicy(DummyPolicyName, args => _defaultFallbackAction(args), _defaultFallbackPolicyOptionsNonGeneric); + Assert.NotNull(policy); + + await policy.ExecuteAsync(TaskWithResponseNonGeneric); + } + + [Fact] + public async Task CreateRetryPolicy_EnsureExplicitPolicyNameRespected() + { + var policyName = "some-name"; + var options = new RetryPolicyOptions + { + ShouldHandleException = _ => true, + RetryCount = 1, + ShouldHandleResultAsError = r => r == "error", + BaseDelay = TimeSpan.Zero + }; + + var policy = _policyFactory.CreateRetryPolicy(policyName, options); + + SetupMetering(policyName, PolicyEvents.RetryPolicyEvent); + + await policy.ExecuteAsync(() => Task.FromResult("error")); + _policyMeter.VerifyAll(); + } + + [Fact] + public async Task CreateRetryPolicy_NonGeneric_EnsureExplicitPolicyNameRespected() + { + var policyName = "some-name"; + var options = new RetryPolicyOptions + { + ShouldHandleException = _ => true, + RetryCount = 1, + BaseDelay = TimeSpan.Zero + }; + + var policy = _policyFactory.CreateRetryPolicy(policyName, options); + + SetupMetering(policyName, PolicyEvents.RetryPolicyEvent); + + await Assert.ThrowsAsync(async () => await policy.ExecuteAsync(TaskWithException)); + + _policyMeter.VerifyAll(); + } + + [Theory] + [InlineData(BackoffType.ExponentialWithJitter)] + [InlineData(BackoffType.Constant)] + [InlineData(BackoffType.Linear)] + public async Task CreateRetryPolicy_WhenExecutedWithDefaultDelay(BackoffType backoffType) + { + _retryPolicyOptions.BaseDelay = TimeSpan.FromMilliseconds(10); + _retryPolicyOptions.BackoffType = backoffType; + + SetupMetering(DummyPolicyName, PolicyEvents.RetryPolicyEvent); + + var policy = _policyFactory.CreateRetryPolicy(DummyPolicyName, _retryPolicyOptions); + Assert.NotNull(policy); + + await Assert.ThrowsAsync(async () => + await policy.ExecuteAsync(TaskWithException)); + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogRetry", _loggerMock.LatestRecord.Id.Name); + } + + [Fact] + public async Task CreateRetryPolicy_NonGeneric_WhenExecutedWithDefaultDelay() + { + _retryPolicyOptions.BaseDelay = TimeSpan.FromMilliseconds(10); + + SetupMetering(DummyPolicyName, PolicyEvents.RetryPolicyEvent); + + var policy = _policyFactory.CreateRetryPolicy(DummyPolicyName, _retryPolicyOptionsNonGeneric); + Assert.NotNull(policy); + + await Assert.ThrowsAsync(async () => + await policy.ExecuteAsync(TaskWithException)); + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogRetry", _loggerMock.LatestRecord.Id.Name); + } + + [Fact] + public async Task CreateRetryPolicy_WhenExecutedWithDefaultDelayAndInfiniteRetry() + { + CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); + int retryCount = 0; + + _retryPolicyOptions.BaseDelay = TimeSpan.FromTicks(1); + _retryPolicyOptions.RetryCount = RetryPolicyOptions.InfiniteRetry; + _retryPolicyOptions.BackoffType = BackoffType.Constant; + _retryPolicyOptions.OnRetryAsync = (_) => + { + if (++retryCount > 100) + { + cts.Cancel(); + } + + return Task.CompletedTask; + }; + + SetupMetering("DefaultRetryPolicy", PolicyEvents.RetryPolicyEvent); + + var policy = _policyFactory.CreateRetryPolicy("DefaultRetryPolicy", _retryPolicyOptions); + Assert.NotNull(policy); + + // .NET Framework throws either Operation or TaskCanceled exception depending on circumstances + await Assert.ThrowsAnyAsync(async () => + await policy.ExecuteAsync((_) => TaskWithException(), cts.Token)); + + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogRetry", _loggerMock.LatestRecord.Id.Name); + + cts.Dispose(); + } + + [Fact] + public void CreateRetryPolicy_WhenExecutedWithExponentialBackoffAndHighRetryCount_ThrowsValidationException() + { + _retryPolicyOptions.BaseDelay = TimeSpan.FromTicks(1); + _retryPolicyOptions.RetryCount = 50; + _retryPolicyOptions.BackoffType = BackoffType.ExponentialWithJitter; + + Assert.Throws(() => _policyFactory.CreateRetryPolicy(DummyPolicyName, _retryPolicyOptions)); + } + + [Theory] + [InlineData(BackoffType.ExponentialWithJitter)] + [InlineData(BackoffType.Linear)] + public void CreateRetryPolicy_WhenCreatedWithInfiniteRetries_ThrowsOnLinearAndExponentialBackoff(BackoffType backoffType) + { + _retryPolicyOptions.RetryCount = RetryPolicyOptions.InfiniteRetry; + _retryPolicyOptions.BackoffType = backoffType; + + Assert.Throws(() => _policyFactory.CreateRetryPolicy(DummyPolicyName, _retryPolicyOptions)); + } + + [Fact] + public async Task CreateRetryPolicy_WhenExecutedWithCustomDelay() + { + _retryPolicyOptions.RetryDelayGenerator = (_) => TimeSpan.FromMilliseconds(5); + + SetupMetering("DefaultRetryPolicy", PolicyEvents.RetryPolicyEvent); + + var policy = _policyFactory.CreateRetryPolicy("DefaultRetryPolicy", _retryPolicyOptions); + Assert.NotNull(policy); + + await Assert.ThrowsAsync(async () => await policy.ExecuteAsync(TaskWithException)); + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogRetry", _loggerMock.LatestRecord.Id.Name); + } + + [Fact] + public async Task CreateRetryPolicy_RetryDelayGeneratorReturnsZeroDelay_EnsureBaseDelayUsed() + { + bool asserted = false; + + _retryPolicyOptions.RetryDelayGenerator = (_) => TimeSpan.Zero; + _retryPolicyOptions.ShouldHandleResultAsError = r => true; + _retryPolicyOptions.BaseDelay = TimeSpan.FromMinutes(12); + _retryPolicyOptions.RetryCount = 1; + _retryPolicyOptions.OnRetryAsync = args => + { + Assert.Equal(TimeSpan.FromMinutes(12), args.WaitingTimeInterval); + asserted = true; + return Task.CompletedTask; + }; + + SetupMetering("DefaultRetryPolicy", PolicyEvents.RetryPolicyEvent); + + var policy = _policyFactory.CreateRetryPolicy("DefaultRetryPolicy", _retryPolicyOptions); + Assert.NotNull(policy); + + await policy.ExecuteAsync(() => Task.FromResult(string.Empty)).AdvanceTimeUntilFinished(_timeProvider); + + Assert.True(asserted); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CreateRetryPolicy_WithFirstDelaysOutOfRange_ThrowsValidationException(bool defaultRetryDelayGenerator) + { + _retryPolicyOptions.RetryDelayGenerator = defaultRetryDelayGenerator ? null : (_ => TimeSpan.FromSeconds(1)); + _retryPolicyOptions.BackoffType = BackoffType.ExponentialWithJitter; + _retryPolicyOptions.BaseDelay = TimeSpan.FromDays(10_000_000); + + Assert.Throws(() => _policyFactory.CreateRetryPolicy(DummyPolicyName, _retryPolicyOptions)); + } + + [Fact] + public async Task CreateRetryPolicy_WhenExecutedWithCustomDelayAndInfiniteRetry() + { + CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + int retryCount = 0; + + _retryPolicyOptions.RetryDelayGenerator = (_) => TimeSpan.FromMilliseconds(5); + _retryPolicyOptions.BackoffType = BackoffType.Constant; + _retryPolicyOptions.RetryCount = RetryPolicyOptions.InfiniteRetry; + _retryPolicyOptions.OnRetryAsync = (_) => + { + if (++retryCount > 100) + { + cts.Cancel(); + } + + return Task.CompletedTask; + }; + + SetupMetering("DefaultRetryPolicy", PolicyEvents.RetryPolicyEvent); + + var policy = _policyFactory.CreateRetryPolicy("DefaultRetryPolicy", _retryPolicyOptions); + Assert.NotNull(policy); + + await Assert.ThrowsAnyAsync(() => policy.ExecuteAsync(_ => TaskWithException(), cts.Token)); + + Assert.Equal(101, retryCount); + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogRetry", _loggerMock.LatestRecord.Id.Name); + + cts.Dispose(); + } + + [Theory] + [InlineData(BackoffType.ExponentialWithJitter)] + [InlineData(BackoffType.Constant)] + [InlineData(BackoffType.Linear)] + public async Task CreateRetryPolicy_WhenExecutedWithInvalidCustomDelay(BackoffType backoffType) + { + _retryPolicyOptions.RetryDelayGenerator = (_) => TimeSpan.FromMilliseconds(-5); + _retryPolicyOptions.BackoffType = backoffType; + + SetupMetering("DefaultRetryPolicy", PolicyEvents.RetryPolicyEvent); + + var policy = _policyFactory.CreateRetryPolicy("DefaultRetryPolicy", _retryPolicyOptions); + Assert.NotNull(policy); + + // Assert it does not throw ArgumentOutOfRangeException from invalid delay + await Assert.ThrowsAsync(async () => await policy.ExecuteAsync(TaskWithException)); + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogRetry", _loggerMock.LatestRecord.Id.Name); + } + + [Fact] + public void CreateRetryPolicy_NullConfiguration_ShouldThrow() + { + Assert.Throws(() => _policyFactory.CreateRetryPolicy(null!, null!)); + + Assert.Throws(() => _policyFactory.CreateRetryPolicy(null!, _retryPolicyOptions)); + + Assert.Throws(() => _policyFactory.CreateRetryPolicy("policy-name", null!)); + } + + [Fact] + public void CreateRetryPolicy_NonGeneric_NullConfiguration_ShouldThrow() + { + Assert.Throws(() => _policyFactory.CreateRetryPolicy(null!, _retryPolicyOptionsNonGeneric)); + + Assert.Throws(() => _policyFactory.CreateRetryPolicy("policy-name", null!)); + } + + [Fact] + public async Task CreateRetryPolicy_NullDelayGenerators_ShouldNotThrow() + { + var policy = _policyFactory.CreateRetryPolicy(DummyPolicyName, _retryPolicyOptions); + var response = await policy.ExecuteAsync(TaskWithResponse); + Assert.Equal(DefaultStringResponse, response); + } + + [Fact] + public async Task CreateRetryPolicy_NonGeneric_NullDelayGenerator_ShouldNotThrow() + { + var policy = _policyFactory.CreateRetryPolicy(DummyPolicyName, _retryPolicyOptionsNonGeneric); + var response = await policy.ExecuteAsync(TaskWithResponse); + Assert.Equal(DefaultStringResponse, response); + } + + [Fact] + public void CreateTimeoutPolicy_WhenProperlyConfigured_ShouldInitialize() + { + var timeoutOptions = new TimeoutPolicyOptions + { + TimeoutInterval = TimeSpan.FromSeconds(1) + }; + + var policy = _policyFactory.CreateTimeoutPolicy(DummyPolicyName, timeoutOptions); + Assert.NotNull(policy); + } + + [Fact] + public async Task CreateTimeoutPolicy_EnsureExplicitPolicyNameRespected() + { + var policyName = "some-name"; + var options = new TimeoutPolicyOptions + { + TimeoutInterval = TimeSpan.FromMilliseconds(1) + }; + + var policy = _policyFactory.CreateTimeoutPolicy(policyName, options); + + SetupMetering(policyName, PolicyEvents.TimeoutPolicyEvent); + + await Assert.ThrowsAsync(() => + policy.ExecuteAsync( + async (t) => + { + await _timeProvider.DelayAndAdvanceAsync(TimeSpan.FromMinutes(1), t); + return "result"; + }, + CancellationToken.None)); + } + + [Fact] + public void CreateTimeoutPolicy_WhenWronglyConfigured_ShouldThrow() + { + var timeoutOptions = new TimeoutPolicyOptions + { + TimeoutInterval = TimeSpan.FromSeconds(-1) + }; + + Assert.Throws( + () => _policyFactory.CreateTimeoutPolicy(DummyPolicyName, timeoutOptions)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CreateTimeoutPolicy_WhenExecutedWithTimeout_ShouldTrackEvent(bool useOldEvent) + { + SetupMetering("DefaultTimeoutPolicy", PolicyEvents.TimeoutPolicyEvent); + + var timeoutOptions = new TimeoutPolicyOptions + { + TimeoutInterval = TimeSpan.FromSeconds(1), + TimeoutStrategy = TimeoutStrategy.Pessimistic + }; + + if (!useOldEvent) + { + timeoutOptions.OnTimedOutAsync = _ => Task.FromResult(true); + } + + var policy = _policyFactory.CreateTimeoutPolicy("DefaultTimeoutPolicy", timeoutOptions); + await Assert.ThrowsAsync( + () => policy.ExecuteAsync( + async () => + { + // Timeout policy based on cancellation tokens + await Task.Delay(TimeSpan.FromSeconds(10)); + return "lala"; + })); + + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogTimeout", _loggerMock.LatestRecord.Id.Name); + } + + [Fact] + public void CreateTimeoutPolicy_NullConfiguration_ShouldThrow() + { + Assert.Throws(() => _policyFactory.CreateTimeoutPolicy(null!, null!)); + + Assert.Throws(() => _policyFactory.CreateTimeoutPolicy(null!, new TimeoutPolicyOptions())); + + Assert.Throws(() => _policyFactory.CreateRetryPolicy("policy-name", null!)); + } + + [Fact] + public async Task CreateFallbackPolicy_WhenExecutedWithNonHandledException_ShouldNoHandleException() + { + _defaultFallbackPolicyOptions.ShouldHandleException = _ => false; + + var policy = _policyFactory.CreateFallbackPolicy(DummyPolicyName, args => _defaultFallbackAction(args), _defaultFallbackPolicyOptions); + Assert.NotNull(policy); + + await Assert.ThrowsAsync(async () => + await policy.ExecuteAsync(TaskWithException)); + } + + [Fact] + public async Task CreateFallbackPolicy_NonGeneric_WhenExecutedWithNonHandledException_ShouldNoHandleException() + { + _defaultFallbackPolicyOptionsNonGeneric.ShouldHandleException = _ => false; + + var policy = _policyFactory.CreateFallbackPolicy(DummyPolicyName, args => _defaultFallbackAction(args), _defaultFallbackPolicyOptionsNonGeneric); + Assert.NotNull(policy); + + await Assert.ThrowsAsync(async () => await policy.ExecuteAsync(TaskWithException)); + } + + [Fact] + public async Task CreateFallbackPolicy_WhenExecutedWithHandledException_ShouldHandleException() + { + SetupMetering(DummyPolicyName, PolicyEvents.FallbackPolicyEvent); + + var policy = _policyFactory.CreateFallbackPolicy( + DummyPolicyName, + args => _defaultFallbackAction(args), + _defaultFallbackPolicyOptions); + Assert.NotNull(policy); + + var result = await policy.ExecuteAsync(TaskWithException); + Assert.Equal(DefaultFallbackReturnResponse, result); + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogFallback", _loggerMock.LatestRecord.Id.Name); + } + + [Fact] + public async Task CreateFallbackPolicy_NonGeneric_WhenExecutedWithHandledException_ShouldHandleException() + { + SetupMetering(DummyPolicyName, PolicyEvents.FallbackPolicyEvent); + + var policy = _policyFactory.CreateFallbackPolicy( + DummyPolicyName, + args => _defaultFallbackAction(args), + _defaultFallbackPolicyOptionsNonGeneric); + Assert.NotNull(policy); + + await policy.ExecuteAsync(TaskWithExceptionNonGeneric); + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogFallback", _loggerMock.LatestRecord.Id.Name); + } + + [Fact] + public async Task CreateFallback_EnsureExplicitPolicyNameRespected() + { + var policyName = "some-name"; + var options = new TimeoutPolicyOptions + { + TimeoutInterval = TimeSpan.FromMilliseconds(1) + }; + + var policy = _policyFactory.CreateFallbackPolicy(policyName, _ => Task.FromResult(DefaultFallbackReturnResponse), _defaultFallbackPolicyOptions); + + SetupMetering(policyName, PolicyEvents.FallbackPolicyEvent); + + var result = await policy.ExecuteAsync(TaskWithException); + + _policyMeter.VerifyAll(); + } + + [Fact] + public async Task CreateFallback_NonGeneric_EnsureExplicitPolicyNameRespected() + { + var policyName = "some-name"; + var policy = _policyFactory.CreateFallbackPolicy(policyName, _ => Task.FromResult(DefaultFallbackReturnResponse), _defaultFallbackPolicyOptionsNonGeneric); + + SetupMetering(policyName, PolicyEvents.FallbackPolicyEvent); + + await policy.ExecuteAsync(TaskWithExceptionNonGeneric); + + _policyMeter.VerifyAll(); + } + + [Fact] + public async Task ObsoleteCreateFallbackPolicy_WhenExecutedWithHandledException_ShouldHandleException() + { + SetupMetering(DummyPolicyName, PolicyEvents.FallbackPolicyEvent); + + var policy = _policyFactory.CreateFallbackPolicy( + DummyPolicyName, + (_) => Task.FromResult(DefaultFallbackReturnResponse), + _defaultFallbackPolicyOptions); + Assert.NotNull(policy); + + var result = await policy.ExecuteAsync(TaskWithException); + Assert.Equal(DefaultFallbackReturnResponse, result); + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogFallback", _loggerMock.LatestRecord.Id.Name); + } + + [Fact] + public async Task ObsoleteCreateFallbackPolicy_NonGeneric_WhenExecutedWithHandledException_ShouldHandleException() + { + SetupMetering(DummyPolicyName, PolicyEvents.FallbackPolicyEvent); + + var policy = _policyFactory.CreateFallbackPolicy( + DummyPolicyName, + _ => Task.FromResult(DefaultFallbackReturnResponse), + _defaultFallbackPolicyOptionsNonGeneric); + Assert.NotNull(policy); + + await policy.ExecuteAsync(TaskWithExceptionNonGeneric); + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogFallback", _loggerMock.LatestRecord.Id.Name); + } + + [Fact] + public async Task CreateFallbackPolicy_WhenExecutedWithNotHandledErrorResponse_ShouldNotHandleResponse() + { + _defaultFallbackPolicyOptions.ShouldHandleException = _ => false; + + var policy = _policyFactory.CreateFallbackPolicy(DummyPolicyName, args => _defaultFallbackAction(args), _defaultFallbackPolicyOptions); + Assert.NotNull(policy); + + var response = await policy.ExecuteAsync(TaskWithResponse); + Assert.Equal(DefaultStringResponse, response); + } + + [Fact] + public async Task CreateFallbackPolicy_WhenExecutedWithHandledErrorResponse_ShouldHandleResponse() + { + _defaultFallbackPolicyOptions.ShouldHandleResultAsError = _ => true; + _defaultFallbackPolicyOptions.ShouldHandleException = _ => false; + + SetupMetering(DummyPolicyName, PolicyEvents.FallbackPolicyEvent); + + var policy = _policyFactory.CreateFallbackPolicy(DummyPolicyName, args => _defaultFallbackAction(args), _defaultFallbackPolicyOptions); + Assert.NotNull(policy); + + var result = await policy.ExecuteAsync(TaskWithResponse); + Assert.Equal(DefaultFallbackReturnResponse, result); + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogFallback", _loggerMock.LatestRecord.Id.Name); + } + + [Fact] + public async Task CreateFallbackPolicy_WhenExecutedForDisposableObject_ShouldDispose() + { + SetupMetering(DummyPolicyName, PolicyEvents.FallbackPolicyEvent); + + var options = new FallbackPolicyOptions + { + ShouldHandleResultAsError = _ => true, + ShouldHandleException = _ => false + }; + + var fallbackResult = new CustomObject("fallbackResult"); + var policy = _policyFactory.CreateFallbackPolicy( + DummyPolicyName, + _ => Task.FromResult(fallbackResult), + options); + Assert.NotNull(policy); + + var initialResult = new CustomObject("initialResult"); + var result = await policy.ExecuteAsync(() => Task.FromResult(initialResult)); + + Assert.Equal(fallbackResult, result); + Assert.Null(initialResult.Content); + + fallbackResult.Dispose(); + initialResult.Dispose(); + } + + [Fact] + public void CreateFallbackPolicy_NullFallbackAction_ShouldThrow() + { + Assert.Throws(() => + _policyFactory.CreateFallbackPolicy(DummyPolicyName, null!, _defaultFallbackPolicyOptions)); + } + + [Fact] + public void CreateFallbackPolicy_NullOptions_ShouldThrow() + { + Assert.Throws(() => _policyFactory.CreateFallbackPolicy(null!, args => _defaultFallbackAction(args), null!)); + Assert.Throws(() => _policyFactory.CreateFallbackPolicy(null!, arg => _defaultFallbackAction(arg), new FallbackPolicyOptions())); + Assert.Throws(() => _policyFactory.CreateFallbackPolicy("name", arg => _defaultFallbackAction(arg), null!)); + Assert.Throws(() => _policyFactory.CreateFallbackPolicy("name", null!, new FallbackPolicyOptions())); + } + + [Fact] + public async Task CreateBulkheadPolicy_WithDefaultsWhenExecutedWithoutBulkhead_ShouldSucceed() + { + var policy = _policyFactory.CreateBulkheadPolicy(DummyPolicyName, Constants.BulkheadPolicy.DefaultOptions); + + var initialResult = "initialResult"; + var result = await policy.ExecuteAsync(() => Task.FromResult(initialResult)); + Assert.Equal(initialResult, result); + } + + [Fact] + public async Task CreateBulkheadPolicy_WhenExecutedWithBulkhead_ShouldThrowRejection() + { + string operationKey = "SomeKey"; + Context contextPassedToExecute = new Context(operationKey); + Context? contextPassedToOnRejected = null; + var options = new BulkheadPolicyOptions + { + MaxConcurrency = 1, + MaxQueuedActions = 0, + OnBulkheadRejectedAsync = args => + { + contextPassedToOnRejected = args.Context; + return Task.CompletedTask; + } + }; + + SetupMetering(DummyPolicyName, "BulkheadPolicy-OnBulkheadRejected"); + + var policy = (AsyncBulkheadPolicy)_policyFactory.CreateBulkheadPolicy(DummyPolicyName, options)!; + Assert.NotNull(policy); + + TaskCompletionSource tcs = new TaskCompletionSource(); + using (CancellationTokenSource cancellationSource = new CancellationTokenSource()) + { +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + Task.Run(() => + { + _ = policy.ExecuteAsync(async () => + { + await tcs.Task; + return string.Empty; + }); + }).ConfigureAwait(false); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + + Within(_cohesionTimeLimit, () => Expect(0, () => policy.BulkheadAvailableCount, nameof(policy.BulkheadAvailableCount))); + + await Assert.ThrowsAsync(async () => await policy.ExecuteAsync(_ => Task.FromResult("x"), contextPassedToExecute)); + + cancellationSource.Cancel(); + tcs.SetCanceled(); + } + + Assert.NotNull(contextPassedToOnRejected); + Assert.Equal(operationKey, contextPassedToOnRejected!.OperationKey); + Assert.Equal(contextPassedToExecute, contextPassedToOnRejected); + + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogBulkhead", _loggerMock.LatestRecord.Id.Name); + } + + [Fact] + public async Task CreateBulkhead_EnsureExplicitPolicyNameRespected() + { + var policyName = "some-name"; + var options = new BulkheadPolicyOptions + { + MaxConcurrency = 1, + MaxQueuedActions = 0 + }; + var policy = _policyFactory.CreateBulkheadPolicy(policyName, options); + using var cts = new CancellationTokenSource(); + + SetupMetering(policyName, "BulkheadPolicy-OnBulkheadRejected"); + + var t = policy.ExecuteAsync(LongTask, cts.Token); + await Assert.ThrowsAsync(() => policy.ExecuteAsync(LongTask, cts.Token)); + cts.Cancel(); + + try + { + await t; + } + catch (OperationCanceledException) + { + // suppress + } + + static async Task LongTask(CancellationToken token) + { + await Task.Delay(TimeSpan.FromDays(1), token); + return "test"; + } + } + + [Fact] + public void CreateBulkheadPolicyWithDefaultMaxConcurrency() + { + var policy = _policyFactory.CreateBulkheadPolicy(DummyPolicyName, Constants.BulkheadPolicy.DefaultOptions); + Assert.NotNull(policy); + } + + [Fact] + public void CreateBulkheadPolicy_NullConfiguration() + { + Assert.Throws(() => _policyFactory.CreateBulkheadPolicy(null!, null!)); + Assert.Throws(() => _policyFactory.CreateBulkheadPolicy(null!, new BulkheadPolicyOptions())); + Assert.Throws(() => _policyFactory.CreateBulkheadPolicy("name", null!)); + } + + [Fact] + public async Task CreateHedgingPolicy_WhenExecutedWithNonHandledException_ShouldNoHandleException() + { + _defaulHedgingPolicyOptions.ShouldHandleException = _ => false; + + var policy = _policyFactory.CreateHedgingPolicy( + DummyPolicyName, + HedgingTestUtilities.HedgedTasksHandler.FunctionsProvider, + _defaulHedgingPolicyOptions); + Assert.NotNull(policy); + + await Assert.ThrowsAsync(async () => + await policy.ExecuteAsync(TaskWithException)); + } + + [Fact] + public async Task CreateHedgingPolicy_NonGeneric_WhenExecutedWithNonHandledException_ShouldNoHandleException() + { + _defaulHedgingPolicyOptionsNonGeneric.ShouldHandleException = _ => false; + + var policy = _policyFactory.CreateHedgingPolicy( + "HedgingPolicy", + HedgingTestUtilities.HedgedTasksHandler.FunctionsProviderNonGeneric, + _defaulHedgingPolicyOptionsNonGeneric); + Assert.NotNull(policy); + + await Assert.ThrowsAsync(async () => + await policy.ExecuteAsync(TaskWithException)); + } + + [Fact] + public async Task CreateHedgingPolicy_WhenExecutedWithHandledException_ShouldHandleException() + { + var fakeTimeProvider = new FakeTimeProvider(); + SystemClock.SleepAsync = fakeTimeProvider.DelayAndAdvanceAsync; + + SetupMetering(DummyPolicyName, PolicyEvents.HedgingPolicyEvent); + + var options = new HedgingPolicyOptions + { + MaxHedgedAttempts = HedgingTestUtilities.HedgedTasksHandler.MaxHedgedTasks, + HedgingDelay = HedgingTestUtilities.DefaultHedgingDelay + }; + + var policy = (_policyFactory.CreateHedgingPolicy( + DummyPolicyName, + HedgingTestUtilities.HedgedTasksHandler.FunctionsProvider, + options) as AsyncHedgingPolicy)!; + Assert.NotNull(policy); + + var result = await policy.ExecuteAsync(TaskWithException).AdvanceTimeUntilFinished(_timeProvider); + + Assert.Contains(result, + new[] + { + "Oranges", "Pears", "Apples" + }); + + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogHedging", _loggerMock.LatestRecord.Id.Name); + } + + [Fact] + public async Task CreateHedgingPolicy_NonGeneric_WhenExecutedWithHandledException_ShouldHandleException() + { + var fakeTimeProvider = new FakeTimeProvider(); + SystemClock.SleepAsync = fakeTimeProvider.DelayAndAdvanceAsync; + + SetupMetering(DummyPolicyName, PolicyEvents.HedgingPolicyEvent); + + var options = new HedgingPolicyOptions + { + MaxHedgedAttempts = HedgingTestUtilities.HedgedTasksHandler.MaxHedgedTasks, + HedgingDelay = HedgingTestUtilities.DefaultHedgingDelay + }; + + var policy = (_policyFactory.CreateHedgingPolicy( + DummyPolicyName, + HedgingTestUtilities.HedgedTasksHandler.FunctionsProviderNonGeneric, + options) as AsyncHedgingPolicy)!; + Assert.NotNull(policy); + + await policy.ExecuteAsync(TaskWithException); + + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogHedging", _loggerMock.LatestRecord.Id.Name); + } + + [Fact] + public async Task CreateHedgingPolicy_WhenExecutedWithNotHandledErrorResponse_ShouldNotHandleResponse() + { + _defaulHedgingPolicyOptions.ShouldHandleException = _ => false; + + var policy = _policyFactory.CreateHedgingPolicy( + DummyPolicyName, + HedgingTestUtilities.HedgedTasksHandler.FunctionsProvider, + _defaulHedgingPolicyOptions); + Assert.NotNull(policy); + + var response = await policy.ExecuteAsync(TaskWithResponse).AdvanceTimeUntilFinished(_timeProvider); + Assert.Equal(DefaultStringResponse, response); + } + + [Fact] + public async Task CreateHedgingPolicy_WhenExecutedWithHandledErrorResponse_ShouldHandleResponse() + { + var fakeTimeProvider = new FakeTimeProvider(); + SystemClock.SleepAsync = fakeTimeProvider.DelayAndAdvanceAsync; + + SetupMetering(DummyPolicyName, PolicyEvents.HedgingPolicyEvent); + + var options = new HedgingPolicyOptions + { + MaxHedgedAttempts = HedgingTestUtilities.HedgedTasksHandler.MaxHedgedTasks, + HedgingDelay = HedgingTestUtilities.DefaultHedgingDelay, + ShouldHandleException = _ => true, + ShouldHandleResultAsError = _ => true + }; + + var policy = (_policyFactory.CreateHedgingPolicy( + DummyPolicyName, + HedgingTestUtilities.HedgedTasksHandler.FunctionsProvider, + options) as AsyncHedgingPolicy)!; + Assert.NotNull(policy); + + var result = await policy.ExecuteAsync(TaskWithResponse).AdvanceTimeUntilFinished(fakeTimeProvider); + + Assert.Contains(result, + new[] + { + "Oranges", "Pears", "Apples", + DefaultStringResponse + }); + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogHedging", _loggerMock.LatestRecord.Id.Name); + } + + [Fact] + public async Task CreateHedgingPolicy_NonGeneric_WhenExecutedWithHandledErrorResponse_ShouldHandleResponse() + { + var fakeTimeProvider = new FakeTimeProvider(); + SystemClock.SleepAsync = fakeTimeProvider.DelayAndAdvanceAsync; + + SetupMetering("Name", PolicyEvents.HedgingPolicyEvent); + + var options = new HedgingPolicyOptions + { + MaxHedgedAttempts = HedgingTestUtilities.HedgedTasksHandler.MaxHedgedTasks, + HedgingDelay = HedgingTestUtilities.DefaultHedgingDelay, + ShouldHandleException = _ => true + }; + + var policy = (_policyFactory.CreateHedgingPolicy( + DummyPolicyName, + HedgingTestUtilities.HedgedTasksHandler.FunctionsProviderNonGeneric, + options) as AsyncHedgingPolicy)!; + Assert.NotNull(policy); + + await policy.ExecuteAsync(() => Task.FromException(new ArgumentNullException())); + + Assert.Equal(LogLevel.Warning, _loggerMock.LatestRecord.Level); + Assert.Equal("LogHedging", _loggerMock.LatestRecord.Id.Name); + } + + [Fact] + public void CreateHedgingPolicy_NullArguments_ShouldThrow() + { + Assert.Throws(() => + _policyFactory.CreateHedgingPolicy( + DummyPolicyName, + null!, + Constants.HedgingPolicy.DefaultOptions())); + + Assert.Throws(() => + _policyFactory.CreateHedgingPolicy( + DummyPolicyName, + HedgingTestUtilities.HedgedTasksHandler.FunctionsProvider, + null!)); + + Assert.Throws(() => + _policyFactory.CreateHedgingPolicy( + DummyPolicyName, + null!, + Constants.HedgingPolicy.DefaultOptions())); + } + + [Fact] + public void CreateHedgingPolicy_NonGeneric_NullArguments_ShouldThrow() + { + Assert.Throws(() => + _policyFactory.CreateHedgingPolicy( + "HedgingPolicy", + null!, + Constants.HedgingPolicyNonGeneric.DefaultOptions())); + + Assert.Throws(() => + _policyFactory.CreateHedgingPolicy( + "HedgingPolicy", + HedgingTestUtilities.HedgedTasksHandler.FunctionsProviderNonGeneric, + null!)); + + Assert.Throws(() => + _policyFactory.CreateHedgingPolicy( + "HedgingPolicy", + null!, + Constants.HedgingPolicyNonGeneric.DefaultOptions())); + } + + [Fact] + public async Task CreateHedging_EnsureExplicitPolicyNameRespected() + { + var policyName = "some-name"; + var options = new HedgingPolicyOptions + { + ShouldHandleResultAsError = e => e == "error" + }; + var policy = _policyFactory.CreateHedgingPolicy(policyName, _defaultHedgedTaskProvider, options); + using var cts = new CancellationTokenSource(); + + SetupMetering(policyName, PolicyEvents.HedgingPolicyEvent); + + await policy.ExecuteAsync(() => Task.FromResult("error")); + _policyMeter.VerifyAll(); + } + + [Fact] + public async Task CreateHedging_NonGeneric_EnsureExplicitPolicyNameRespected() + { + var policyName = "some-name"; + var options = new HedgingPolicyOptions(); + var policy = _policyFactory.CreateHedgingPolicy(policyName, _defaultHedgedTaskProviderNonGeneric, options); + using var cts = new CancellationTokenSource(); + + SetupMetering(policyName, PolicyEvents.HedgingPolicyEvent); + + await policy.ExecuteAsync(() => Task.FromException(new ArgumentNullException())); + + _policyMeter.VerifyAll(); + } + + [Fact] + public void CreateHedgingPolicy_NullConfiguration() + { + Assert.Throws(() => _policyFactory.CreateHedgingPolicy(DummyPolicyName, _defaultHedgedTaskProvider, null!)); + Assert.Throws(() => _policyFactory.CreateHedgingPolicy(DummyPolicyName, null!, _defaulHedgingPolicyOptions)); + + Assert.Throws(() => _policyFactory.CreateHedgingPolicy("name", _defaultHedgedTaskProvider, null!)); + Assert.Throws(() => _policyFactory.CreateHedgingPolicy("name", null!, _defaulHedgingPolicyOptions)); + Assert.Throws(() => _policyFactory.CreateHedgingPolicy(null!, _defaultHedgedTaskProvider, _defaulHedgingPolicyOptions)); + } + + [Fact] + public void CreateHedgingPolicy_NonGeneric_NullConfiguration() + { + Assert.Throws(() => _policyFactory.CreateHedgingPolicy("HedgingPolicy", _defaultHedgedTaskProviderNonGeneric, null!)); + Assert.Throws(() => _policyFactory.CreateHedgingPolicy("HedgingPolicy", null!, _defaulHedgingPolicyOptionsNonGeneric)); + + Assert.Throws(() => _policyFactory.CreateHedgingPolicy("name", _defaultHedgedTaskProviderNonGeneric, null!)); + Assert.Throws(() => _policyFactory.CreateHedgingPolicy("name", null!, _defaulHedgingPolicyOptionsNonGeneric)); + Assert.Throws(() => _policyFactory.CreateHedgingPolicy(null!, _defaultHedgedTaskProviderNonGeneric, _defaulHedgingPolicyOptionsNonGeneric)); + } + + [Fact] + public void SetPipelineIdentifiers_EnsureMeterInitialized() + { + var id = PipelineId.Create("pipeline", "key"); + + _policyMeter.Setup(v => v.Initialize(id)); + _policyFactory.Initialize(id); + + _policyMeter.VerifyAll(); + } + + private static Task TaskWithResponse() + { + return Task.FromResult(DefaultStringResponse); + } + + private static Task TaskWithResponseNonGeneric() + { + return Task.FromResult(DefaultStringResponse); + } + + private static Task TaskWithException() + { + throw new InvalidOperationException("Something went wrong"); + } + + private static Task TaskWithExceptionNonGeneric() + { + throw new InvalidOperationException("Something went wrong"); + } + + private static AssertionFailure? Expect(int expected, Func actualFunc, string measure) + { + int actual = actualFunc(); + return actual != expected ? new AssertionFailure(expected, actual, measure) : null; + } + + private void SetupMetering(string policyName, string eventName) + { + _policyMeter.Setup(x => x.RecordEvent( + policyName, + eventName, + It.IsAny>(), + It.IsAny())); + } + + private void SetupMetering(string policyName, string eventName) + { + _policyMeter.Setup(x => x.RecordEvent( + policyName, + eventName, + It.IsAny(), + It.IsAny())); + } + + private void Within(TimeSpan timeSpan, Func actionContainingAssertions) + { + TimeSpan permitted = timeSpan; + Stopwatch watch = Stopwatch.StartNew(); + while (true) + { + var potentialFailure = actionContainingAssertions(); + if (potentialFailure == null) + { + break; + } + + if (watch.Elapsed > permitted) + { + _output.WriteLine("Failing assertion on: {0}", potentialFailure.Measure); + Assert.Equal(potentialFailure.Expected, potentialFailure.Actual); + throw new InvalidOperationException("Code should never reach here. Preceding assertion should fail."); + } + + bool signaled = _statusChangedEvent.WaitOne(_shimTimeSpan); + if (signaled) + { + // Following TraceableAction.CaptureCompletion() signalling the AutoResetEvent, + // there can be race conditions between on the one hand exiting the bulkhead semaphore (and potentially another execution gaining it), + // and the assertion being verified here about those same facts. + // If that race is lost by the real-world state change, and the AutoResetEvent signal occurred very close to timeoutTime, + // there might not be a second chance. + // We therefore permit another shim time for the condition to come good. + permitted += _cohesionTimeLimit; + } + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/PolicyMeteringTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/PolicyMeteringTests.cs new file mode 100644 index 0000000000..c21d2955e4 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/PolicyMeteringTests.cs @@ -0,0 +1,211 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Diagnostics.ExceptionSummarization; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metering; +using Moq; +using Polly; +using Xunit; + +#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize +#pragma warning disable CA1063 // Implement IDisposable Correctly + +namespace Microsoft.Extensions.Resilience.Polly.Test.Internals; + +public class PolicyMeteringTests : IDisposable +{ + private const string PipelineName = "pipeline-name"; + + private const string PipelineKey = "pipeline-key"; + + private const string ResultType = "String"; + + private const string MetricName = @"R9\Resilience\Policies"; + + private readonly Mock _summarizer; + private readonly Mock _outgoingContext; + private readonly Meter _meter; + private readonly MetricCollector _metricCollector; + private PolicyMetering _metering; + private FailureResultContext _context; + + public PolicyMeteringTests() + { + _summarizer = new Mock(MockBehavior.Strict); + _outgoingContext = new Mock(MockBehavior.Strict); + + _meter = new(); + _metricCollector = new(_meter); + + var services = new ServiceCollection(); + services.TryAddSingleton(_outgoingContext.Object); + services.ConfigureFailureResultContext(v => _context); + + _metering = new PolicyMetering(_meter, _summarizer.Object, services.BuildServiceProvider()); + } + + public void Dispose() + { + _metricCollector.Dispose(); + _meter.Dispose(); + } + + [Fact] + public void Initialize_Twice_Throws() + { + Initialize(); + Assert.Throws(() => Initialize()); + } + + [InlineData("", TelemetryConstants.Unknown)] + [InlineData(null, TelemetryConstants.Unknown)] + [InlineData(TelemetryConstants.Unknown, TelemetryConstants.Unknown)] + [InlineData("test", "test")] + [Theory] + public void Initialize_EnsurePipelineKeyRespected(string pipelineKey, string expectedKey) + { + Initialize(pipelineKey); + + RecordEvent(true, "policy", "ev", null); + + Assert.Equal(expectedKey, Counter.LatestWritten!.GetDimension(ResilienceDimensions.PipelineKey)); + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public void NotInitialized_DoesNotMeter(bool typed) + { + RecordEvent(typed, "policy", "ev", null); + + Counter.AllValues.Should().BeEmpty(); + } + + [Fact] + public void NoOutgoingContext_ShouldNotThrow() + { + var services = new ServiceCollection(); + services.AddOptions(); + + _metering = new PolicyMetering(_meter, _summarizer.Object, services.BuildServiceProvider()); + + Initialize(); + + RecordEvent(true, "policy", "ev", null); + + Assert.Equal(TelemetryConstants.Unknown, Counter.LatestWritten!.GetDimension(ResilienceDimensions.DependencyName)); + Assert.Equal(TelemetryConstants.Unknown, Counter.LatestWritten!.GetDimension(ResilienceDimensions.RequestName)); + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public void RecordEvent_NullException_Ok(bool typed) + { + Initialize(); + + RecordEvent(typed, "policy", "ev", null); + + var latest = Counter.LatestWritten; + + Assert.Equal(PipelineName, latest!.GetDimension(ResilienceDimensions.PipelineName)); + Assert.Equal(PipelineKey, latest.GetDimension(ResilienceDimensions.PipelineKey)); + Assert.Equal(ResultType, latest.GetDimension(ResilienceDimensions.ResultType)); + Assert.Equal("policy", latest.GetDimension(ResilienceDimensions.PolicyName)); + Assert.Equal("ev", latest.GetDimension(ResilienceDimensions.EventName)); + Assert.Equal(TelemetryConstants.Unknown, latest.GetDimension(ResilienceDimensions.FailureSource)); + Assert.Equal(TelemetryConstants.Unknown, latest.GetDimension(ResilienceDimensions.FailureReason)); + Assert.Equal(TelemetryConstants.Unknown, latest.GetDimension(ResilienceDimensions.FailureSummary)); + Assert.Equal(TelemetryConstants.Unknown, latest.GetDimension(ResilienceDimensions.DependencyName)); + Assert.Equal(TelemetryConstants.Unknown, latest.GetDimension(ResilienceDimensions.RequestName)); + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public void RecordEvent_Exception_Ok(bool typed) + { + Initialize(); + var er = new InvalidOperationException(); + + _summarizer.Setup(v => v.Summarize(er)).Returns(new ExceptionSummary("type", "desc", "details")); + + RecordEvent(typed, "policy", "ev", er); + + var latest = Counter.LatestWritten; + + Assert.Equal(TelemetryConstants.Unknown, latest!.GetDimension(ResilienceDimensions.FailureSource)); + Assert.Equal("InvalidOperationException", latest.GetDimension(ResilienceDimensions.FailureReason)); + Assert.Equal("type:desc:details", latest.GetDimension(ResilienceDimensions.FailureSummary)); + } + + [Fact] + public void RecordEvent_FailureResult_Ok() + { + Initialize(); + _context = FailureResultContext.Create("src", "reason", "summary"); + _metering.RecordEvent("policy", "ev", new DelegateResult("test"), new Context()); + + var latest = Counter.LatestWritten; + + Assert.Equal("src", latest!.GetDimension(ResilienceDimensions.FailureSource)); + Assert.Equal("reason", latest.GetDimension(ResilienceDimensions.FailureReason)); + Assert.Equal("summary", latest.GetDimension(ResilienceDimensions.FailureSummary)); + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public void RecordEvent_RequestMetadata(bool typed) + { + Initialize(); + var er = new InvalidOperationException(); + var metadata1 = new RequestMetadata { DependencyName = "dep", RequestName = "req" }; + var metadata2 = new RequestMetadata { DependencyName = "dep2", RequestName = "req2" }; + + _outgoingContext.Setup(o => o.RequestMetadata).Returns(metadata1); + RecordEvent(typed, "policy", "ev", null, new Context()); + + var latest = Counter.LatestWritten; + + Assert.Equal("dep", latest!.GetDimension(ResilienceDimensions.DependencyName)); + Assert.Equal("req", latest.GetDimension(ResilienceDimensions.RequestName)); + + var ctx = new Context + { + [TelemetryConstants.RequestMetadataKey] = metadata2 + }; + RecordEvent(typed, "policy", "ev", null, ctx); + + latest = Counter.LatestWritten; + Assert.Equal("dep2", latest!.GetDimension(ResilienceDimensions.DependencyName)); + Assert.Equal("req2", latest.GetDimension(ResilienceDimensions.RequestName)); + } + + private void RecordEvent(bool typed, string policyName, string eventName, Exception? exception, Context? context = null) + { + if (typed) + { + _metering.RecordEvent(policyName, eventName, exception != null ? new DelegateResult(exception) : null, context ?? new Context()); + } + else + { + _metering.RecordEvent(policyName, eventName, exception, context ?? new Context()); + } + } + + private MetricValuesHolder Counter => _metricCollector.GetCounterValues(MetricName)!; + + private void Initialize(string pipelineKey = PipelineKey) + { + _metering.Initialize(PipelineId.Create(PipelineName, pipelineKey)); + _outgoingContext.Setup(o => o.RequestMetadata).Returns(null); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/RetryPolicyOptionsCustomValidatorTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/RetryPolicyOptionsCustomValidatorTests.cs new file mode 100644 index 0000000000..ede491ed14 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Internals/RetryPolicyOptionsCustomValidatorTests.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Internals; + +public class RetryPolicyOptionsCustomValidatorTests +{ + [InlineData(2_147_483_647, true)] + [InlineData(2_147_483_648, false)] + [Theory] + public void Validate_Ok(long baseDelay, bool valid) + { + var options = new RetryPolicyOptions + { + BaseDelay = TimeSpan.FromMilliseconds(baseDelay), + BackoffType = BackoffType.Linear, + RetryCount = 1 + }; + + var validator = new RetryPolicyOptionsCustomValidator(); + var errors = validator.Validate("dummy", options); + + if (valid) + { + Assert.False(errors.Failed); + } + else + { + Assert.True(errors.Failed); + Assert.Equal( + $"Property RetryCount: unable to validate retry delay #0 = {baseDelay}. Must be a positive TimeSpan and less than {int.MaxValue} milliseconds long.", + errors.FailureMessage); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/BreakActionArgumentsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/BreakActionArgumentsTests.cs new file mode 100644 index 0000000000..b17ae2a10a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/BreakActionArgumentsTests.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.Extensions.Resilience.Options; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class BreakActionArgumentsTests +{ + [Fact] + public void Constructor_NoParameters_ShouldInitialize() + { +#pragma warning disable SA1129 + var instance = new BreakActionArguments(); + +#pragma warning disable xUnit2002 + Assert.NotNull(instance); + } + + [Fact] + public void Constructor_With4Parameters_ShouldInitializeProperties() + { + var expectedError = "Something went wrong"; + var delegateResult = new DelegateResult(new InvalidOperationException(expectedError)); + var expectedBreakDuration = TimeSpan.FromSeconds(2); + var context = new Context(); + var cancellationToken = new CancellationToken(); + + var instance = new BreakActionArguments( + delegateResult, + context, + expectedBreakDuration, + cancellationToken); + + Assert.NotNull(instance); + Assert.Equal(delegateResult, instance.Result); + Assert.Equal(expectedBreakDuration, instance.BreakDuration); + Assert.Equal(context, instance.Context); + Assert.Equal(cancellationToken, instance.CancellationToken); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/BreakActionArgumentsTestsNonGeneric.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/BreakActionArgumentsTestsNonGeneric.cs new file mode 100644 index 0000000000..d53b0f68c6 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/BreakActionArgumentsTestsNonGeneric.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.Extensions.Resilience.Options; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class BreakActionArgumentsTestsNonGeneric +{ + [Fact] + public void Constructor_NoParameters_ShouldInitialize() + { +#pragma warning disable SA1129 + var instance = new BreakActionArguments(); + +#pragma warning disable xUnit2002 + Assert.NotNull(instance); + } + + [Fact] + public void Constructor_With4Parameters_ShouldInitializeProperties() + { + var expectedError = "Something went wrong"; + var exception = new InvalidOperationException(expectedError); + var expectedBreakDuration = TimeSpan.FromSeconds(2); + var context = new Context(); + var cancellationToken = new CancellationToken(); + + var instance = new BreakActionArguments( + exception, + context, + expectedBreakDuration, + cancellationToken); + + Assert.NotNull(instance); + Assert.Equal(exception, instance.Exception); + Assert.Equal(expectedBreakDuration, instance.BreakDuration); + Assert.Equal(context, instance.Context); + Assert.Equal(cancellationToken, instance.CancellationToken); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/BulkheadPolicyOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/BulkheadPolicyOptionsTests.cs new file mode 100644 index 0000000000..3450b8f4a7 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/BulkheadPolicyOptionsTests.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Options; +using Xunit; +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class BulkheadPolicyOptionsTests +{ + private readonly BulkheadPolicyOptions _testClass; + + public BulkheadPolicyOptionsTests() + { + _testClass = new BulkheadPolicyOptions(); + } + + [Fact] + public void Constructor_ShouldInitialize() + { + var instance = new BulkheadPolicyOptions(); + Assert.NotNull(instance); + } + + [Fact] + public void MaxConcurrencyProperty_ShouldGetAndSet() + { + var testValue = 100; + _testClass.MaxConcurrency = testValue; + Assert.Equal(testValue, _testClass.MaxConcurrency); + } + + [Fact] + public void MaxQueuedActionsProperty_ShouldGetAndSet() + { + var testValue = 1; + _testClass.MaxQueuedActions = testValue; + Assert.Equal(testValue, _testClass.MaxQueuedActions); + } + + [Fact] + public void DefaultInstance_ShouldInitializeWithDefault() + { + var defaultConfiguration = Constants.BulkheadPolicy.DefaultOptions; + Assert.NotNull(defaultConfiguration); + Assert.Equal(1000, defaultConfiguration.MaxConcurrency); + Assert.Equal(Constants.BulkheadPolicy.DefaultOptions.MaxQueuedActions, defaultConfiguration.MaxQueuedActions); + } + + [Fact] + public void OnRejected_ValidValue_ShouldGetAndSet() + { + Func testValue = _ => Task.CompletedTask; + + _testClass.OnBulkheadRejectedAsync = testValue; + Assert.Equal(testValue, _testClass.OnBulkheadRejectedAsync); + } + + [Fact] + public void OnRejectedSet_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.OnBulkheadRejectedAsync = null!); + } + + [Fact] + public async Task OnRejected_DefaultValue_ShouldBeInitialized() + { + var instance = new BulkheadPolicyOptions(); + Assert.NotNull(instance); + + var function = instance.OnBulkheadRejectedAsync; + Assert.NotNull(function); + + var args = default(BulkheadTaskArguments); + await function(args); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/BulkheadTaskArgumentsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/BulkheadTaskArgumentsTests.cs new file mode 100644 index 0000000000..b8f00a0b82 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/BulkheadTaskArgumentsTests.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Microsoft.Extensions.Resilience.Options; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class BulkheadTaskArgumentsTests +{ + [Fact] + public void Constructor_NoParameters_ShouldInitialize() + { +#pragma warning disable SA1129 + var instance = new BulkheadTaskArguments(); + +#pragma warning disable xUnit2002 + Assert.NotNull(instance); + } + + [Fact] + public void Constructor_WithParameters_ShouldInitializeProperties() + { + var context = new Context(); + var instance = new BulkheadTaskArguments( + context, + CancellationToken.None); + + Assert.NotNull(instance); + Assert.NotNull(instance.CancellationToken); + Assert.Equal(context, instance.Context); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/CircuitBreakerPolicyOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/CircuitBreakerPolicyOptionsTests.cs new file mode 100644 index 0000000000..afb93ecd17 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/CircuitBreakerPolicyOptionsTests.cs @@ -0,0 +1,174 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Resilience.Options; +using Xunit; +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class CircuitBreakerPolicyOptionsTests +{ + private readonly CircuitBreakerPolicyOptions _testClass; + + public CircuitBreakerPolicyOptionsTests() + { + _testClass = new CircuitBreakerPolicyOptions(); + } + + [Fact] + public void Constructor_ShouldInitialize() + { + var instance = new CircuitBreakerPolicyOptions(); + Assert.NotNull(instance); + } + + [Fact] + public void FailureThreshold_ValidValue_ShouldGetAndSet() + { + const double TestValue = .09; + _testClass.FailureThreshold = TestValue; + + Assert.Equal(TestValue, _testClass.FailureThreshold); + OptionsUtilities.ValidateOptions(_testClass); + } + + [Theory] + [InlineData(-1)] + [InlineData(1.1)] + [InlineData(0.0)] + [InlineData(0)] + [InlineData(-0.5)] + public void FailureThreshold_InvalidValue_ShouldThrow(double testValue) + { + _testClass.FailureThreshold = testValue; + Assert.Throws(() => + OptionsUtilities.ValidateOptions(_testClass)); + } + + [Fact] + public void MinimumThroughput_ValidValue_ShouldGetAndSet() + { + const int TestValue = 931_955_621; + _testClass.MinimumThroughput = TestValue; + + Assert.Equal(TestValue, _testClass.MinimumThroughput); + OptionsUtilities.ValidateOptions(_testClass); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + public void MinimumThroughput_InvalidValue_ShouldThrow(int testValue) + { + _testClass.MinimumThroughput = testValue; + Assert.Throws(() => + OptionsUtilities.ValidateOptions(_testClass)); + } + + [Fact] + + public void BreakDuration_ValidValue_ShouldGetAndSet() + { + var testValue = TimeSpan.FromMilliseconds(567); + + _testClass.BreakDuration = testValue; + + Assert.Equal(testValue, _testClass.BreakDuration); + OptionsUtilities.ValidateOptions(_testClass); + } + + [Fact] + public void BreakDuration_SetInvalidValue_ShouldThrow() + { + var testValue = TimeSpan.FromDays(-1); + _testClass.BreakDuration = testValue; + Assert.Throws(() => + OptionsUtilities.ValidateOptions(_testClass)); + } + + [Fact] + public void SamplingDuration_ValidValueShouldGetAndSet() + { + var testValue = TimeSpan.FromMilliseconds(567); + _testClass.SamplingDuration = testValue; + + Assert.Equal(testValue, _testClass.SamplingDuration); + OptionsUtilities.ValidateOptions(_testClass); + } + + [Fact] + public void SamplingDuration_InvalidValue_ShouldThrow() + { + var testValue = TimeSpan.Zero; + _testClass.SamplingDuration = testValue; + Assert.Throws(() => + OptionsUtilities.ValidateOptions(_testClass)); + } + + [Fact] + public void ShouldHandleResultAsError_ValidValue_ShouldGetAndSet() + { + Predicate testValue = _ => true; + _testClass.ShouldHandleResultAsError = testValue; + Assert.Equal(testValue, _testClass.ShouldHandleResultAsError); + } + + [Fact] + public void ShouldHandleResultAsError_DefaultValue_ShouldReturnFalse() + { + var shouldHandle = _testClass.ShouldHandleResultAsError(string.Empty); + Assert.False(shouldHandle); + } + + [Fact] + public void ShouldHandleResultAsError_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.ShouldHandleResultAsError = null!); + } + + [Fact] + public void ShouldHandleException_ValidValue_ShouldGetAndSet() + { + Predicate testValue = _ => true; + _testClass.ShouldHandleException = testValue; + Assert.Equal(testValue, _testClass.ShouldHandleException); + } + + [Fact] + public void ShouldHandleException_DefaultValue_ShouldReturnTrue() + { + var shouldHandle = _testClass.ShouldHandleException(new AggregateException()); + Assert.True(shouldHandle); + } + + [Fact] + public void ShouldHandleException_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.ShouldHandleException = null!); + } + + [Fact] + public void OnCircuitBreak_ValidValue_ShouldGetAndSet() + { + Action> testValue = _ => { }; + + _testClass.OnCircuitBreak = testValue; + Assert.Equal(testValue, _testClass.OnCircuitBreak); + } + + [Fact] + public void OnCircuitBreakSet_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.OnCircuitBreak = null!); + } + + [Fact] + public void OnCircuitReset_ValidValue_ShouldGetAndSet() + { + Action testValue = _ => { }; + _testClass.OnCircuitReset = testValue; + + Assert.Equal(testValue, _testClass.OnCircuitReset); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/CircuitBreakerPolicyOptionsTestsNonGeneric.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/CircuitBreakerPolicyOptionsTestsNonGeneric.cs new file mode 100644 index 0000000000..6ac4d25136 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/CircuitBreakerPolicyOptionsTestsNonGeneric.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Resilience.Options; +using Xunit; +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class CircuitBreakerPolicyOptionsTestsNonGeneric +{ + private readonly CircuitBreakerPolicyOptions _testClass; + + public CircuitBreakerPolicyOptionsTestsNonGeneric() + { + _testClass = new CircuitBreakerPolicyOptions(); + } + + [Fact] + public void Constructor_ShouldInitialize() + { + var instance = new CircuitBreakerPolicyOptions(); + Assert.NotNull(instance); + } + + [Fact] + public void FailureThreshold_ValidValue_ShouldGetAndSet() + { + const double TestValue = .09; + _testClass.FailureThreshold = TestValue; + + Assert.Equal(TestValue, _testClass.FailureThreshold); + OptionsUtilities.ValidateOptions(_testClass); + } + + [Theory] + [InlineData(-1)] + [InlineData(1.1)] + [InlineData(0.0)] + [InlineData(0)] + [InlineData(-0.5)] + public void FailureThreshold_InvalidValue_ShouldThrow(double testValue) + { + _testClass.FailureThreshold = testValue; + Assert.Throws(() => + OptionsUtilities.ValidateOptions(_testClass)); + } + + [Fact] + public void MinimumThroughput_ValidValue_ShouldGetAndSet() + { + const int TestValue = 931_955_621; + _testClass.MinimumThroughput = TestValue; + + Assert.Equal(TestValue, _testClass.MinimumThroughput); + OptionsUtilities.ValidateOptions(_testClass); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + public void MinimumThroughput_InvalidValue_ShouldThrow(int testValue) + { + _testClass.MinimumThroughput = testValue; + Assert.Throws(() => + OptionsUtilities.ValidateOptions(_testClass)); + } + + [Fact] + + public void BreakDuration_ValidValue_ShouldGetAndSet() + { + var testValue = TimeSpan.FromMilliseconds(567); + _testClass.BreakDuration = testValue; + Assert.Equal(testValue, _testClass.BreakDuration); + OptionsUtilities.ValidateOptions(_testClass); + } + + [Fact] + public void BreakDuration_SetInvalidValue_ShouldThrow() + { + var testValue = TimeSpan.FromDays(-1); + _testClass.BreakDuration = testValue; + Assert.Throws(() => + OptionsUtilities.ValidateOptions(_testClass)); + } + + [Fact] + public void SamplingDuration_ValidValueShouldGetAndSet() + { + var testValue = TimeSpan.FromMilliseconds(567); + _testClass.SamplingDuration = testValue; + + Assert.Equal(testValue, _testClass.SamplingDuration); + OptionsUtilities.ValidateOptions(_testClass); + } + + [Fact] + public void SamplingDuration_InvalidValue_ShouldThrow() + { + var testValue = TimeSpan.Zero; + _testClass.SamplingDuration = testValue; + Assert.Throws(() => + OptionsUtilities.ValidateOptions(_testClass)); + } + + [Fact] + public void ShouldHandleException_ValidValue_ShouldGetAndSet() + { + Predicate testValue = _ => true; + _testClass.ShouldHandleException = testValue; + Assert.Equal(testValue, _testClass.ShouldHandleException); + } + + [Fact] + public void ShouldHandleException_DefaultValue_ShouldReturnTrue() + { + var shouldHandle = _testClass.ShouldHandleException(new AggregateException()); + Assert.True(shouldHandle); + } + + [Fact] + public void ShouldHandleException_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.ShouldHandleException = null!); + } + + [Fact] + public void OnCircuitBreak_ValidValue_ShouldGetAndSet() + { + Action testValue = _ => { }; + + _testClass.OnCircuitBreak = testValue; + Assert.Equal(testValue, _testClass.OnCircuitBreak); + } + + [Fact] + public void OnCircuitBreakSet_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.OnCircuitBreak = null!); + } + + [Fact] + public void OnCircuitReset_ValidValue_ShouldGetAndSet() + { + Action testValue = _ => { }; + _testClass.OnCircuitReset = testValue; + + Assert.Equal(testValue, _testClass.OnCircuitReset); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/Constants.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/Constants.cs new file mode 100644 index 0000000000..407a6b11a5 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/Constants.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Resilience.Options; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public static class Constants +{ + public static class BulkheadPolicy + { + public static readonly BulkheadPolicyOptions DefaultOptions = new(); + } + + public static class CircuitBreakerPolicy + { + public static CircuitBreakerPolicyOptions DefaultOptions() => new(); + } + + public static class FallbackPolicy + { + public static FallbackPolicyOptions DefaultOptions() => new(); + } + + public static class HedgingPolicy + { + public static HedgingPolicyOptions DefaultOptions() => new(); + } + + public static class HedgingPolicyNonGeneric + { + public static HedgingPolicyOptions DefaultOptions() => new(); + } + + public static class RetryPolicy + { + public static RetryPolicyOptions DefaultOptions() => new(); + } + + public static class RetryPolicyNonGeneric + { + public static RetryPolicyOptions DefaultOptions() => new(); + } + + public static class TimeoutPolicy + { + public static TimeoutPolicyOptions DefaultOptions => new(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackPolicyOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackPolicyOptionsTests.cs new file mode 100644 index 0000000000..f32b15d912 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackPolicyOptionsTests.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Options; +using Xunit; +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class FallbackPolicyOptionsTests +{ + private readonly FallbackPolicyOptions _testClass; + + public FallbackPolicyOptionsTests() + { + _testClass = new FallbackPolicyOptions(); + } + + [Fact] + public void Constructor_ShouldInitialize() + { + Assert.NotNull(_testClass); + } + + [Fact] + public void ShouldHandleResultAsError_ValidValue_ShouldGetAndSet() + { + Predicate testValue = _ => true; + _testClass.ShouldHandleResultAsError = testValue; + Assert.Equal(testValue, _testClass.ShouldHandleResultAsError); + } + + [Fact] + public void ShouldHandleResultAsError_DefaultValue_ShouldReturnFalse() + { + var shouldHandle = _testClass.ShouldHandleResultAsError(string.Empty); + Assert.False(shouldHandle); + } + + [Fact] + public void ShouldHandleResultAsError_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.ShouldHandleResultAsError = null!); + } + + [Fact] + public void ShouldHandleException_ValidValue_ShouldGetAndSet() + { + Predicate testValue = _ => true; + _testClass.ShouldHandleException = testValue; + Assert.Equal(testValue, _testClass.ShouldHandleException); + } + + [Fact] + public void ShouldHandleException_DefaultValue_ShouldReturnTrue() + { + var shouldHandle = _testClass.ShouldHandleException(new AggregateException()); + Assert.True(shouldHandle); + } + + [Fact] + public void ShouldHandleException_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.ShouldHandleException = null!); + } + + [Fact] + public void OnFallback_ValidValue_ShouldGetAndSet() + { + Func, Task> testValue = _ => Task.CompletedTask; + + _testClass.OnFallbackAsync = testValue; + Assert.Equal(testValue, _testClass.OnFallbackAsync); + } + + [Fact] + public void OnFallbackSet_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.OnFallbackAsync = null!); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackPolicyOptionsTestsNonGeneric.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackPolicyOptionsTestsNonGeneric.cs new file mode 100644 index 0000000000..5f026d1401 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackPolicyOptionsTestsNonGeneric.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Options; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class FallbackPolicyOptionsTestsNonGeneric +{ + private readonly FallbackPolicyOptions _testClass; + + public FallbackPolicyOptionsTestsNonGeneric() + { + _testClass = new FallbackPolicyOptions(); + } + + [Fact] + public void Constructor_ShouldInitialize() + { + Assert.NotNull(_testClass); + } + + [Fact] + public void ShouldHandleException_ValidValue_ShouldGetAndSet() + { + Predicate testValue = _ => true; + _testClass.ShouldHandleException = testValue; + Assert.Equal(testValue, _testClass.ShouldHandleException); + } + + [Fact] + public void ShouldHandleException_DefaultValue_ShouldReturnTrue() + { + var shouldHandle = _testClass.ShouldHandleException(new AggregateException()); + Assert.True(shouldHandle); + } + + [Fact] + public void ShouldHandleException_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.ShouldHandleException = null!); + } + + [Fact] + public void OnFallback_ValidValue_ShouldGetAndSet() + { + Func testValue = _ => Task.CompletedTask; + + _testClass.OnFallbackAsync = testValue; + Assert.Equal(testValue, _testClass.OnFallbackAsync); + } + + [Fact] + public void OnFallbackSet_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.OnFallbackAsync = null!); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackScenarioTaskArgumentsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackScenarioTaskArgumentsTests.cs new file mode 100644 index 0000000000..7c2085d1ff --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackScenarioTaskArgumentsTests.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Microsoft.Extensions.Resilience.Options; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class FallbackScenarioTaskArgumentsTests +{ + [Fact] + public void Constructor_NoParameters_ShouldInitialize() + { +#pragma warning disable SA1129 + var instance = new FallbackScenarioTaskArguments(); + + Assert.Equal(CancellationToken.None, instance.CancellationToken); + Assert.Null(instance.Context); + } + + [Fact] + public void Constructor_WithParameters_ShouldInitializeProperties() + { + var context = new Context(); + using var cts = new CancellationTokenSource(); + var ct = cts.Token; + + var instance = new FallbackScenarioTaskArguments(context, ct); + + Assert.Equal(ct, instance.CancellationToken); + Assert.Equal(context, instance.Context); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackTaskArgumentsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackTaskArgumentsTests.cs new file mode 100644 index 0000000000..7b14f2b1b7 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackTaskArgumentsTests.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.Extensions.Resilience.Options; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class FallbackTaskArgumentsTests +{ + [Fact] + public void Constructor_NoParameters_ShouldInitialize() + { +#pragma warning disable SA1129 + var instance = new FallbackTaskArguments(); + +#pragma warning disable xUnit2002 + Assert.NotNull(instance); + } + + [Fact] + public void Constructor_WithParameters_ShouldInitializeProperties() + { + var expectedError = "Something went wrong"; + var delegateResult = new DelegateResult(new InvalidOperationException(expectedError)); + var context = new Context(); + using var cts = new CancellationTokenSource(); + var ct = cts.Token; + + var instance = new FallbackTaskArguments( + delegateResult, + context, + ct); + + Assert.NotNull(instance); + Assert.Equal(delegateResult, instance.Result); + Assert.Equal(ct, instance.CancellationToken); + Assert.Equal(context, instance.Context); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackTaskArgumentsTestsNonGeneric.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackTaskArgumentsTestsNonGeneric.cs new file mode 100644 index 0000000000..af5a171eba --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/FallbackTaskArgumentsTestsNonGeneric.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.Extensions.Resilience.Options; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class FallbackTaskArgumentsTestsNonGeneric +{ + [Fact] + public void Constructor_NoParameters_ShouldInitialize() + { +#pragma warning disable SA1129 + var instance = new FallbackTaskArguments(); + +#pragma warning disable xUnit2002 + Assert.NotNull(instance); + } + + [Fact] + public void Constructor_WithParameters_ShouldInitializeProperties() + { + var expectedError = "Something went wrong"; + var exception = new InvalidOperationException(expectedError); + var context = new Context(); + using var cts = new CancellationTokenSource(); + var ct = cts.Token; + + var instance = new FallbackTaskArguments( + exception, + context, + ct); + + Assert.NotNull(instance); + Assert.Equal(exception, instance.Exception); + Assert.Equal(ct, instance.CancellationToken); + Assert.Equal(context, instance.Context); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingDelayArgumentsTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingDelayArgumentsTest.cs new file mode 100644 index 0000000000..76e4477d83 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingDelayArgumentsTest.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Microsoft.Extensions.Resilience.Options; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class HedgingDelayArgumentsTest +{ + [Fact] + public void Constructor_NoParameters_ShouldInitialize() + { +#pragma warning disable SA1129 // Do not use default value type constructor + var instance = new HedgingDelayArguments(); +#pragma warning restore SA1129 // Do not use default value type constructor + + Assert.Null(instance.Context); + Assert.Equal(0, instance.AttemptNumber); + } + + [Fact] + public void Constructor_WithParameters_ShouldInitializeProperties() + { + var context = new Context(); +#pragma warning disable SA1129 // Do not use default value type constructor + var cancellationToken = new CancellationToken(); +#pragma warning restore SA1129 // Do not use default value type constructor + var instance = new HedgingDelayArguments( + context, + 2, + cancellationToken); + + Assert.Equal(context, instance.Context); + Assert.Equal(2, instance.AttemptNumber); + Assert.Equal(cancellationToken, instance.CancellationToken); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingPolicyOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingPolicyOptionsTests.cs new file mode 100644 index 0000000000..a84b1e25df --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingPolicyOptionsTests.cs @@ -0,0 +1,162 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Options; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class HedgingPolicyOptionsTests +{ + private readonly HedgingPolicyOptions _testClass; + + public HedgingPolicyOptionsTests() + { + _testClass = new HedgingPolicyOptions(); + } + + [Fact] + public void Constructor_ShouldInitialize() + { + var instance = new HedgingPolicyOptions(); + Assert.NotNull(instance); + Assert.NotNull(_testClass.OnHedgingAsync); + Assert.NotNull(_testClass.ShouldHandleResultAsError); + Assert.NotNull(_testClass.ShouldHandleException); + Assert.Null(_testClass.HedgingDelayGenerator); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(5)] + [InlineData(10)] + [InlineData(15)] + public void HedgingDelayGenerator_Can_Set(int seconds) + { + var o = new HedgingPolicyOptions(); + + var e = Record.Exception(() => o.HedgingDelayGenerator = (_) => TimeSpan.FromSeconds(seconds)); + + Assert.Null(e); + Assert.NotNull(o.HedgingDelayGenerator); + Assert.Equal(o.HedgingDelayGenerator!(default), TimeSpan.FromSeconds(seconds)); + } + + [Fact] + public void InfiniteHedgingDelay_CorrectValue() + { + Assert.Equal(TimeSpan.FromMilliseconds(-1), HedgingPolicyOptions.InfiniteHedgingDelay); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(1)] + public void HedgingDelayProperty_ValidValue_ShouldGetAndSet(int value) + { + var testValue = TimeSpan.FromMilliseconds(value); + _testClass.HedgingDelay = testValue; + + Assert.Equal(testValue, _testClass.HedgingDelay); + OptionsUtilities.ValidateOptions(_testClass); + } + + [Theory] + [InlineData(-2)] + [InlineData(-3)] + public void HedgingDelayProperty_InvalidValue_ShouldThrow(int testValue) + { + _testClass.HedgingDelay = TimeSpan.FromSeconds(testValue); + Assert.Throws(() => + OptionsUtilities.ValidateOptions(_testClass)); + } + + [Fact] + public void MaxHedgedAttemptsProperty_ValidValue_ShouldGetAndSet() + { + var testValue = 2; + _testClass.MaxHedgedAttempts = testValue; + + Assert.Equal(testValue, _testClass.MaxHedgedAttempts); + OptionsUtilities.ValidateOptions(_testClass); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(1)] + public void MaxHedgedAttemptsProperty_InvalidValue_ShouldThrow(int testValue) + { + _testClass.MaxHedgedAttempts = testValue; + Assert.Throws(() => OptionsUtilities.ValidateOptions(_testClass)); + } + + [Fact] + public void DefaultInstance_ShouldInitializeWithDefault() + { + var defaultOptions = Constants.HedgingPolicy.DefaultOptions(); + Assert.NotNull(defaultOptions); + Assert.Equal(TimeSpan.FromSeconds(2), defaultOptions.HedgingDelay); + } + + [Fact] + public void ShouldHandleResultAsError_ValidValue_ShouldGetAndSet() + { + Predicate testValue = _ => true; + _testClass.ShouldHandleResultAsError = testValue; + Assert.Equal(testValue, _testClass.ShouldHandleResultAsError); + } + + [Fact] + public void ShouldHandleResultAsError_DefaultValue_ShouldReturnFalse() + { + var shouldHandle = _testClass.ShouldHandleResultAsError(string.Empty); + Assert.False(shouldHandle); + } + + [Fact] + public void ShouldHandleResultAsError_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.ShouldHandleResultAsError = null!); + } + + [Fact] + public void ShouldHandleException_ValidValue_ShouldGetAndSet() + { + Predicate testValue = _ => true; + _testClass.ShouldHandleException = testValue; + Assert.Equal(testValue, _testClass.ShouldHandleException); + } + + [Fact] + public void ShouldHandleException_DefaultValue_ShouldReturnTrue() + { + var shouldHandle = _testClass.ShouldHandleException(new AggregateException()); + Assert.True(shouldHandle); + } + + [Fact] + public void ShouldHandleException_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.ShouldHandleException = null!); + } + + [Fact] + public void OnHedging_ValidValue_ShouldGetAndSet() + { + Func, Task> testValue = _ => Task.CompletedTask; + + _testClass.OnHedgingAsync = testValue; + Assert.Equal(testValue, _testClass.OnHedgingAsync); + } + + [Fact] + public void OnHedgingSet_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.OnHedgingAsync = null!); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingPolicyOptionsTestsNonGeneric.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingPolicyOptionsTestsNonGeneric.cs new file mode 100644 index 0000000000..bb08c4cffa --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingPolicyOptionsTestsNonGeneric.cs @@ -0,0 +1,124 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Options; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class HedgingPolicyOptionsTestsNonGeneric +{ + private readonly HedgingPolicyOptions _testClass; + + public HedgingPolicyOptionsTestsNonGeneric() + { + _testClass = new HedgingPolicyOptions(); + } + + [Fact] + public void Constructor_ShouldInitialize() + { + var instance = new HedgingPolicyOptions(); + Assert.NotNull(instance); + Assert.NotNull(_testClass.OnHedgingAsync); + Assert.NotNull(_testClass.ShouldHandleException); + Assert.Null(_testClass.HedgingDelayGenerator); + } + + [Fact] + public void InfiniteHedgingDelay_CorrectValue() + { + Assert.Equal(TimeSpan.FromMilliseconds(-1), HedgingPolicyOptions.InfiniteHedgingDelay); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(1)] + public void HedgingDelayProperty_ValidValue_ShouldGetAndSet(int value) + { + var testValue = TimeSpan.FromMilliseconds(value); + _testClass.HedgingDelay = testValue; + + Assert.Equal(testValue, _testClass.HedgingDelay); + OptionsUtilities.ValidateOptions(_testClass); + } + + [Theory] + [InlineData(-2)] + [InlineData(-3)] + public void HedgingDelayProperty_InvalidValue_ShouldThrow(int testValue) + { + _testClass.HedgingDelay = TimeSpan.FromSeconds(testValue); + Assert.Throws(() => + OptionsUtilities.ValidateOptions(_testClass)); + } + + [Fact] + public void MaxHedgedAttemptsProperty_ValidValue_ShouldGetAndSet() + { + var testValue = 2; + _testClass.MaxHedgedAttempts = testValue; + + Assert.Equal(testValue, _testClass.MaxHedgedAttempts); + OptionsUtilities.ValidateOptions(_testClass); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(1)] + public void MaxHedgedAttemptsProperty_InvalidValue_ShouldThrow(int testValue) + { + _testClass.MaxHedgedAttempts = testValue; + Assert.Throws(() => + OptionsUtilities.ValidateOptions(_testClass)); + } + + [Fact] + public void DefaultInstance_ShouldInitializeWithDefault() + { + var defaultOptions = Constants.HedgingPolicyNonGeneric.DefaultOptions(); + Assert.NotNull(defaultOptions); + Assert.Equal(TimeSpan.FromSeconds(2), defaultOptions.HedgingDelay); + } + + [Fact] + public void ShouldHandleException_ValidValue_ShouldGetAndSet() + { + Predicate testValue = _ => true; + _testClass.ShouldHandleException = testValue; + Assert.Equal(testValue, _testClass.ShouldHandleException); + } + + [Fact] + public void ShouldHandleException_DefaultValue_ShouldReturnTrue() + { + var shouldHandle = _testClass.ShouldHandleException(new AggregateException()); + Assert.True(shouldHandle); + } + + [Fact] + public void ShouldHandleException_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.ShouldHandleException = null!); + } + + [Fact] + public void OnHedging_ValidValue_ShouldGetAndSet() + { + Func testValue = _ => Task.CompletedTask; + + _testClass.OnHedgingAsync = testValue; + Assert.Equal(testValue, _testClass.OnHedgingAsync); + } + + [Fact] + public void OnHedgingSet_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.OnHedgingAsync = null!); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingTaskArgumentsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingTaskArgumentsTests.cs new file mode 100644 index 0000000000..f773cab312 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingTaskArgumentsTests.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.Extensions.Resilience.Options; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class HedgingTaskArgumentsTests +{ + [Fact] + public void Constructor_NoParameters_ShouldInitialize() + { +#pragma warning disable SA1129 + var instance = new HedgingTaskArguments(); + +#pragma warning disable xUnit2002 + Assert.NotNull(instance); + } + + [Fact] + public void Constructor_WithParameters_ShouldInitializeProperties() + { + var expectedError = "Something went wrong"; + var delegateResult = new DelegateResult(new InvalidOperationException(expectedError)); + var expectedAttempts = 2; + var context = new Context(); + var token = new CancellationToken(); + + var instance = new HedgingTaskArguments( + delegateResult, + context, + expectedAttempts, + token); + + Assert.NotNull(instance); + Assert.Equal(delegateResult, instance.Result); + Assert.Equal(expectedAttempts, instance.AttemptNumber); + Assert.Equal(context, instance.Context); + Assert.Equal(token, instance.CancellationToken); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingTaskArgumentsTestsNonGeneric.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingTaskArgumentsTestsNonGeneric.cs new file mode 100644 index 0000000000..0aa9f7537f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/HedgingTaskArgumentsTestsNonGeneric.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.Extensions.Resilience.Options; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class HedgingTaskArgumentsTestsNonGeneric +{ + [Fact] + public void Constructor_NoParameters_ShouldInitialize() + { + var instance = default(HedgingTaskArguments); + + Assert.Null(instance.Exception); + Assert.Equal(0, instance.AttemptNumber); + Assert.Null(instance.Context); + } + + [Fact] + public void Constructor_WithParameters_ShouldInitializeProperties() + { + var expectedError = "Something went wrong"; + var exception = new InvalidOperationException(expectedError); + var expectedAttempts = 2; + var context = new Context(); + var token = CancellationToken.None; + + var instance = new HedgingTaskArguments( + exception, + context, + expectedAttempts, + token); + + Assert.Equal(exception, instance.Exception); + Assert.Equal(expectedAttempts, instance.AttemptNumber); + Assert.Equal(context, instance.Context); + Assert.Equal(token, instance.CancellationToken); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/OptionsUtilities.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/OptionsUtilities.cs new file mode 100644 index 0000000000..501f50fafb --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/OptionsUtilities.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public static class OptionsUtilities +{ + public static void ValidateOptions(object options) + { + var context = new ValidationContext(options); + Validator.ValidateObject(options, context, true); + } + + public static bool EqualOptions(T options1, T options2) + { + if (options1 is null && options2 is null) + { + return true; + } + + if (options1 is null || options2 is null) + { + return false; + } + + var propertiesValuesByName1 = options1.GetPropertiesValuesByName(); + var propertiesValuesByName2 = options2.GetPropertiesValuesByName(); + + foreach (var propertyDefinition1 in propertiesValuesByName1) + { + var propertyName = propertyDefinition1.Key; + var propertyValue1 = propertyDefinition1.Value; + + if (!propertiesValuesByName2.TryGetValue(propertyName, out var propertyValue2) || + !Equals(propertyValue1, propertyValue2)) + { + return false; + } + } + + return true; + } + + private static IDictionary GetPropertiesValuesByName(this T options) + { + return options! + .GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .GroupBy(property => property.Name) + .ToDictionary( + propertyGroup => propertyGroup.Key, + propertyGroup => propertyGroup.Last().GetValue(options)!); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/ResetActionArgumentsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/ResetActionArgumentsTests.cs new file mode 100644 index 0000000000..6547caa053 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/ResetActionArgumentsTests.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Microsoft.Extensions.Resilience.Options; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class ResetActionArgumentsTests +{ + [Fact] + public void Constructor_NoParameters_ShouldInitialize() + { +#pragma warning disable SA1129 + var instance = new ResetActionArguments(); + + Assert.Null(instance.Context); + Assert.Equal(CancellationToken.None, instance.CancellationToken); + } + + [Fact] + public void Constructor_WithParameters_ShouldInitializeProperties() + { + var context = new Context(); + var instance = new ResetActionArguments(context, CancellationToken.None); + + Assert.Equal(CancellationToken.None, instance.CancellationToken); + Assert.Equal(context, instance.Context); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryActionArgumentsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryActionArgumentsTests.cs new file mode 100644 index 0000000000..e376bc01b7 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryActionArgumentsTests.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.Extensions.Resilience.Options; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class RetryActionArgumentsTests +{ + [Fact] + public void Constructor_NoParameters_ShouldInitialize() + { +#pragma warning disable SA1129 + var instance = new RetryActionArguments(); + +#pragma warning disable xUnit2002 + Assert.NotNull(instance); + } + + [Fact] + public void Constructor_With5Parameters_ShouldInitializeProperties() + { + var expectedError = "Something went wrong"; + var delegateResult = new DelegateResult(new InvalidOperationException(expectedError)); + var expectedWaitingInterval = TimeSpan.FromSeconds(2); + var expectedAttempts = 2; + var context = new Context(); + var cancellationToken = new CancellationToken(); + + var instance = new RetryActionArguments( + delegateResult, + context, + expectedWaitingInterval, + expectedAttempts, + cancellationToken); + + Assert.NotNull(instance); + Assert.Equal(delegateResult, instance.Result); + Assert.Equal(expectedWaitingInterval, instance.WaitingTimeInterval); + Assert.Equal(expectedAttempts, instance.AttemptNumber); + Assert.Equal(context, instance.Context); + Assert.Equal(cancellationToken, instance.CancellationToken); + Assert.Equal(context, instance.Context); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryActionArgumentsTestsNonGeneric.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryActionArgumentsTestsNonGeneric.cs new file mode 100644 index 0000000000..f12a30ffee --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryActionArgumentsTestsNonGeneric.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.Extensions.Resilience.Options; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class RetryActionArgumentsTestsNonGeneric +{ + [Fact] + public void Constructor_NoParameters_ShouldInitialize() + { +#pragma warning disable SA1129 + var instance = new RetryActionArguments(); + +#pragma warning disable xUnit2002 + Assert.NotNull(instance); + } + + [Fact] + public void Constructor_With5Parameters_ShouldInitializeProperties() + { + var expectedError = "Something went wrong"; + var exception = new InvalidOperationException(expectedError); + var expectedWaitingInterval = TimeSpan.FromSeconds(2); + var expectedAttempts = 2; + var context = new Context(); + var cancellationToken = new CancellationToken(); + + var instance = new RetryActionArguments( + exception, + context, + expectedWaitingInterval, + expectedAttempts, + cancellationToken); + + Assert.NotNull(instance); + Assert.Equal(exception, instance.Exception); + Assert.Equal(expectedWaitingInterval, instance.WaitingTimeInterval); + Assert.Equal(expectedAttempts, instance.AttemptNumber); + Assert.Equal(context, instance.Context); + Assert.Equal(cancellationToken, instance.CancellationToken); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryDelayArgumentsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryDelayArgumentsTests.cs new file mode 100644 index 0000000000..76254844a5 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryDelayArgumentsTests.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Microsoft.Extensions.Resilience.Options; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class RetryDelayArgumentsTests +{ + [Fact] + public void Constructor_NoParameters_ShouldInitialize() + { +#pragma warning disable SA1129 + var instance = new RetryDelayArguments(); + + Assert.Null(instance.Result); + Assert.Null(instance.Context); + Assert.Equal(CancellationToken.None, instance.CancellationToken); + } + + [Fact] + public void Constructor_WithParameters_ShouldInitializeProperties() + { + var expectedError = "Something went wrong"; + var delegateResult = new DelegateResult(new InvalidOperationException(expectedError)); + var context = new Context(); + var cancellationToken = new CancellationToken(); + + var instance = new RetryDelayArguments( + delegateResult, + context, + cancellationToken); + + Assert.Equal(delegateResult, instance.Result); + Assert.Equal(context, instance.Context); + Assert.Equal(cancellationToken, instance.CancellationToken); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryPolicyOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryPolicyOptionsTests.cs new file mode 100644 index 0000000000..9ad03ecebd --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryPolicyOptionsTests.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Options; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class RetryPolicyOptionsTests +{ + private readonly RetryPolicyOptions _testClass; + + public RetryPolicyOptionsTests() + { + _testClass = new RetryPolicyOptions(); + } + + [Fact] + public void Default_RetryPolicyOptions_Returns_Completed_Task() + { + var result = RetryPolicyOptions.DefaultOnRetryAsync(default); + + Assert.Equal(Task.CompletedTask, result); + } + + [Fact] + public void Constructor_ShouldInitialize() + { + var instance = new RetryPolicyOptions(); + Assert.NotNull(instance); + } + + [Fact] + public void RetryCount_ShouldGetAndSet() + { + const int TestValue = 100; + _testClass.RetryCount = TestValue; + Assert.Equal(TestValue, _testClass.RetryCount); + } + + [Fact] + public void BackoffType_ShouldGetAndSet() + { + const BackoffType TestValue = BackoffType.ExponentialWithJitter; + _testClass.BackoffType = TestValue; + Assert.Equal(TestValue, _testClass.BackoffType); + } + + [Fact] + public void BackoffBasedDelay_ShouldGetAndSet() + { + var testValue = new TimeSpan(1234); + _testClass.BaseDelay = testValue; + Assert.Equal(testValue, _testClass.BaseDelay); + } + + [Fact] + public void DelayGenerators_ShouldGetNullDefault() + { + Assert.Null(_testClass.RetryDelayGenerator); + } + + [Fact] + public void ShouldHandleResultAsError_ValidValue_ShouldGetAndSet() + { + Predicate testValue = _ => true; + _testClass.ShouldHandleResultAsError = testValue; + Assert.Equal(testValue, _testClass.ShouldHandleResultAsError); + } + + [Fact] + public void ShouldHandleResultAsError_DefaultValue_ShouldReturnFalse() + { + var shouldHandle = _testClass.ShouldHandleResultAsError(string.Empty); + Assert.False(shouldHandle); + } + + [Fact] + public void ShouldHandleResultAsError_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.ShouldHandleResultAsError = null!); + } + + [Fact] + public void ShouldHandleException_ValidValue_ShouldGetAndSet() + { + Predicate testValue = _ => true; + _testClass.ShouldHandleException = testValue; + Assert.Equal(testValue, _testClass.ShouldHandleException); + } + + [Fact] + public void ShouldHandleException_DefaultValue_ShouldReturnTrue() + { + var shouldHandle = _testClass.ShouldHandleException(new AggregateException()); + Assert.True(shouldHandle); + } + + [Fact] + public void ShouldHandleException_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.ShouldHandleException = null!); + } + + [Fact] + public void OnRetryAsync_ValidValue_ShouldGetAndSet() + { + Func, Task> testValue = _ => Task.CompletedTask; + + _testClass.OnRetryAsync = testValue; + Assert.Equal(testValue, _testClass.OnRetryAsync); + } + + [Fact] + public void OnRetryAsyncSet_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.OnRetryAsync = null!); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryPolicyOptionsTestsNonGeneric.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryPolicyOptionsTestsNonGeneric.cs new file mode 100644 index 0000000000..70b7a9d64c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/RetryPolicyOptionsTestsNonGeneric.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Options; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class RetryPolicyOptionsTestsNonGeneric +{ + private readonly RetryPolicyOptions _testClass; + + public RetryPolicyOptionsTestsNonGeneric() + { + _testClass = new RetryPolicyOptions(); + } + + [Fact] + public void Constructor_ShouldInitialize() + { + var instance = new RetryPolicyOptions(); + Assert.NotNull(instance); + } + + [Fact] + public void RetryCount_ShouldGetAndSet() + { + const int TestValue = 1_052_822_497; + _testClass.RetryCount = TestValue; + Assert.Equal(TestValue, _testClass.RetryCount); + } + + [Fact] + public void BackoffType_ShouldGetAndSet() + { + const BackoffType TestValue = BackoffType.ExponentialWithJitter; + _testClass.BackoffType = TestValue; + Assert.Equal(TestValue, _testClass.BackoffType); + } + + [Fact] + public void BackoffBasedDelay_ShouldGetAndSet() + { + var testValue = new TimeSpan(1234); + _testClass.BaseDelay = testValue; + Assert.Equal(testValue, _testClass.BaseDelay); + } + + [Fact] + public void ShouldHandleException_ValidValue_ShouldGetAndSet() + { + Predicate testValue = _ => true; + _testClass.ShouldHandleException = testValue; + Assert.Equal(testValue, _testClass.ShouldHandleException); + } + + [Fact] + public void ShouldHandleException_DefaultValue_ShouldReturnTrue() + { + var shouldHandle = _testClass.ShouldHandleException(new AggregateException()); + Assert.True(shouldHandle); + } + + [Fact] + public void ShouldHandleException_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.ShouldHandleException = null!); + } + + [Fact] + public void OnRetry_ValidValue_ShouldGetAndSet() + { + Func testValue = _ => Task.CompletedTask; + + _testClass.OnRetryAsync = testValue; + Assert.Equal(testValue, _testClass.OnRetryAsync); + } + + [Fact] + public void OnRetrySet_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.OnRetryAsync = null!); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/TimeoutPolicyOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/TimeoutPolicyOptionsTests.cs new file mode 100644 index 0000000000..70e9a26ce9 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/TimeoutPolicyOptionsTests.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Options; +using Polly.Timeout; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class TimeoutPolicyOptionsTests +{ + private readonly TimeoutPolicyOptions _testClass; + + public TimeoutPolicyOptionsTests() + { + _testClass = new TimeoutPolicyOptions(); + } + + [Fact] + public void Constructor_ShouldInitialize() + { + var instance = new TimeoutPolicyOptions(); + Assert.NotNull(instance); + } + + [Fact] + public void TimeoutIntervalProperty_ValidValue_ShouldGetAndSet() + { + var testValue = new TimeSpan(234); + _testClass.TimeoutInterval = testValue; + Assert.Equal(testValue, _testClass.TimeoutInterval); + OptionsUtilities.ValidateOptions(_testClass); + } + + [Fact] + public void TimeoutStrategyProperty_ValidValue_ShouldGetAndSet() + { + _testClass.TimeoutStrategy = TimeoutStrategy.Pessimistic; + Assert.Equal(TimeoutStrategy.Pessimistic, _testClass.TimeoutStrategy); + OptionsUtilities.ValidateOptions(_testClass); + } + + [Fact] + public void TimeoutIntervalProperty_InvalidValue_ShouldThrow() + { + var testValue = new TimeSpan(-2); + _testClass.TimeoutInterval = testValue; + Assert.Equal(testValue, _testClass.TimeoutInterval); + Assert.Throws(() => + OptionsUtilities.ValidateOptions(_testClass)); + } + + [Fact] + public void DefaultInstance_ShouldInitializeWithDefault() + { + var defaultConfiguration = Constants.TimeoutPolicy.DefaultOptions; + + Assert.NotNull(defaultConfiguration); + Assert.Equal(Constants.TimeoutPolicy.DefaultOptions.TimeoutInterval, defaultConfiguration.TimeoutInterval); + Assert.NotNull(defaultConfiguration.OnTimedOutAsync); + } + + [Fact] + public void OnTimedOutAsyncSet_NullValue_ShouldThrow() + { + Assert.Throws(() => _testClass.OnTimedOutAsync = null!); + } + + [Fact] + public void OnTimedOutAsync_ValidValue_ShouldGetAndSet() + { + Func testValue = _ => Task.CompletedTask; + + _testClass.OnTimedOutAsync = testValue; + Assert.Equal(testValue, _testClass.OnTimedOutAsync); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/TimeoutTaskArgumentsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/TimeoutTaskArgumentsTests.cs new file mode 100644 index 0000000000..42db9de58b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/Options/TimeoutTaskArgumentsTests.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Microsoft.Extensions.Resilience.Options; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test.Options; + +public class TimeoutTaskArgumentsTests +{ + [Fact] + public void Constructor_NoParameters_ShouldInitialize() + { +#pragma warning disable SA1129 + var instance = new TimeoutTaskArguments(); + + Assert.Null(instance.Context); + Assert.Equal(CancellationToken.None, instance.CancellationToken); + } + + [Fact] + public void Constructor_WithParameters_ShouldInitializeProperties() + { + var context = new Context(); + var instance = new TimeoutTaskArguments(context, CancellationToken.None); + + Assert.Equal(CancellationToken.None, instance.CancellationToken); + Assert.Equal(context, instance.Context); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/PollyServiceCollectionExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/PollyServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000000..b9af3eaeb0 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/PollyServiceCollectionExtensionsTests.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Telemetry.Metering; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test; + +#pragma warning disable CS0618 +public class PollyServiceCollectionExtensionsTests +{ + private readonly IServiceCollection _serviceCollection; + + public PollyServiceCollectionExtensionsTests() + { + _serviceCollection = new ServiceCollection(); + _serviceCollection.RegisterMetering().AddLogging().AddSingleton(Mock.Of()); + } + + [Fact] + public void ConfigureFailureResultContext() + { + var expectedDimensionValue = "lalala"; + _ = _serviceCollection + .ConfigureFailureResultContext((_) => FailureResultContext.Create(failureReason: expectedDimensionValue)); + + using var serviceProvider = _serviceCollection.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>>()?.Value; + Assert.NotNull(options); + + var context = options!.GetContextFromResult(string.Empty); + + Assert.Equal("unknown", context.FailureSource); + Assert.Equal(expectedDimensionValue, context.FailureReason); + Assert.Equal("unknown", context.AdditionalInformation); + } + + [Fact] + public void ConfigureFailureResultContext_ArgumentValidation() + { + Assert.Throws(() => _serviceCollection.ConfigureFailureResultContext(null!)); + Assert.Throws(() => PollyServiceCollectionExtensions.ConfigureFailureResultContext(null!, args => default)); + } + + [Fact] + public void ShouldThrowArgumentExceptionWhenFailureSourceIsNullOrEmpty() + { + string s = "lalala"; + + ShouldThrowArgumentException((_) => FailureResultContext.Create(null!, s, s)); + ShouldThrowArgumentException((_) => FailureResultContext.Create(s, null!, s)); + ShouldThrowArgumentException((_) => FailureResultContext.Create(s, s, null!)); + + ShouldThrowArgumentException((_) => FailureResultContext.Create("", s, s), false); + ShouldThrowArgumentException((_) => FailureResultContext.Create(s, "", s), false); + ShouldThrowArgumentException((_) => FailureResultContext.Create(s, s, ""), false); + } + + private void ShouldThrowArgumentException(Func testCode, bool isNull = true) + { + _ = _serviceCollection + .ConfigureFailureResultContext(testCode); + + using var serviceProvider = _serviceCollection.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>>()?.Value; + Assert.NotNull(options); + + if (isNull) + { + Assert.Throws(() => options!.GetContextFromResult(string.Empty)); + } + else + { + Assert.Throws(() => options!.GetContextFromResult(string.Empty)); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/ResilienceDimensionsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/ResilienceDimensionsTests.cs new file mode 100644 index 0000000000..49264e9874 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/ResilienceDimensionsTests.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test; + +public class ResilienceDimensionsTests +{ + [Fact] + public void DimensionNames_Ok() + { + var names = ResilienceDimensions.DimensionNames; + + Assert.Equal(10, names.Count); + + Assert.Equal(ResilienceDimensions.PipelineName, names[0]); + Assert.Equal(ResilienceDimensions.PipelineKey, names[1]); + Assert.Equal(ResilienceDimensions.ResultType, names[2]); + Assert.Equal(ResilienceDimensions.PolicyName, names[3]); + Assert.Equal(ResilienceDimensions.EventName, names[4]); + Assert.Equal(ResilienceDimensions.FailureSource, names[5]); + Assert.Equal(ResilienceDimensions.FailureReason, names[6]); + Assert.Equal(ResilienceDimensions.FailureSummary, names[7]); + Assert.Equal(ResilienceDimensions.DependencyName, names[8]); + Assert.Equal(ResilienceDimensions.RequestName, names[9]); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/ResiliencePollyFakeClockTestsCollection.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/ResiliencePollyFakeClockTestsCollection.cs new file mode 100644 index 0000000000..3d20e3ec2b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/ResiliencePollyFakeClockTestsCollection.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test; + +[CollectionDefinition(nameof(ResiliencePollyFakeClockTestsCollection), DisableParallelization = true)] +public class ResiliencePollyFakeClockTestsCollection +{ +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/RetryOptionsExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/RetryOptionsExtensionsTests.cs new file mode 100644 index 0000000000..040e8ab120 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Polly/RetryOptionsExtensionsTests.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Resilience.Options; +using Polly.Contrib.WaitAndRetry; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Polly.Test; + +public class RetryOptionsExtensionsTests +{ + private const int DefaultRetryCount = 2; + private static readonly TimeSpan _defaultBackoffDelay = TimeSpan.FromSeconds(1); + + private readonly RetryPolicyOptions _testClass; + + public RetryOptionsExtensionsTests() + { + _testClass = new RetryPolicyOptions + { + BaseDelay = _defaultBackoffDelay, + RetryCount = DefaultRetryCount + }; + } + + [Fact] + public void GetDelays_ExponentialWithJitter_ShouldReturnJitterBackoff() + { + _testClass.BackoffType = BackoffType.ExponentialWithJitter; + var delays = _testClass.GetDelays().ToArray(); + + Assert.NotEmpty(delays); + Assert.Equal(DefaultRetryCount, delays.Length); + foreach (var entry in delays) + { + Assert.True(entry.TotalSeconds > 0); + } + } + + [InlineData(BackoffType.ExponentialWithJitter, false)] + [InlineData(BackoffType.Constant, true)] + [InlineData(BackoffType.Linear, true)] + [Theory] + public void GetDelays_MultipleEnumeration_EnsureExpectedDelays(BackoffType type, bool shouldBeEqual) + { + _testClass.BackoffType = type; + var source = _testClass.GetDelays(); + var delays1 = source.ToArray(); + var delays2 = source.ToArray(); + + Assert.NotEmpty(delays1); + Assert.Equal(delays1.Length, delays2.Length); + Assert.Equal(shouldBeEqual, delays1.SequenceEqual(delays2)); + Assert.Equal(shouldBeEqual, source is List); + } + + [Fact] + public void GetDelays_Linear_ShouldReturnLinearBackoff() + { + _testClass.BackoffType = BackoffType.Linear; + var expectedDelay = Backoff.LinearBackoff(_defaultBackoffDelay, DefaultRetryCount); + var delay = _testClass.GetDelays(); + Assert.Equal(expectedDelay, delay); + } + + [Fact] + public void GetDelays_Constant_ShouldReturnConstantBackoff() + { + _testClass.BackoffType = BackoffType.Constant; + var expectedDelay = Backoff.ConstantBackoff(_defaultBackoffDelay, DefaultRetryCount); + var delay = _testClass.GetDelays(); + Assert.Equal(expectedDelay, delay); + } + + [Fact] + public void GetDelays_Other_ShouldThrow() + { + _testClass.BackoffType = (BackoffType)5; + Assert.Throws(() => _testClass.GetDelays()); + } + + [Fact] + public void GetDelays_Single_ShouldReturnSingleDelay() + { + _testClass.BackoffType = BackoffType.Constant; + var newDelay = TimeSpan.FromSeconds(1); + var expectedDelay = Backoff.ConstantBackoff(newDelay, 1).Single(); + var delay = _testClass.GetDelays().First(); + Assert.Equal(expectedDelay, delay); + } + + [Fact] + public void GetDelays_NullOptions_Throws() + { + Assert.Throws(() => RetryPolicyOptionsExtensions.GetDelays(null!)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Helpers/ResilienceTestHelper.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Helpers/ResilienceTestHelper.cs new file mode 100644 index 0000000000..9bbc64adcf --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Helpers/ResilienceTestHelper.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Telemetry.Metering; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Test.Helpers; + +[Flags] +public enum MethodArgs +{ + None = 0, + + ConfigureMethod = 1 << 0, + + Configuration = 1 << 1, +} + +public abstract class ResilienceTestHelper +{ + protected const string PolicyName = "some-policy-name"; + protected const string DefaultPipelineName = "pipeline-name"; + protected const string DefaultPipelineKey = "pipeline-key"; + protected static readonly IConfigurationSection EmptyConfiguration = new ConfigurationBuilder().Build().GetSection(string.Empty); + + protected IServiceCollection Services { get; } = new ServiceCollection().AddLogging().RegisterMetering(); + + protected static IConfigurationSection CreateEmptyConfiguration() + { + return new ConfigurationBuilder().Build().GetSection(string.Empty); + } + + protected static IConfigurationSection CreateConfiguration(string key, string value) + { + return new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { "dummySection:" + key, value } + }) + .Build() + .GetSection("dummySection"); + } + + protected static bool HasEmptyArgs(MethodArgs args) => !args.HasFlag(MethodArgs.Configuration) && !args.HasFlag(MethodArgs.ConfigureMethod); + + protected static void AssertOptions(T options, Func accessor, TValue? expectedValue, bool useOptions) + where T : class, new() + { + if (!useOptions) + { + Assert.Equal(accessor(new T()), accessor(options)); + } + else + { + Assert.Equal(expectedValue, accessor(options)); + } + } + + public static IEnumerable AllCombinations() => GetAllCombinations().Select(v => new object[] { v }); + + public static IEnumerable ConfigureMethodCombinations() => GetAllCombinations().Where(v => v.HasFlag(MethodArgs.ConfigureMethod)).Select(v => new object[] { v }); + + public static IEnumerable ConfigurationCombinations() => GetAllCombinations().Where(v => v.HasFlag(MethodArgs.Configuration)).Select(v => new object[] { v }); + + public static IEnumerable GetAllCombinations() + { + yield return MethodArgs.None; + yield return MethodArgs.ConfigureMethod; + yield return MethodArgs.Configuration; + yield return MethodArgs.Configuration | MethodArgs.ConfigureMethod; + } + + protected IAsyncPolicy CreatePipeline(string name = DefaultPipelineName) + => Services.BuildServiceProvider().GetRequiredService().CreatePipeline(name, DefaultPipelineKey); + + protected IAsyncPolicy CreatePipeline(string name = DefaultPipelineName) + => Services.BuildServiceProvider().GetRequiredService().CreatePipeline(name, DefaultPipelineKey); + + internal static string GetOptionsName(SupportedPolicies policy, string pipelineName, string policyName) => OptionsNameHelper.GetPolicyOptionsName(policy, pipelineName, policyName); +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/AsyncPolicyPipelineTResultTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/AsyncPolicyPipelineTResultTests.cs new file mode 100644 index 0000000000..b0be64470f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/AsyncPolicyPipelineTResultTests.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Internal; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Internal.Test; + +public class AsyncPolicyPipelineTResultTests +{ + public static IEnumerable GetRange() => Enumerable.Range(1, 10).Select(i => new object[] { i }); + + private class TestNoOpPolicy : AsyncPolicy + { + public bool Visited { get; private set; } + + protected override async Task ImplementationAsync( + Func> action, + Context context, + CancellationToken cancellationToken, + bool continueOnCapturedContext) + { + ((List>)context["order"]).Add(this); + + Visited = true; + return await action(context, cancellationToken).ConfigureAwait(continueOnCapturedContext); + } + } + + [Fact] + public void Ctor_WhenEmptyArgument_ShouldThrow() + { + var policies = Array.Empty>(); + + Assert.Throws(() => new AsyncPolicyPipeline(policies)); + } + + [Theory] + [MemberData(nameof(GetRange))] + public async Task ExecuteAsync_ShouldVisitAllPolicies(int numberOfPolicies) + { + var policies = Enumerable.Range(0, numberOfPolicies) + .Select(_ => new TestNoOpPolicy()) + .Cast>() + .ToArray(); + var wrapperPolicy = new AsyncPolicyPipeline(policies); + var expected = "expected"; + var context = new Context(); + var order = new List>(); + context["order"] = order; + + var result = await wrapperPolicy.ExecuteAsync(_ => + { + ((List>)context["order"]).Add(null!); + return Task.FromResult(expected); + }, + context); + + Assert.Equal(expected, result); + Assert.All(policies, policy => Assert.True(((TestNoOpPolicy)policy).Visited)); + Assert.Null(order.Last()); + + for (int i = 0; i < policies.Length; i++) + { + var item = order[i]; + Assert.Equal(policies[i], order[i]); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/AsyncPolicyPipelineTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/AsyncPolicyPipelineTests.cs new file mode 100644 index 0000000000..ee5736cbe3 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/AsyncPolicyPipelineTests.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Resilience.Internal; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Internal.Test; + +public class AsyncPolicyPipelineTests +{ + public static IEnumerable GetRange() => Enumerable.Range(1, 10).Select(i => new object[] { i }); + + private class TestNoOpPolicy : AsyncPolicy + { + public bool Visited { get; private set; } + + protected override async Task ImplementationAsync( + Func> action, + Context context, + CancellationToken cancellationToken, + bool continueOnCapturedContext) + { + ((List)context["order"]).Add(this); + + Visited = true; + return await action(context, cancellationToken).ConfigureAwait(continueOnCapturedContext); + } + } + + [Fact] + public void Ctor_WhenEmptyArgument_ShouldThrow() + { + var policies = Array.Empty(); + + Assert.Throws(() => new AsyncPolicyPipeline(policies)); + } + + [Theory] + [MemberData(nameof(GetRange))] + public async Task ExecuteAsync_ShouldVisitAllPolicies(int numberOfPolicies) + { + var policies = Enumerable.Range(0, numberOfPolicies) + .Select(_ => new TestNoOpPolicy()) + .Cast() + .ToArray(); + var wrapperPolicy = new AsyncPolicyPipeline(policies); + var expectedString = "expected"; + var expectedInt = 10; + var expectedBool = true; + var context = new Context(); + var order = new List(); + context["order"] = order; + + var resultString = await wrapperPolicy.ExecuteAsync(_ => + { + ((List)context["order"]).Add(null!); + return Task.FromResult(expectedString); + }, + context); + + var resultInt = await wrapperPolicy.ExecuteAsync(_ => + { + ((List)context["order"]).Add(null!); + return Task.FromResult(expectedInt); + }, + context); + + var resultBool = await wrapperPolicy.ExecuteAsync(_ => + { + ((List)context["order"]).Add(null!); + return Task.FromResult(expectedBool); + }, + context); + + Assert.Equal(expectedString, resultString); + Assert.Equal(expectedInt, resultInt); + Assert.Equal(expectedBool, resultBool); + + Assert.All(policies, policy => Assert.True(((TestNoOpPolicy)policy).Visited)); + Assert.Null(order.Last()); + + for (int i = 0; i < policies.Length; i++) + { + var item = order[i]; + Assert.Equal(policies[i], order[i]); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/NoopChangeTokenTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/NoopChangeTokenTests.cs new file mode 100644 index 0000000000..8af604828b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/NoopChangeTokenTests.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Resilience.Internal; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Internal.Test; +public sealed class NoopChangeTokenTests +{ + private readonly NoopChangeToken _token = new(); + + [Fact] + public void Constructor_ShouldInitialize() + { + Assert.False(_token.HasChanged); + Assert.True(_token.ActiveChangeCallbacks); + + var dummyObject = "apples"; + var disposable = _token.RegisterChangeCallback(dummyObject => { }, dummyObject); + Assert.NotNull(disposable); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/OnChangeListenersHandlerTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/OnChangeListenersHandlerTests.cs new file mode 100644 index 0000000000..8d2fc68b2a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/OnChangeListenersHandlerTests.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Internal.Test; +public class OnChangeListenersHandlerTests +{ + [Fact] + public void TryCaptureOnChange_DistinctNamesAndSameType_ShouldAdd() + { + var name1 = "options1"; + var name2 = "options2"; + using var handler = GetHandler(); + + Assert.True(handler.TryCaptureOnChange(name1)); + Assert.True(handler.TryCaptureOnChange(name2)); + } + + [Fact] + public void TryCaptureOnChange_SameNameAndType_ShouldAddOnlyOnce() + { + var name1 = "options1"; + using var handler = GetHandler(); + + Assert.True(handler.TryCaptureOnChange(name1)); + Assert.False(handler.TryCaptureOnChange(name1)); + Assert.False(handler.TryCaptureOnChange(name1)); + } + + [Theory] + [InlineData("name", "name")] + [InlineData("name1", "name2")] + public void TryCaptureOnChange_DistinctTypes_ShouldAdd(string name1, string name2) + { + using var handler = GetHandler(); + Assert.True(handler.TryCaptureOnChange(name1)); + Assert.True(handler.TryCaptureOnChange(name2)); + } + + [Fact] + public void TryCaptureOnChange_WhenNullListenerReturned_ShouldNotAdd() + { + var name = "dummyPipeline"; + var disposeCalls = 0; + var listenerMock = new Mock(); + var optionsMonitorMock = new Mock>(MockBehavior.Strict); + var options = new TimeoutPolicyOptions(); + listenerMock.Setup(mock => mock.Dispose()).Callback(() => { disposeCalls++; }); + optionsMonitorMock + .Setup(mock => mock.Get(name)) + .Returns(options); + optionsMonitorMock + .SetupSequence(mock => mock.OnChange(It.IsAny>())) + .Returns((IDisposable)null!) + .Returns(listenerMock.Object); + + using var handler = GetHandler(services => + services.AddSingleton(optionsMonitorMock.Object)); + + Assert.False(handler.TryCaptureOnChange(name)); + Assert.True(handler.TryCaptureOnChange(name)); + } + + private static IOnChangeListenersHandler GetHandler(Action configure = null!) + { + var services = new ServiceCollection() + .AddSingleton(); + services.AddOptions(); + services.AddOptions(); + configure?.Invoke(services); + + return services.BuildServiceProvider().GetRequiredService(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/OptionsNameHelperTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/OptionsNameHelperTest.cs new file mode 100644 index 0000000000..2c566ef8be --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/OptionsNameHelperTest.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Resilience.Internal; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Internal.Test; +public class OptionsNameHelperTest +{ + [Fact] + public void GetPolicyOptionsName_Ok() + { + var name = OptionsNameHelper.GetPolicyOptionsName(SupportedPolicies.RetryPolicy, "pipeline", "retry"); + + Assert.Equal("pipeline-RetryPolicy-retry", name); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/PipelineMeteringTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/PipelineMeteringTests.cs new file mode 100644 index 0000000000..d07b4bd2fa --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/PipelineMeteringTests.cs @@ -0,0 +1,171 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.ExceptionSummarization; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metering; +using Moq; +using Polly; +using Xunit; + +#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize +#pragma warning disable CA1063 // Implement IDisposable Correctly + +namespace Microsoft.Extensions.Resilience.Polly.Test.Internals; + +public class PipelineMeteringTests : IDisposable +{ + private const string PipelineName = "pipeline-name"; + + private const string PipelineKey = "pipeline-key"; + + private const string ResultType = "String"; + + private const string MetricName = @"R9\Resilience\Pipelines"; + + private readonly Mock _summarizer; + private readonly Mock _outgoingContext; + private readonly Meter _meter; + private readonly MetricCollector _metricCollector; + private PipelineMetering _metering; + + public PipelineMeteringTests() + { + _summarizer = new Mock(MockBehavior.Strict); + _outgoingContext = new Mock(MockBehavior.Strict); + _meter = new(); + _metricCollector = new(_meter); + _metering = new PipelineMetering(_meter, _summarizer.Object, new[] { _outgoingContext.Object }); + } + + public void Dispose() + { + _metricCollector.Dispose(); + _meter.Dispose(); + } + + [Fact] + public void Initialize_Twice_Throws() + { + Initialize(); + Assert.Throws(() => Initialize()); + } + + [InlineData("", TelemetryConstants.Unknown)] + [InlineData(null, TelemetryConstants.Unknown)] + [InlineData(TelemetryConstants.Unknown, TelemetryConstants.Unknown)] + [InlineData("test", "test")] + [Theory] + public void Initialize_EnsurePipelineKeyRespected(string pipelineKey, string expectedKey) + { + Initialize(pipelineKey); + + RecordPipelineExecution(null); + + Assert.Equal(expectedKey, Counter.LatestWritten!.GetDimension(ResilienceDimensions.PipelineKey)); + } + + [Fact] + public void NotInitialized_Throws() + { + Assert.Throws(() => RecordPipelineExecution(null)); + } + + [Fact] + public void NoOutgoingContext_ShouldNotThrow() + { + var services = new ServiceCollection(); + services.AddOptions(); + + _metering = new PipelineMetering(_meter, _summarizer.Object, Array.Empty()); + + Initialize(); + + RecordPipelineExecution(null); + + Assert.Equal(TelemetryConstants.Unknown, Counter.LatestWritten!.GetDimension(ResilienceDimensions.DependencyName)); + Assert.Equal(TelemetryConstants.Unknown, Counter.LatestWritten!.GetDimension(ResilienceDimensions.RequestName)); + } + + [Fact] + public void RecordEvent_NullException_Ok() + { + Initialize(); + + RecordPipelineExecution(null); + + var latest = Counter.LatestWritten!; + + Assert.Equal(PipelineName, latest.GetDimension(ResilienceDimensions.PipelineName)); + Assert.Equal(PipelineKey, latest.GetDimension(ResilienceDimensions.PipelineKey)); + Assert.Equal(ResultType, latest.GetDimension(ResilienceDimensions.ResultType)); + Assert.Equal(TelemetryConstants.Unknown, latest.GetDimension(ResilienceDimensions.FailureSource)); + Assert.Equal(TelemetryConstants.Unknown, latest.GetDimension(ResilienceDimensions.FailureReason)); + Assert.Equal(TelemetryConstants.Unknown, latest.GetDimension(ResilienceDimensions.FailureSummary)); + Assert.Equal(TelemetryConstants.Unknown, latest.GetDimension(ResilienceDimensions.DependencyName)); + Assert.Equal(TelemetryConstants.Unknown, latest.GetDimension(ResilienceDimensions.RequestName)); + } + + [Fact] + public void RecordEvent_Exception_Ok() + { + Initialize(); + var er = new InvalidOperationException(); + + _summarizer.Setup(v => v.Summarize(er)).Returns(new ExceptionSummary("type", "desc", "details")); + + RecordPipelineExecution(er); + + var latest = Counter.LatestWritten!; + + Assert.Equal(TelemetryConstants.Unknown, latest.GetDimension(ResilienceDimensions.FailureSource)); + Assert.Equal("InvalidOperationException", latest.GetDimension(ResilienceDimensions.FailureReason)); + Assert.Equal("type:desc:details", latest.GetDimension(ResilienceDimensions.FailureSummary)); + } + + [Fact] + public void RecordEvent_RequestMetadata() + { + Initialize(); + var er = new InvalidOperationException(); + var metadata1 = new RequestMetadata { DependencyName = "dep", RequestName = "req" }; + var metadata2 = new RequestMetadata { DependencyName = "dep2", RequestName = "req2" }; + + _outgoingContext.Setup(o => o.RequestMetadata).Returns(metadata1); + RecordPipelineExecution(null, new Context()); + + var latest = Counter.LatestWritten!; + + Assert.Equal("dep", latest.GetDimension(ResilienceDimensions.DependencyName)); + Assert.Equal("req", latest.GetDimension(ResilienceDimensions.RequestName)); + + var ctx = new Context + { + [TelemetryConstants.RequestMetadataKey] = metadata2 + }; + RecordPipelineExecution(null, ctx); + + latest = Counter.LatestWritten!; + + Assert.Equal("dep2", latest.GetDimension(ResilienceDimensions.DependencyName)); + Assert.Equal("req2", latest.GetDimension(ResilienceDimensions.RequestName)); + } + + private void RecordPipelineExecution(Exception? exception, Context? context = null) + { + _metering.RecordPipelineExecution(1, exception, context ?? new Context()); + + } + + private MetricValuesHolder Counter => _metricCollector.GetHistogramValues(MetricName)!; + + private void Initialize(string pipelineKey = PipelineKey) + { + _metering.Initialize(PipelineId.Create(PipelineName, pipelineKey)); + _outgoingContext.Setup(o => o.RequestMetadata).Returns(null); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/PipelineTelemetryTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/PipelineTelemetryTests.cs new file mode 100644 index 0000000000..04d01e56f8 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/PipelineTelemetryTests.cs @@ -0,0 +1,112 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Microsoft.Extensions.Time.Testing; +using Moq; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Internal.Test; + +public class PipelineTelemetryTests +{ + [Fact] + public async Task Create_EnsureMetering() + { + var metering = new Mock(MockBehavior.Strict); + var policy = PipelineTelemetry.Create(PipelineId.Create("a", "b"), Policy.NoOpAsync(), metering.Object, NullLogger.Instance, TimeProvider.System); + var nonGenericPolicy = PipelineTelemetry.Create(PipelineId.Create("a", "b"), Policy.NoOpAsync(), metering.Object, NullLogger.Instance, TimeProvider.System); + + metering.Setup(o => o.RecordPipelineExecution(It.IsAny(), null, It.IsAny())); + await policy.ExecuteAsync(() => Task.FromResult("dummy")); + await nonGenericPolicy.ExecuteAsync(() => Task.FromResult("dummy")); + + metering.Verify(o => o.RecordPipelineExecution(It.IsAny(), null, It.IsAny()), Times.Exactly(2)); + + var error = new InvalidOperationException(); + metering.Setup(o => o.RecordPipelineExecution(It.IsAny(), error, It.IsAny())); + await Assert.ThrowsAsync(() => policy.ExecuteAsync(() => throw error)); + await Assert.ThrowsAsync(() => nonGenericPolicy.ExecuteAsync(() => throw error)); + + metering.Verify(o => o.RecordPipelineExecution(It.IsAny(), error, It.IsAny()), Times.Exactly(2)); + } + + [InlineData("Dummy", false)] + [InlineData("", false)] + [InlineData("Dummy", true)] + [InlineData("", true)] + [Theory] + public async Task Create_EnsureLogging(string key, bool error) + { + var collector = new FakeLogCollector(); + var timeProvider = new FakeTimeProvider(); + var logger = new FakeLogger(collector); + var policy = PipelineTelemetry.Create( + PipelineId.Create("a", key), + Policy.NoOpAsync(), + Mock.Of(), + logger, + timeProvider); + + var nonGenericPolicy = PipelineTelemetry.Create( + PipelineId.Create("b", key), + Policy.NoOpAsync(), + Mock.Of(), + logger, + timeProvider); + + timeProvider.Advance(TimeSpan.FromMinutes(1)); + + if (error) + { + try + { + await policy.ExecuteAsync(() => throw new InvalidOperationException()); + } + catch (InvalidOperationException) + { + // ok + } + + try + { + await nonGenericPolicy.ExecuteAsync(() => throw new InvalidOperationException()); + } + catch (InvalidOperationException) + { + // ok + } + } + else + { + await policy.ExecuteAsync(() => Task.FromResult("dummy")); + await nonGenericPolicy.ExecuteAsync(() => Task.FromResult("dummy")); + } + + if (string.IsNullOrEmpty(key)) + { + key = TelemetryConstants.Unknown; + } + + var entries = collector.GetSnapshot(); + Assert.Equal(4, entries.Count); + Assert.Equal($"Executing pipeline. Pipeline Name: a, Pipeline Key: {key}", entries[0].Message); + Assert.Equal($"Executing pipeline. Pipeline Name: b, Pipeline Key: {key}", entries[2].Message); + + if (error) + { + Assert.Equal($"Pipeline execution failed in 0ms. Pipeline Name: a, Pipeline Key: {key}", entries[1].Message); + Assert.Equal($"Pipeline execution failed in 0ms. Pipeline Name: b, Pipeline Key: {key}", entries[3].Message); + } + else + { + Assert.Equal($"Pipeline executed in 0ms. Pipeline Name: a, Pipeline Key: {key}", entries[1].Message); + Assert.Equal($"Pipeline executed in 0ms. Pipeline Name: b, Pipeline Key: {key}", entries[3].Message); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/PolicyPipelineBuilderTResultTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/PolicyPipelineBuilderTResultTest.cs new file mode 100644 index 0000000000..19f918b229 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/PolicyPipelineBuilderTResultTest.cs @@ -0,0 +1,257 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Extensions.Resilience.Polly.Test.Hedging; +using Microsoft.Extensions.Resilience.Polly.Test.Options; +using Moq; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Internal.Test; + +public sealed class PolicyPipelineBuilderTResultTest : IDisposable +{ + private const string DefaultPolicyName = "default-policy-name-for-test"; + + private static readonly FallbackPolicyOptions _defaultFallbackPolicyOptions = new(); + private static readonly CircuitBreakerPolicyOptions _defaultCircuitBreakerConfig = new(); + private static readonly RetryPolicyOptions _defaultRetryPolicyConfig = new(); + private static readonly HedgingPolicyOptions _defaultHedgingPolicyOptions = new(); + private static readonly IAsyncPolicy _defaultPolicy = Policy.NoOpAsync(); + private static readonly IAsyncPolicy _defaultPolicyNonGeneric = Policy.NoOpAsync(); + private static readonly FallbackScenarioTaskProvider _defaultFallbackAction = _ => Task.FromResult("42"); + + private readonly Mock _policyFactoryMock; + private readonly Mock _metering; + private readonly PolicyPipelineBuilder _builder; + + public PolicyPipelineBuilderTResultTest() + { + _metering = new Mock(MockBehavior.Strict); + _policyFactoryMock = new Mock(MockBehavior.Strict); + _builder = new PolicyPipelineBuilder(_policyFactoryMock.Object, _metering.Object, NullLogger.Instance); + } + + public void Dispose() + { + _metering.VerifyAll(); + _policyFactoryMock.VerifyAll(); + } + + [Fact] + public void AddCircuitBreakerPolicy_ValidConfiguration_ShouldReturnInstance() + { + _policyFactoryMock + .Setup(mock => mock.CreateCircuitBreakerPolicy(DefaultPolicyName, _defaultCircuitBreakerConfig)) + .Returns(_defaultPolicy); + + var builderWithPolicy = _builder.AddCircuitBreakerPolicy(DefaultPolicyName, _defaultCircuitBreakerConfig); + Assert.NotNull(builderWithPolicy); + } + + [Fact] + public void AddCircuitBreakerPolicy_NullArguments_ShouldThrow() + { + Assert.Throws(() => _builder.AddCircuitBreakerPolicy(DefaultPolicyName, null!)); + } + + [Fact] + public void AddRetryPolicy_ValidConfiguration_ShouldReturnInstance() + { + _policyFactoryMock + .Setup(mock => mock.CreateRetryPolicy(DefaultPolicyName, _defaultRetryPolicyConfig)) + .Returns(_defaultPolicy); + + var builderWithPolicy = _builder.AddRetryPolicy(DefaultPolicyName, _defaultRetryPolicyConfig); + Assert.NotNull(builderWithPolicy); + } + + [Fact] + public void AddRetryPolicy_NullConfiguration_ShouldThrow() + { + Assert.Throws(() => _builder.AddRetryPolicy(DefaultPolicyName, null!)); + } + + [Fact] + public void AddTimeoutPolicy_ValidConfiguration_ShouldReturnInstance() + { + var defaultTimeoutOptions = new TimeoutPolicyOptions + { + TimeoutInterval = TimeSpan.FromSeconds(10) + }; + + _policyFactoryMock + .Setup(mock => mock.CreateTimeoutPolicy(DefaultPolicyName, defaultTimeoutOptions)) + .Returns(_defaultPolicyNonGeneric); + + var builderWithPolicy = _builder.AddTimeoutPolicy(DefaultPolicyName, defaultTimeoutOptions); + Assert.NotNull(builderWithPolicy); + } + + [Fact] + public void AddTimeoutPolicy_MultipleTimeoutPolicies_ShouldAllowAndReturnInstance() + { + var primaryTimeoutOptions = new TimeoutPolicyOptions { TimeoutInterval = TimeSpan.FromSeconds(30) }; + var secondaryTimeoutOptions = new TimeoutPolicyOptions { TimeoutInterval = TimeSpan.FromSeconds(10) }; + _policyFactoryMock + .Setup(mock => mock.CreateTimeoutPolicy("primary-timeout", primaryTimeoutOptions)) + .Returns(_defaultPolicyNonGeneric); + _policyFactoryMock + .Setup(mock => mock.CreateTimeoutPolicy("secondary-timeout", secondaryTimeoutOptions)) + .Returns(_defaultPolicyNonGeneric); + + var builderWithPolicy = _builder + .AddTimeoutPolicy("primary-timeout", primaryTimeoutOptions) + .AddTimeoutPolicy("secondary-timeout", secondaryTimeoutOptions); + + Assert.Equal(_builder, builderWithPolicy); + } + + [Fact] + public void AddFallbackPolicy_ValidConfiguration_ShouldReturnInstance() + { + _policyFactoryMock + .Setup(mock => mock.CreateFallbackPolicy( + DefaultPolicyName, + It.IsAny>(), + _defaultFallbackPolicyOptions)) + .Returns(_defaultPolicy); + + var builderWithPolicy = _builder.AddFallbackPolicy( + DefaultPolicyName, + _defaultFallbackAction, + _defaultFallbackPolicyOptions); + + Assert.NotNull(builderWithPolicy); + } + + [Fact] + public void AddFallbackPolicy_NullArguments_ShouldThrow() + { + Assert.Throws(() => _builder.AddFallbackPolicy(DefaultPolicyName, null!, _defaultFallbackPolicyOptions)); + + Assert.Throws(() => _builder.AddFallbackPolicy(DefaultPolicyName, _defaultFallbackAction, null!)); + } + + [Fact] + public void AddBulkheadPolicy_Null_ShouldThrow() + { + Assert.Throws(() => _builder.AddBulkheadPolicy(DefaultPolicyName, null!)); + } + + [Fact] + public void AddBulkheadPolicy_ShouldReturnInstance() + { + var options = Constants.BulkheadPolicy.DefaultOptions; + + _policyFactoryMock + .Setup(mock => mock.CreateBulkheadPolicy(DefaultPolicyName, options)) + .Returns(_defaultPolicyNonGeneric); + + var policy = _builder.AddBulkheadPolicy(DefaultPolicyName, options); + Assert.NotNull(policy); + } + + [Fact] + public void AddHedgingPolicy_ValidConfiguration_ShouldReturnInstance() + { + _policyFactoryMock + .Setup(mock => mock.CreateHedgingPolicy( + DefaultPolicyName, + HedgingTestUtilities.HedgedTasksHandler.FunctionsProvider, + _defaultHedgingPolicyOptions)) + .Returns(_defaultPolicy); + + var builderWithPolicy = _builder.AddHedgingPolicy( + DefaultPolicyName, + HedgingTestUtilities.HedgedTasksHandler.FunctionsProvider, + _defaultHedgingPolicyOptions); + + Assert.NotNull(builderWithPolicy); + } + + [Fact] + public void AddHedgingPolicy_NullArguments_ShouldThrow() + { + Assert.Throws( + () => _builder.AddHedgingPolicy( + DefaultPolicyName, + HedgingTestUtilities.HedgedTasksHandler.FunctionsProvider, + null!)); + + Assert.Throws( + () => _builder.AddHedgingPolicy( + DefaultPolicyName, + null!, + _defaultHedgingPolicyOptions)); + } + + [Fact] + public void Build_SinglePolicyConfigured_ShouldReturnSamePolicy() + { + _policyFactoryMock + .Setup(mock => mock.CreateCircuitBreakerPolicy(DefaultPolicyName, _defaultCircuitBreakerConfig)) + .Returns(_defaultPolicy); + + var singlePolicy = _builder.AddCircuitBreakerPolicy(DefaultPolicyName, _defaultCircuitBreakerConfig).Build(); + Assert.NotNull(singlePolicy); + Assert.Equal(_defaultPolicy, singlePolicy); + } + + [Fact] + public void Build_NoPolicyConfigured_ShouldThrow() + { + Assert.Throws(() => _builder.Build()); + } + + [Fact] + public void Build_MultiplePoliciesConfigured_ShouldReturnPolicyWrap() + { + _policyFactoryMock + .Setup(mock => mock.CreateCircuitBreakerPolicy("circuit", _defaultCircuitBreakerConfig)) + .Returns(_defaultPolicy); + _policyFactoryMock + .Setup(mock => mock.CreateRetryPolicy("retry", _defaultRetryPolicyConfig)) + .Returns(_defaultPolicy); + + var policy = _builder + .AddCircuitBreakerPolicy("circuit", _defaultCircuitBreakerConfig) + .AddRetryPolicy("retry", _defaultRetryPolicyConfig) + .Build(); + Assert.NotNull(policy); + + var policyWrap = policy as AsyncPolicyPipeline; + Assert.NotNull(policyWrap); + } + + [InlineData("name", "", "String-name")] + [InlineData("name", "key", "String-name-key")] + [Theory] + public void SetPipelineIdentifiers_EnsurePolicyKeyAssignedAndMeteredPolicyReturned(string name, string key, string expectedKey) + { + var id = PipelineId.Create(name, key); + + // arrange + _policyFactoryMock + .Setup(mock => mock.CreateCircuitBreakerPolicy("circuit", _defaultCircuitBreakerConfig)) + .Returns(() => Policy.NoOpAsync()); + _policyFactoryMock.Setup(f => f.Initialize(id)); + _metering.Setup(v => v.Initialize(id)); + + // act + _builder.Initialize(id); + + var policy = _builder + .AddCircuitBreakerPolicy("circuit", _defaultCircuitBreakerConfig) + .Build(); + + // assert + Assert.Equal(expectedKey, policy.PolicyKey); + Assert.IsType>(policy); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/PolicyPipelineBuilderTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/PolicyPipelineBuilderTest.cs new file mode 100644 index 0000000000..8fff40a6d1 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/PolicyPipelineBuilderTest.cs @@ -0,0 +1,257 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Extensions.Resilience.Polly.Test.Hedging; +using Microsoft.Extensions.Resilience.Polly.Test.Options; +using Moq; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Internal.Test; + +public sealed class PolicyPipelineBuilderTest : IDisposable +{ + private const string DefaultPolicyName = "default-policy-name-for-test"; + + private static readonly FallbackPolicyOptions _defaultFallbackPolicyOptions = new(); + private static readonly CircuitBreakerPolicyOptions _defaultCircuitBreakerConfig = new(); + private static readonly RetryPolicyOptions _defaultRetryPolicyConfig = new(); + private static readonly HedgingPolicyOptions _defaultHedgingPolicyOptions = new(); + private static readonly IAsyncPolicy _defaultPolicy = Policy.NoOpAsync(); + private static readonly FallbackScenarioTaskProvider _defaultFallbackAction = _ => Task.FromResult("42"); + + private readonly Mock _policyFactoryMock; + private readonly Mock _metering; + private readonly PolicyPipelineBuilder _builder; + + public PolicyPipelineBuilderTest() + { + _metering = new Mock(MockBehavior.Strict); + _policyFactoryMock = new Mock(MockBehavior.Strict); + _builder = new PolicyPipelineBuilder(_policyFactoryMock.Object, _metering.Object, NullLogger.Instance); + } + + public void Dispose() + { + _metering.VerifyAll(); + _policyFactoryMock.VerifyAll(); + } + + [Fact] + public void AddCircuitBreakerPolicy_ValidConfiguration_ShouldReturnInstance() + { + _policyFactoryMock + .Setup(mock => mock.CreateCircuitBreakerPolicy(DefaultPolicyName, _defaultCircuitBreakerConfig)) + .Returns(_defaultPolicy); + + var builderWithPolicy = _builder.AddCircuitBreakerPolicy(DefaultPolicyName, _defaultCircuitBreakerConfig); + Assert.NotNull(builderWithPolicy); + } + + [Fact] + public void AddCircuitBreakerPolicy_NullArguments_ShouldThrow() + { + Assert.Throws(() => _builder.AddCircuitBreakerPolicy(DefaultPolicyName, null!)); + } + + [Fact] + public void AddRetryPolicy_ValidConfiguration_ShouldReturnInstance() + { + _policyFactoryMock + .Setup(mock => mock.CreateRetryPolicy(DefaultPolicyName, _defaultRetryPolicyConfig)) + .Returns(_defaultPolicy); + + var builderWithPolicy = _builder.AddRetryPolicy(DefaultPolicyName, _defaultRetryPolicyConfig); + Assert.NotNull(builderWithPolicy); + } + + [Fact] + public void AddRetryPolicy_NullConfiguration_ShouldThrow() + { + Assert.Throws(() => _builder.AddRetryPolicy(DefaultPolicyName, null!)); + } + + [Fact] + public void AddTimeoutPolicy_ValidConfiguration_ShouldReturnInstance() + { + var defaultTimeoutOptions = new TimeoutPolicyOptions + { + TimeoutInterval = TimeSpan.FromSeconds(10) + }; + + _policyFactoryMock + .Setup(mock => mock.CreateTimeoutPolicy(DefaultPolicyName, defaultTimeoutOptions)) + .Returns(_defaultPolicy); + + var builderWithPolicy = _builder.AddTimeoutPolicy(DefaultPolicyName, defaultTimeoutOptions); + Assert.NotNull(builderWithPolicy); + } + + [Fact] + public void AddTimeoutPolicy_MultipleTimeoutPolicies_ShouldAllowAndReturnInstance() + { + var primaryTimeoutOptions = new TimeoutPolicyOptions { TimeoutInterval = TimeSpan.FromSeconds(30) }; + var secondaryTimeoutOptions = new TimeoutPolicyOptions { TimeoutInterval = TimeSpan.FromSeconds(10) }; + _policyFactoryMock + .Setup(mock => mock.CreateTimeoutPolicy("primary-timeout", primaryTimeoutOptions)) + .Returns(_defaultPolicy); + _policyFactoryMock + .Setup(mock => mock.CreateTimeoutPolicy("secondary-timeout", secondaryTimeoutOptions)) + .Returns(_defaultPolicy); + + var builderWithPolicy = _builder + .AddTimeoutPolicy("primary-timeout", primaryTimeoutOptions) + .AddTimeoutPolicy("secondary-timeout", secondaryTimeoutOptions); + + Assert.Equal(_builder, builderWithPolicy); + } + + [Fact] + public void AddFallbackPolicy_ValidConfiguration_ShouldReturnInstance() + { + _policyFactoryMock + .Setup(mock => mock.CreateFallbackPolicy( + DefaultPolicyName, + It.IsAny(), + _defaultFallbackPolicyOptions)) + .Returns(_defaultPolicy); + + var builderWithPolicy = _builder.AddFallbackPolicy( + DefaultPolicyName, + _defaultFallbackAction, + _defaultFallbackPolicyOptions); + + Assert.NotNull(builderWithPolicy); + } + + [Fact] + public void AddFallbackPolicy_NullArguments_ShouldThrow() + { + Assert.Throws(() => _builder.AddFallbackPolicy(DefaultPolicyName, null!, _defaultFallbackPolicyOptions)); + + Assert.Throws(() => _builder.AddFallbackPolicy(DefaultPolicyName, _defaultFallbackAction, null!)); + } + + [Fact] + public void AddBulkheadPolicy_Null_ShouldThrow() + { + Assert.Throws(() => _builder.AddBulkheadPolicy(DefaultPolicyName, null!)); + } + + [Fact] + public void AddBulkheadPolicy_ShouldReturnInstance() + { + var options = Constants.BulkheadPolicy.DefaultOptions; + + _policyFactoryMock + .Setup(mock => mock.CreateBulkheadPolicy(DefaultPolicyName, options)) + .Returns(_defaultPolicy); + + var policy = _builder.AddBulkheadPolicy(DefaultPolicyName, options); + Assert.NotNull(policy); + } + + [Fact] + public void AddHedgingPolicy_ValidConfiguration_ShouldReturnInstance() + { + _policyFactoryMock + .Setup(mock => mock.CreateHedgingPolicy( + DefaultPolicyName, + HedgingTestUtilities.HedgedTasksHandler.FunctionsProviderNonGeneric, + _defaultHedgingPolicyOptions)) + .Returns(_defaultPolicy); + + var builderWithPolicy = _builder.AddHedgingPolicy( + DefaultPolicyName, + HedgingTestUtilities.HedgedTasksHandler.FunctionsProviderNonGeneric, + _defaultHedgingPolicyOptions); + + Assert.NotNull(builderWithPolicy); + } + + [Fact] + public void AddHedgingPolicy_NullArguments_ShouldThrow() + { + Assert.Throws( + () => _builder.AddHedgingPolicy( + DefaultPolicyName, + HedgingTestUtilities.HedgedTasksHandler.FunctionsProviderNonGeneric, + null!)); + + Assert.Throws( + () => _builder.AddHedgingPolicy( + DefaultPolicyName, + null!, + _defaultHedgingPolicyOptions)); + } + + [Fact] + public void Build_SinglePolicyConfigured_ShouldReturnSamePolicy() + { + _policyFactoryMock + .Setup(mock => mock.CreateCircuitBreakerPolicy(DefaultPolicyName, _defaultCircuitBreakerConfig)) + .Returns(_defaultPolicy); + + var singlePolicy = _builder.AddCircuitBreakerPolicy(DefaultPolicyName, _defaultCircuitBreakerConfig).Build(); + Assert.NotNull(singlePolicy); + Assert.Equal(_defaultPolicy, singlePolicy); + } + + [Fact] + public void Build_MultiplePoliciesConfigured_ShouldReturnPolicyWrap() + { + _policyFactoryMock + .Setup(mock => mock.CreateCircuitBreakerPolicy("circuit", _defaultCircuitBreakerConfig)) + .Returns(_defaultPolicy); + _policyFactoryMock + .Setup(mock => mock.CreateRetryPolicy("retry", _defaultRetryPolicyConfig)) + .Returns(_defaultPolicy); + + var policy = _builder + .AddCircuitBreakerPolicy("circuit", _defaultCircuitBreakerConfig) + .AddRetryPolicy("retry", _defaultRetryPolicyConfig) + .Build(); + Assert.NotNull(policy); + + var policyWrap = policy as AsyncPolicyPipeline; + Assert.NotNull(policyWrap); + } + + [InlineData("name", "", "String-name")] + [InlineData("name", "key", "String-name-key")] + [Theory] + public void SetPipelineIdentifiers_EnsurePolicyKeyAssignedAndMeteredPolicyReturned(string name, string key, string expectedKey) + { + var id = PipelineId.Create(name, key); + + // arrange + _policyFactoryMock + .Setup(mock => mock.CreateCircuitBreakerPolicy("circuit", _defaultCircuitBreakerConfig)) + .Returns(() => Policy.NoOpAsync()); + _policyFactoryMock.Setup(f => f.Initialize(id)); + _metering.Setup(v => v.Initialize(id)); + + // act + _builder.Initialize(id); + + var policy = _builder + .AddCircuitBreakerPolicy("circuit", _defaultCircuitBreakerConfig) + .Build(); + + // assert + Assert.Equal(expectedKey, policy.PolicyKey); + Assert.IsType(policy); + } + + [Fact] + public void Build_NoPoliciesConfigured_ShouldThrow() + { + var exception = Assert.Throws(() => _builder.Build()); + Assert.Equal("At least one policy must be configured.", exception.Message); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineBuilderExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineBuilderExtensionsTest.cs new file mode 100644 index 0000000000..573678c092 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineBuilderExtensionsTest.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Extensions.Resilience.Test.Helpers; +using Moq; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Internal.Test; + +public class ResiliencePipelineBuilderExtensionsTest : ResilienceTestHelper +{ + private readonly IResiliencePipelineBuilder _builder; + private readonly Mock> _pipelineBuilder = new(MockBehavior.Strict); + + public ResiliencePipelineBuilderExtensionsTest() + { + Services.TryAddSingleton(_pipelineBuilder.Object); + + _builder = Services.AddResiliencePipeline(DefaultPipelineName); + _pipelineBuilder.Setup(b => b.Initialize(PipelineId.Create(DefaultPipelineName, DefaultPipelineKey))); + } + + [Fact] + public void AddPolicy_ArgumentValidation() + { + Assert.Throws(() => Resilience.Internal.ResiliencePipelineBuilderExtensions.AddPolicy(null!, (_, _) => { })); + Assert.Throws(() => Resilience.Internal.ResiliencePipelineBuilderExtensions.AddPolicy(_builder, null!)); + } + + [Fact] + public void AddPolicy_WithOptions_Ok() + { + var called = false; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { { "bulkhead:MaxQueuedActions", "11" } }) + .Build() + .GetSection("bulkhead"); + _builder.AddPolicy( + SupportedPolicies.BulkheadPolicy, + "test", + options => options.Configure(configuration, o => o.MaxConcurrency = 22), + (builder, options) => + { + Assert.Equal(22, options.MaxConcurrency); + Assert.Equal(11, options.MaxQueuedActions); + called = true; + builder.AddBulkheadPolicy("test", options); + }); + + var provider = Services.BuildServiceProvider(); + var resilienceProvider = provider.GetRequiredService(); + var options = provider + .GetRequiredService>() + .Get(OptionsNameHelper.GetPolicyOptionsName(SupportedPolicies.BulkheadPolicy, DefaultPipelineName, "test")); + _pipelineBuilder.Setup(v => v.AddBulkheadPolicy("test", options)).Returns(_pipelineBuilder.Object); + _pipelineBuilder.Setup(v => v.Build()).Returns(Policy.NoOpAsync); + + resilienceProvider.CreatePipeline(DefaultPipelineName, DefaultPipelineKey); + + Assert.True(called); + } + + [Fact] + public void AddPolicy_WithOptions_EnsureValidated() + { + _builder.AddPolicy( + SupportedPolicies.BulkheadPolicy, + "test", + o => o.Configure(o => o.MaxConcurrency = -1), + (builder, options) => builder.AddBulkheadPolicy("test", options)); + + Assert.Throws(() => CreatePipeline()); + } + + private class DummyValidator : IValidateOptions + { + public ValidateOptionsResult Validate(string? name, BulkheadPolicyOptions options) + { + if (options.MaxConcurrency < 0) + { + return ValidateOptionsResult.Fail("ERROR"); + } + + return ValidateOptionsResult.Success; + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineFactoryOptionsTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineFactoryOptionsTest.cs new file mode 100644 index 0000000000..e9c6f5ac48 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineFactoryOptionsTest.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Resilience.Internal; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Internal.Test; +public class ResiliencePipelineFactoryOptionsTest +{ + [Fact] + public void BuilderActions_EnsureNotNull() + { + var options = new ResiliencePipelineFactoryOptions(); + + Assert.NotNull(options.BuilderActions); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineFactoryOptionsValidatorTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineFactoryOptionsValidatorTest.cs new file mode 100644 index 0000000000..f902235103 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineFactoryOptionsValidatorTest.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Resilience.Internal; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Internal.Test; +public class ResiliencePipelineFactoryOptionsValidatorTest +{ + [Fact] + public void Valid_Ok() + { + var validator = new ResiliencePipelineFactoryOptionsValidator(); + + Assert.True(validator.Validate("test", CreateValid()).Succeeded); + } + + [Fact] + public void InvalidConfiguration_EnsureError() + { + var validator = new ResiliencePipelineFactoryOptionsValidator(); + + var options = CreateValid(); + options.BuilderActions.Clear(); + Assert.True(validator.Validate("test", options).Failed); + } + + private static ResiliencePipelineFactoryOptions CreateValid() + { + var options = new ResiliencePipelineFactoryOptions(); + + options.BuilderActions.Add(builder => { }); + + return options; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineFactoryTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineFactoryTest.cs new file mode 100644 index 0000000000..bb29cbb616 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineFactoryTest.cs @@ -0,0 +1,157 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Moq; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Internal.Test; + +public class ResiliencePipelineFactoryTest +{ + private const string PipelineName = "default-pipeline"; + private const string PipelineKey = "default-key"; + private static readonly PipelineId _pipelineId = PipelineId.Create(PipelineName, PipelineKey); + + private readonly ServiceCollection _services; + private readonly IResiliencePipelineBuilder _builder; + private readonly Mock> _pipelineBuilder = new(MockBehavior.Strict); + private readonly IAsyncPolicy _defaultPolicy = Policy.NoOpAsync(); + private readonly TimeoutPolicyOptions _defaultOptions = new(); + + public ResiliencePipelineFactoryTest() + { + _services = new ServiceCollection(); + _ = _services.AddLogging(); + _services.TryAddSingleton(_pipelineBuilder.Object); + _builder = _services.AddResiliencePipeline(PipelineName); + } + + [Fact] + public void Create_NotConfigured_Throws() + { + Assert.Throws(() => CreateFactory(out _).CreatePipeline("not-configured", string.Empty)); + } + + [Fact] + public void Create_NullArgument_Throws() + { + Assert.Throws(() => CreateFactory(out _).CreatePipeline(null!, string.Empty)); + Assert.Throws(() => CreateFactory(out _).CreatePipeline(string.Empty, string.Empty)); + } + + [Fact] + public void AddPolicy_EnsureCalled() + { + _pipelineBuilder.Setup(v => v.AddTimeoutPolicy("test", _defaultOptions)).Returns(_pipelineBuilder.Object).Verifiable(); + _pipelineBuilder.Setup(v => v.Initialize(_pipelineId)); + _pipelineBuilder.Setup(v => v.Build()).Returns(_defaultPolicy).Verifiable(); + + AddPolicy(builder => builder.AddTimeoutPolicy("test", _defaultOptions)); + + var factory = CreateFactory(out var provider); + + var dynamicPolicy = factory.CreatePipeline(PipelineName, PipelineKey) as AsyncDynamicPipeline; + Assert.Equal(_defaultPolicy, dynamicPolicy!.CurrentValue); + + _pipelineBuilder.VerifyAll(); + } + + [Fact] + public void AddPolicy_Multiple_EnsureCalledAndOrderPreserved() + { + var otherOptions = new TimeoutPolicyOptions(); + var order = 0; + + _pipelineBuilder.Setup(v => v.AddTimeoutPolicy("test1", _defaultOptions)).Returns(_pipelineBuilder.Object).Callback(() => + { + Assert.Equal(0, order); + order++; + }).Verifiable(); + + _pipelineBuilder.Setup(v => v.AddTimeoutPolicy("test2", otherOptions)).Returns(_pipelineBuilder.Object).Callback(() => + { + Assert.Equal(1, order); + order++; + }).Verifiable(); + _pipelineBuilder.Setup(v => v.Initialize(_pipelineId)); + _pipelineBuilder.Setup(v => v.Build()).Returns(_defaultPolicy).Verifiable(); + + AddPolicy(builder => builder.AddTimeoutPolicy("test1", _defaultOptions)); + AddPolicy(builder => builder.AddTimeoutPolicy("test2", otherOptions)); + + var factory = CreateFactory(out var provider); + + factory.CreatePipeline(PipelineName, PipelineKey); + + _pipelineBuilder.VerifyAll(); + } + + [Fact] + public void Create_EnsureRecreated() + { + _pipelineBuilder.Setup(v => v.AddTimeoutPolicy("test", _defaultOptions)).Returns(_pipelineBuilder.Object).Verifiable(); + _pipelineBuilder.Setup(v => v.Initialize(_pipelineId)); + _pipelineBuilder.Setup(v => v.Build()).Returns(() => Policy.NoOpAsync()).Verifiable(); + + AddPolicy(builder => builder.AddTimeoutPolicy("test", _defaultOptions)); + + var factory = CreateFactory(out var provider); + + Assert.NotEqual(factory.CreatePipeline(PipelineName, PipelineKey), factory.CreatePipeline(PipelineName, PipelineKey)); + } + + [Fact] + public void Create_WithPipelineKey() + { + _pipelineBuilder.Setup(v => v.AddTimeoutPolicy("test", _defaultOptions)).Returns(_pipelineBuilder.Object).Verifiable(); + _pipelineBuilder.Setup(v => v.Initialize(It.Is(v => v.PipelineKey == "key"))); + _pipelineBuilder.Setup(v => v.Build()).Returns(() => Policy.NoOpAsync()).Verifiable(); + + AddPolicy(builder => builder.AddTimeoutPolicy("test", _defaultOptions)); + + var factory = CreateFactory(out var provider); + + Assert.NotNull(factory.CreatePipeline(PipelineName, "key")); + } + + [Fact] + public void Create_MultiplePipelines_EnsureDistinctResults() + { + var options1 = new TimeoutPolicyOptions(); + var options2 = new TimeoutPolicyOptions(); + + _pipelineBuilder.Setup(v => v.Initialize(_pipelineId)); + _pipelineBuilder.Setup(v => v.Initialize(It.Is(v => v.PipelineName == "other" && v.PipelineKey == string.Empty))); + _pipelineBuilder.Setup(v => v.AddTimeoutPolicy("test", options1)).Returns(_pipelineBuilder.Object).Verifiable(); + _pipelineBuilder.Setup(v => v.AddTimeoutPolicy("test", options2)).Returns(_pipelineBuilder.Object).Verifiable(); + _pipelineBuilder.Setup(v => v.Build()).Returns(() => Policy.NoOpAsync()).Verifiable(); + + AddPolicy(builder => builder.AddTimeoutPolicy("test", options1), PipelineName); + AddPolicy(builder => builder.AddTimeoutPolicy("test", options2), "other"); + + var factory = CreateFactory(out var provider); + + Assert.NotEqual(factory.CreatePipeline(PipelineName, PipelineKey), factory.CreatePipeline("other", string.Empty)); + + _pipelineBuilder.VerifyAll(); + } + + private IResiliencePipelineFactory CreateFactory(out IServiceProvider serviceProvider) + { + serviceProvider = _services.BuildServiceProvider(); + + return serviceProvider.GetRequiredService(); + } + + private void AddPolicy(Action> configure, string pipelineName = PipelineName) + { + _builder.Services.AddResiliencePipeline(pipelineName).AddPolicy((builder, _) => configure(builder)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineProviderTest.DynamicChanges.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineProviderTest.DynamicChanges.cs new file mode 100644 index 0000000000..0fef52b7e0 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineProviderTest.DynamicChanges.cs @@ -0,0 +1,370 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Moq; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Internal.Test; + +public sealed partial class ResiliencePipelineProviderTest : IDisposable +{ + private const string PipelineName = "testPipeline"; + private const string SecondaryPipelineName = "RandomPipeline"; + private const string PolicyName = "testPolicy"; + + private const string DefaultConfigurationKey = "TimeoutPolicyOptions:TimeoutInterval"; + private const string OtherConfigurationKey = "TimeoutPolicyOptions_Other:TimeoutInterval"; + private const string NoNameConfigurationKey = "TimeoutPolicyOptions_NoName:TimeoutInterval"; + + private static readonly string _updateFailureMessage = $"Pipeline update failed. Pipeline Name: {PipelineName}."; + private static readonly string _updateSuccessMessage = $"Pipeline {PipelineName} has been updated."; + + public void Dispose() + { + _provider.Dispose(); + _factory.VerifyAll(); + } + + [Fact] + public async Task GetPipeline_ConfigurationUpdatedForTargetPipeline_EnsureNewPolicy() + { + var config = new ReloadableConfiguration(); + var pipelineLogger = new FakeLogger>(); + var policyLogger = new FakeLogger(); + using var provider = GetPipelineProvider(pipelineLogger, policyLogger, config); + + var pipelineOnFirstCall = provider.GetPipeline(PipelineName) as AsyncDynamicPipeline; + var pipelineOnSecondCall = provider.GetPipeline(PipelineName) as AsyncDynamicPipeline; + Assert.NotNull(pipelineOnFirstCall); + Assert.Same(pipelineOnFirstCall, pipelineOnSecondCall); + Assert.Equal(0, pipelineLogger.Collector.Count); + + var pipelineOnFirstCallCurrent = pipelineOnFirstCall.CurrentValue; + var pipelineOnSecondCallCurrent = pipelineOnSecondCall!.CurrentValue; + Assert.Same(pipelineOnFirstCallCurrent, pipelineOnSecondCallCurrent); + + // Trigger onChange callback of the options monitor + await config.UpdateTimeoutAndReloadAsync("00:15:00"); + + var pipelineOnFirstCallAfterChange = provider.GetPipeline(PipelineName) as AsyncDynamicPipeline; + Assert.NotNull(pipelineOnFirstCallAfterChange); + Assert.Same(pipelineOnFirstCall, pipelineOnFirstCallAfterChange); + + var pipelineOnFirstCallAfterChangeCurrent = pipelineOnFirstCallAfterChange.CurrentValue; + Assert.NotSame(pipelineOnFirstCallCurrent, pipelineOnFirstCallAfterChangeCurrent); + + // Just for my sanity, let's see if any change event got triggered in the background after multiple calls. + await Task.Delay(5000); + + var pipelineOnSecondCallAfterChangeCurrent = GetCurrentValueOfPipeline(PipelineName, provider); + Assert.Same(pipelineOnFirstCallAfterChangeCurrent, pipelineOnSecondCallAfterChangeCurrent); + Assert.Equal(1, policyLogger.Collector.Count); + Assert.Equal(1, pipelineLogger.Collector.Count); + Assert.Equal(_updateSuccessMessage, pipelineLogger.LatestRecord.Message); + } + + [Theory] + [InlineData(OtherConfigurationKey)] + [InlineData(NoNameConfigurationKey)] + public async Task GetPipeline_ConfigurationUpdatedForDifferentConfigurationProvider_EnsureSamePolicy( + string configurationKey) + { + var pipelineLogger = new FakeLogger>(); + var policyLogger = new FakeLogger(); + var configProvider1 = new ReloadableConfiguration(); + var configProvider2 = new ReloadableConfiguration(); + var config1 = new ConfigurationBuilder().Add(configProvider1).Build(); + var config2 = new ConfigurationBuilder().Add(configProvider2).Build(); + + var services = GetServiceCollection(pipelineLogger, policyLogger); + _ = services + .AddResiliencePipeline(PipelineName) + .AddTimeoutPolicy(PolicyName, config1.GetSection("TimeoutPolicyOptions")); + _ = services + .AddResiliencePipeline(SecondaryPipelineName) + .AddTimeoutPolicy("otherPolicy", config2.GetSection("TimeoutPolicyOptions_Other")); + _ = services.Configure(null, config2.GetSection("TimeoutPolicyOptions_NoName")); + + using var provider = GetPipelineProvider(services); + var pipeline1 = GetCurrentValueOfPipeline(PipelineName, provider); + var pipeline2 = GetCurrentValueOfPipeline(SecondaryPipelineName, provider); + + Assert.Equal(0, pipelineLogger.Collector.Count); + Assert.Equal(0, policyLogger.Collector.Count); + + await configProvider2.UpdateTimeoutAndReloadAsync("00:15:00", configurationKey); + + // The first pipeline is not affected by the change + var pipeline1AfterChange = GetCurrentValueOfPipeline(PipelineName, provider); + Assert.Same(pipeline1, pipeline1AfterChange); + + // The second pipeline should be impacted by the change + var pipeline2AfterChange = GetCurrentValueOfPipeline(SecondaryPipelineName, provider); + Assert.NotSame(pipeline2, pipeline2AfterChange); + + Assert.Equal(1, policyLogger.Collector.Count); + Assert.Equal(1, pipelineLogger.Collector.Count); + Assert.Equal($"Pipeline {SecondaryPipelineName} has been updated.", pipelineLogger.LatestRecord.Message); + } + + [Fact] + public void GetPipeline_WhenNoChanges_RegistersOnlyOneListenerPerPipelineAndKey() + { + var onChangeCalls = 0; + var optionsMonitorMock = new Mock>>(MockBehavior.Strict); + var listenerMock = new Mock(MockBehavior.Strict); + var services = new ServiceCollection().RegisterMetering().AddLogging(); + + var options = new ResiliencePipelineFactoryOptions(); + var timeoutOptions = new TimeoutPolicyOptions(); + options.BuilderActions.Add(b => b.AddTimeoutPolicy(PolicyName, timeoutOptions)); + + listenerMock.Setup(mock => mock.Dispose()); + optionsMonitorMock.Setup(mock => mock.Get(PipelineName)).Returns(options); + optionsMonitorMock + .SetupSequence(mock => mock.OnChange(It.IsAny, string?>>())) + .Returns(() => + { + onChangeCalls++; + return listenerMock.Object; + }) + .Returns(() => + { + onChangeCalls++; + return listenerMock.Object; + }) + .Returns(() => + { + onChangeCalls++; + return null!; + }); + + _ = services.AddResiliencePipeline(PipelineName).AddTimeoutPolicy(PolicyName, o => o.TimeoutInterval = timeoutOptions.TimeoutInterval); + _ = services.AddSingleton(optionsMonitorMock.Object); + + using var provider = GetPipelineProvider(services); + + var pipelineOnFirstCall = provider.GetPipeline(PipelineName); + var pipelineOnSecondCall = provider.GetPipeline(PipelineName); + Assert.Same(pipelineOnFirstCall, pipelineOnSecondCall); + Assert.Equal(1, onChangeCalls); + + var key1 = "key1"; + var key2 = "key2"; + var pipelineWithKey1 = provider.GetPipeline(PipelineName, key1); + var pipelineWithKey2 = provider.GetPipeline(PipelineName, key2); + Assert.NotSame(pipelineWithKey1, pipelineWithKey2); + Assert.Equal(3, onChangeCalls); + } + + [Fact] + public async Task GetPipeline_InvalidConfigurationUpdatedForTargetPipeline_ShouldUseSamePolicy() + { + var config = new ReloadableConfiguration(); + var pipelineLogger = new FakeLogger>(); + var policyLogger = new FakeLogger(); + using var provider = GetPipelineProvider(pipelineLogger, policyLogger, config); + + var pipelineCurrent = GetCurrentValueOfPipeline(PipelineName, provider); + Assert.Equal(0, pipelineLogger.Collector.Count); + Assert.Equal(0, policyLogger.Collector.Count); + + // Trigger onChange callback of the options monitor. + await config.UpdateTimeoutAndReloadAsync("-00:15:00"); + + var pipelineCurrentAfterChange = GetCurrentValueOfPipeline(PipelineName, provider); + Assert.Same(pipelineCurrent, pipelineCurrentAfterChange); + Assert.Equal(0, policyLogger.Collector.Count); + Assert.Equal(1, pipelineLogger.Collector.Count); + Assert.Equal(_updateFailureMessage, pipelineLogger.LatestRecord.Message); + } + + [Fact] + public async Task GetPipeline_MultipleOptionsAndOneInvalid_ShouldUseSamePolicy() + { + var configProvider = new ReloadableConfiguration(); + var config = new ConfigurationBuilder().Add(configProvider).Build(); + var pipelineLogger = new FakeLogger>(); + var timeoutPolicyLogger = new FakeLogger(); + var retryPolicyLogger = new FakeLogger>(); + + var services = GetServiceCollection(pipelineLogger, timeoutPolicyLogger); + _ = services + .AddSingleton>>(retryPolicyLogger) + .AddResiliencePipeline(PipelineName) + .AddTimeoutPolicy(PolicyName, config.GetSection("TimeoutPolicyOptions")) + .AddRetryPolicy(PolicyName, config.GetSection("RetryPolicyOptions")) + .AddTimeoutPolicy("AnotherTimeout", config.GetSection("TimeoutPolicyOptions_Other")) + .AddTimeoutPolicy("AnotherAnotherTimeout", config.GetSection("TimeoutPolicyOptions_NoName")); + + using var provider = GetPipelineProvider(services); + var pipelineCurrent = GetCurrentValueOfPipeline(PipelineName, provider); + Assert.Equal(0, pipelineLogger.Collector.Count); + Assert.Equal(0, timeoutPolicyLogger.Collector.Count); + + // Trigger onChange callback of the options monitor + await configProvider.UpdateTimeoutAndReloadAsync("-00:15:00"); + + var pipelineCurrentAfterChange = GetCurrentValueOfPipeline(PipelineName, provider); + Assert.Same(pipelineCurrent, pipelineCurrentAfterChange); + + // The update event will be triggered for each policy 1xRetry, 3xTimeout + // but the OnChange event will not be propagated for the invalid value + Assert.Equal(1, retryPolicyLogger.Collector.Count); + Assert.Equal(2, timeoutPolicyLogger.Collector.Count); + Assert.Equal(1, pipelineLogger.Collector.Count); + Assert.Equal(_updateFailureMessage, pipelineLogger.LatestRecord.Message); + } + + [Fact] + public async Task GetPipeline_OnStaticActionConfigurations_EnsureNoUpdateImpactsPipelines() + { + var configProvider = new ReloadableConfiguration(); + var pipelineLogger = new FakeLogger>(); + var ignoredOptionsLogger = new FakeLogger(); + var policyLogger = new FakeLogger(); + var config = new ConfigurationBuilder().Add(configProvider).Build(); + var services = GetServiceCollection(pipelineLogger, policyLogger); + _ = services + .Configure(config.GetSection("TimeoutPolicyOptions")) + .AddSingleton>(ignoredOptionsLogger) + .AddResiliencePipeline(PipelineName) + .AddCircuitBreakerPolicy(PolicyName, o => o.FailureThreshold = 0.3); + + using var provider = GetPipelineProvider(services); + var pipelineCurrent = GetCurrentValueOfPipeline(PipelineName, provider); + + await configProvider.UpdateTimeoutAndReloadAsync("00:15:00"); + + var pipelineAfterUpdateCurrent = GetCurrentValueOfPipeline(PipelineName, provider); + Assert.Same(pipelineCurrent, pipelineAfterUpdateCurrent); + + Assert.Equal(0, ignoredOptionsLogger.Collector.Count); + Assert.Equal(0, policyLogger.Collector.Count); + Assert.Equal(0, pipelineLogger.Collector.Count); + } + + [Fact] + public async Task GetPipeline_DuplicatedOptionsName_ShouldRegisterSingleListener() + { + var configProvider = new ReloadableConfiguration(); + var config = new ConfigurationBuilder().Add(configProvider).Build(); + var pipelineLogger = new FakeLogger>(); + var timeoutPolicyLogger = new FakeLogger(); + + var services = GetServiceCollection(pipelineLogger, timeoutPolicyLogger); + _ = services + .AddResiliencePipeline(PipelineName) + .AddTimeoutPolicy(PolicyName, config.GetSection("TimeoutPolicyOptions")) + .AddTimeoutPolicy(PolicyName, config.GetSection("TimeoutPolicyOptions")); + + using var provider = GetPipelineProvider(services); + var pipelineCurrent = GetCurrentValueOfPipeline(PipelineName, provider); + Assert.Equal(0, pipelineLogger.Collector.Count); + Assert.Equal(0, timeoutPolicyLogger.Collector.Count); + + // Trigger onChange callback of the options monitor + await configProvider.UpdateTimeoutAndReloadAsync("00:15:00"); + + var pipelineCurrentAfterChange = GetCurrentValueOfPipeline(PipelineName, provider); + Assert.NotSame(pipelineCurrent, pipelineCurrentAfterChange); + + Assert.Equal(2, timeoutPolicyLogger.Collector.Count); + Assert.Equal(1, pipelineLogger.Collector.Count); + Assert.Equal(_updateSuccessMessage, pipelineLogger.LatestRecord.Message); + } + + private static ResiliencePipelineProvider GetPipelineProvider( + ILogger> pipelineLogger, + ILogger policyLogger, + ReloadableConfiguration configProvider) + { + var config = new ConfigurationBuilder().Add(configProvider).Build(); + var services = GetServiceCollection(pipelineLogger, policyLogger); + + _ = services + .AddResiliencePipeline(PipelineName) + .AddTimeoutPolicy(PolicyName, config.GetSection("TimeoutPolicyOptions")); + + return GetPipelineProvider(services); + } + + private static ResiliencePipelineProvider GetPipelineProvider(IServiceCollection services) + { + return (services.BuildServiceProvider().GetRequiredService() as ResiliencePipelineProvider)!; + } + + private static IAsyncPolicy GetCurrentValueOfPipeline(string pipelineName, ResiliencePipelineProvider provider) + { + var pipeline = provider.GetPipeline(pipelineName) as AsyncDynamicPipeline; + var current = pipeline?.CurrentValue; + Assert.NotNull(current); + + return current; + } + + private static IServiceCollection GetServiceCollection( + ILogger> pipelineLogger, + ILogger policyLogger) + { + return new ServiceCollection() + .RegisterMetering() + .AddLogging() + .AddSingleton(pipelineLogger) + .AddSingleton(policyLogger); + } + + private class ReloadableConfiguration : ConfigurationProvider, IConfigurationSource + { + public ReloadableConfiguration() + { + Data = new Dictionary + { + { DefaultConfigurationKey, "00:00:10" }, + { OtherConfigurationKey, "00:00:11" }, + { NoNameConfigurationKey, "00:00:12" }, + { "RetryPolicyOptions:RetryCount", "4" } + }; + } + + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return this; + } + + public void Reload(Dictionary data) + { + foreach (var kvp in data) + { + Data[kvp.Key] = kvp.Value; + } + + // Note: The event should run in a background thread. + // This is faking that. + _ = Task.Run(() => OnReload()); + } + + public async Task UpdateTimeoutAndReloadAsync(string timeoutValue, string key = DefaultConfigurationKey) + { + var content = new Dictionary(Data) + { + [key] = timeoutValue + }; + + Reload(content); + await Task.Delay(5000); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineProviderTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineProviderTest.cs new file mode 100644 index 0000000000..a9f432318e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/Internal/ResiliencePipelineProviderTest.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Resilience.Internal; +using Moq; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Internal.Test; + +public sealed partial class ResiliencePipelineProviderTest +{ + private readonly Mock _factory = new(MockBehavior.Strict); + private readonly ResiliencePipelineProvider _provider; + + public ResiliencePipelineProviderTest() + { + _provider = new ResiliencePipelineProvider(_factory.Object); + } + + [Fact] + public void GetPipeline_ArgumentValidation() + { + Assert.Throws(() => _provider.GetPipeline(null!)); + Assert.Throws(() => _provider.GetPipeline(string.Empty)); + + Assert.Throws(() => _provider.GetPipeline(null!, "key")); + Assert.Throws(() => _provider.GetPipeline(string.Empty, "key")); + + Assert.Throws(() => _provider.GetPipeline(string.Empty)); + + Assert.Throws(() => _provider.GetPipeline("name", null!)); + Assert.Throws(() => _provider.GetPipeline("name", string.Empty)); + } + + [Fact] + public void GetPipeline_EnsureCached() + { + _factory.Setup(v => v.CreatePipeline("test", string.Empty)).Returns(() => Policy.NoOpAsync()).Verifiable(); + + Assert.Same(_provider.GetPipeline("test"), _provider.GetPipeline("test")); + + _factory.VerifyAll(); + } + + [Fact] + public void GetPipeline_ByDifferentKeys_EnsureNotSame() + { + var calls = 0; + + _factory.Setup(v => v.CreatePipeline("test", "A")).Callback(() => calls++).Returns(() => Policy.NoOpAsync()).Verifiable(); + _factory.Setup(v => v.CreatePipeline("test", "B")).Callback(() => calls++).Returns(() => Policy.NoOpAsync()).Verifiable(); + + Assert.NotSame(_provider.GetPipeline("test", "A"), _provider.GetPipeline("test", "B")); + Assert.Same(_provider.GetPipeline("test", "A"), _provider.GetPipeline("test", "A")); + Assert.Same(_provider.GetPipeline("test", "B"), _provider.GetPipeline("test", "B")); + + Assert.Equal(2, calls); + + _factory.VerifyAll(); + } + + [Fact] + public void GetPipeline_SamePolicyNameDifferentTypes_EnsureDistinctPolicyInstances() + { + _factory.Setup(v => v.CreatePipeline("test", string.Empty)).Returns(() => Policy.NoOpAsync()).Verifiable(); + _factory.Setup(v => v.CreatePipeline("test", string.Empty)).Returns(() => Policy.NoOpAsync()).Verifiable(); + + Assert.NotSame(_provider.GetPipeline("test"), _provider.GetPipeline("test")); + + _factory.VerifyAll(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResilienceFakeClockTestsCollection.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResilienceFakeClockTestsCollection.cs new file mode 100644 index 0000000000..b9251d2678 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResilienceFakeClockTestsCollection.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Resilience.Test; + +[CollectionDefinition(nameof(ResilienceFakeClockTestsCollection), DisableParallelization = true)] +public class ResilienceFakeClockTestsCollection +{ +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.BulkheadT.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.BulkheadT.cs new file mode 100644 index 0000000000..9353de3d37 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.BulkheadT.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Extensions.Resilience.Test.Helpers; +using Microsoft.Shared.Text; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Test; + +public partial class ResiliencePipelineBuilderExtensionsTest +{ + [Fact] + public void AddBulkheadPolicy_EnsureValidation() + { + _builder.AddBulkheadPolicy(PolicyName, options => options.MaxQueuedActions = -1); + + Assert.Throws(() => CreatePipeline()); + } + + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddBulkheadPolicy_NullBuilder_Throws(MethodArgs mode) + { + IResiliencePipelineBuilder builder = null!; + + Assert.Throws(() => AddBulkheadPolicy(mode, builder, "dummy", EmptyConfiguration, options => { })); + } + + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddBulkheadPolicy_NullOrEmptyPolicyName_Throws(MethodArgs mode) + { + Assert.Throws(() => AddBulkheadPolicy(mode, _builder, null!, EmptyConfiguration, options => { })); + Assert.Throws(() => AddBulkheadPolicy(mode, _builder, string.Empty, EmptyConfiguration, options => { })); + } + + [MemberData(nameof(ConfigureMethodCombinations))] + [Theory] + public void AddBulkheadPolicy_NullConfigureMethod_Throws(MethodArgs mode) + { + Assert.Throws(() => AddBulkheadPolicy(mode, _builder, "dummy", EmptyConfiguration, null!)); + + } + + [MemberData(nameof(ConfigurationCombinations))] + [Theory] + public void AddBulkheadPolicy_NullConfiguration_Throws(MethodArgs mode) + { + Assert.Throws(() => AddBulkheadPolicy(mode, _builder, "dummy", null!, options => { })); + + } + + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddBulkheadPolicy_Ok(MethodArgs mode) + { + // arrange + var expectedQueuedActions = 13; + var expectedConcurrency = 48; + var configuration = CreateConfiguration("MaxQueuedActions", expectedQueuedActions.ToInvariantString()); + var optionsName = OptionsNameHelper.GetPolicyOptionsName(SupportedPolicies.BulkheadPolicy, DefaultPipelineName, PolicyName); + + AddBulkheadPolicy(mode, _builder, PolicyName, configuration, options => options.MaxConcurrency = expectedConcurrency); + + var serviceProvider = _builder.Services.BuildServiceProvider(); + var factory = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>().Get(optionsName); + + _pipelineBuilder.Setup(v => v.AddBulkheadPolicy(PolicyName, options)).Returns(_pipelineBuilder.Object).Verifiable(); + _pipelineBuilder.Setup(v => v.Build()).Returns(Policy.NoOpAsync()).Verifiable(); + + // act + factory.CreatePipeline(DefaultPipelineName, DefaultPipelineKey); + + // assert + _pipelineBuilder.VerifyAll(); + + AssertOptions(options, o => o.MaxQueuedActions, expectedQueuedActions, mode.HasFlag(MethodArgs.Configuration)); + AssertOptions(options, o => o.MaxConcurrency, expectedConcurrency, mode.HasFlag(MethodArgs.ConfigureMethod)); + } + + private static void AddBulkheadPolicy( + MethodArgs mode, + IResiliencePipelineBuilder builder, + string policyName, + IConfigurationSection configuration, + Action configureMethod) + { + _ = mode switch + { + MethodArgs.None => builder.AddBulkheadPolicy(policyName), + MethodArgs.Configuration => builder.AddBulkheadPolicy(policyName, configuration), + MethodArgs.ConfigureMethod => builder.AddBulkheadPolicy(policyName, configureMethod), + MethodArgs.Configuration | MethodArgs.ConfigureMethod => builder.AddBulkheadPolicy(policyName, configuration, configureMethod), + _ => throw new NotSupportedException() + }; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.CircuitBreakerT.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.CircuitBreakerT.cs new file mode 100644 index 0000000000..a22685dc2d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.CircuitBreakerT.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Extensions.Resilience.Test.Helpers; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Test; + +public partial class ResiliencePipelineBuilderExtensionsTest +{ + [Fact] + public void AddCircuitBreakerPolicy_EnsureValidation() + { + _builder.AddCircuitBreakerPolicy(PolicyName, options => options.BreakDuration = TimeSpan.MinValue); + + Assert.Throws(() => CreatePipeline()); + } + + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddCircuitBreakerPolicy_NullBuilder_Throws(MethodArgs mode) + { + IResiliencePipelineBuilder builder = null!; + + Assert.Throws(() => AddCircuitBreakerPolicy(mode, builder, "dummy", EmptyConfiguration, options => { })); + } + + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddCircuitBreakerPolicy_NullOrEmptyPolicyName_Throws(MethodArgs mode) + { + Assert.Throws(() => AddCircuitBreakerPolicy(mode, _builder, null!, EmptyConfiguration, options => { })); + Assert.Throws(() => AddCircuitBreakerPolicy(mode, _builder, string.Empty, EmptyConfiguration, options => { })); + } + + [MemberData(nameof(ConfigureMethodCombinations))] + [Theory] + public void AddCircuitBreakerPolicy_NullConfigureMethod_Throws(MethodArgs mode) + { + Assert.Throws(() => AddCircuitBreakerPolicy(mode, _builder, "dummy", EmptyConfiguration, null!)); + + } + + [MemberData(nameof(ConfigurationCombinations))] + [Theory] + public void AddCircuitBreakerPolicy_NullConfiguration_Throws(MethodArgs mode) + { + Assert.Throws(() => AddCircuitBreakerPolicy(mode, _builder, "dummy", null!, options => { })); + + } + + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddCircuitBreakerPolicy_Ok(MethodArgs mode) + { + // arrange + var expectedFailureThreshold = 0.001; + var expectedMinimumThroughput = 8; + var configuration = CreateConfiguration("FailureThreshold", expectedFailureThreshold.ToString(CultureInfo.InvariantCulture)); + var optionsName = OptionsNameHelper.GetPolicyOptionsName(SupportedPolicies.CircuitBreaker, DefaultPipelineName, PolicyName); + + AddCircuitBreakerPolicy(mode, _builder, PolicyName, configuration, options => options.MinimumThroughput = expectedMinimumThroughput); + + var serviceProvider = _builder.Services.BuildServiceProvider(); + var factory = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>>().Get(optionsName); + + _pipelineBuilder.Setup(v => v.AddCircuitBreakerPolicy(PolicyName, options)).Returns(_pipelineBuilder.Object).Verifiable(); + _pipelineBuilder.Setup(v => v.Build()).Returns(Policy.NoOpAsync()).Verifiable(); + + // act + factory.CreatePipeline(DefaultPipelineName, DefaultPipelineKey); + + // assert + _pipelineBuilder.VerifyAll(); + + AssertOptions(options, o => o.FailureThreshold, expectedFailureThreshold, mode.HasFlag(MethodArgs.Configuration)); + AssertOptions(options, o => o.MinimumThroughput, expectedMinimumThroughput, mode.HasFlag(MethodArgs.ConfigureMethod)); + } + + private static void AddCircuitBreakerPolicy( + MethodArgs mode, + IResiliencePipelineBuilder builder, + string policyName, + IConfigurationSection configuration, + Action> configureMethod) + { + _ = mode switch + { + MethodArgs.None => builder.AddCircuitBreakerPolicy(policyName), + MethodArgs.Configuration => builder.AddCircuitBreakerPolicy(policyName, configuration), + MethodArgs.ConfigureMethod => builder.AddCircuitBreakerPolicy(policyName, configureMethod), + MethodArgs.Configuration | MethodArgs.ConfigureMethod => builder.AddCircuitBreakerPolicy(policyName, configuration, configureMethod), + _ => throw new NotSupportedException() + }; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.FallbackT.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.FallbackT.cs new file mode 100644 index 0000000000..9888294ad9 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.FallbackT.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Extensions.Resilience.Test.Helpers; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Test; + +public partial class ResiliencePipelineBuilderExtensionsTest +{ + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddFallbackPolicy_NullBuilder_Throws(MethodArgs mode) + { + IResiliencePipelineBuilder builder = null!; + + Assert.Throws(() => AddFallbackPolicy(mode, builder, "dummy", FallbackTask, EmptyConfiguration, options => { })); + } + + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddFallbackPolicy_NullOrEmptyPolicyName_Throws(MethodArgs mode) + { + Assert.Throws(() => AddFallbackPolicy(mode, _builder, null!, FallbackTask, EmptyConfiguration, options => { })); + Assert.Throws(() => AddFallbackPolicy(mode, _builder, string.Empty, FallbackTask, EmptyConfiguration, options => { })); + } + + [MemberData(nameof(ConfigureMethodCombinations))] + [Theory] + public void AddFallbackPolicy_NullConfigureMethod_Throws(MethodArgs mode) + { + Assert.Throws(() => AddFallbackPolicy(mode, _builder, "dummy", FallbackTask, EmptyConfiguration, null!)); + + } + + [MemberData(nameof(ConfigurationCombinations))] + [Theory] + public void AddFallbackPolicy_NullConfiguration_Throws(MethodArgs mode) + { + Assert.Throws(() => AddFallbackPolicy(mode, _builder, "dummy", FallbackTask, null!, options => { })); + + } + + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddFallbackPolicy_NullFallbackScenarioTask_Throws(MethodArgs mode) + { + Assert.Throws(() => AddFallbackPolicy(mode, _builder, "dummy", null!, EmptyConfiguration, o => { })); + + } + + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddFallbackPolicy_Ok(MethodArgs mode) + { + // arrange + var expectedOnFallback = OnFallback; + var configuration = CreateEmptyConfiguration(); + var optionsName = OptionsNameHelper.GetPolicyOptionsName(SupportedPolicies.FallbackPolicy, DefaultPipelineName, PolicyName); + + AddFallbackPolicy(mode, _builder, PolicyName, FallbackTask, configuration, options => options.OnFallbackAsync = expectedOnFallback); + + var serviceProvider = _builder.Services.BuildServiceProvider(); + var factory = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>>().Get(optionsName); + + _pipelineBuilder.Setup(v => v.AddFallbackPolicy(PolicyName, FallbackTask, options)).Returns(_pipelineBuilder.Object).Verifiable(); + _pipelineBuilder.Setup(v => v.Build()).Returns(Policy.NoOpAsync()).Verifiable(); + + // act + factory.CreatePipeline(DefaultPipelineName, DefaultPipelineKey); + + // assert + _pipelineBuilder.VerifyAll(); + + AssertOptions(options, o => o.OnFallbackAsync, expectedOnFallback, mode.HasFlag(MethodArgs.ConfigureMethod)); + + static Task OnFallback(FallbackTaskArguments args) => Task.FromResult(string.Empty); + } + + private static void AddFallbackPolicy( + MethodArgs mode, + IResiliencePipelineBuilder builder, + string policyName, + FallbackScenarioTaskProvider provider, + IConfigurationSection configuration, + Action> configureMethod) + { + _ = mode switch + { + MethodArgs.None => builder.AddFallbackPolicy(policyName, provider), + MethodArgs.Configuration => builder.AddFallbackPolicy(policyName, provider, configuration), + MethodArgs.ConfigureMethod => builder.AddFallbackPolicy(policyName, provider, configureMethod), + MethodArgs.Configuration | MethodArgs.ConfigureMethod => builder.AddFallbackPolicy(policyName, provider, configuration, configureMethod), + _ => throw new NotSupportedException() + }; + } + + private static Task FallbackTask(FallbackScenarioTaskArguments args) => Task.FromResult(string.Empty); +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.HedgingT.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.HedgingT.cs new file mode 100644 index 0000000000..f62a74e778 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.HedgingT.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Extensions.Resilience.Test.Helpers; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Test; + +public partial class ResiliencePipelineBuilderExtensionsTest +{ + [Fact] + public void AddHedgingPolicy_EnsureValidation() + { + _builder.AddHedgingPolicy(PolicyName, HedgedTaskProvider, options => options.MaxHedgedAttempts = -1); + + Assert.Throws(() => CreatePipeline()); + } + + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddHedgingPolicy_NullBuilder_Throws(MethodArgs mode) + { + IResiliencePipelineBuilder builder = null!; + + Assert.Throws(() => AddHedgingPolicy(mode, builder, "dummy", HedgedTaskProvider, EmptyConfiguration, options => { })); + } + + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddHedgingPolicy_NullOrEmptyPolicyName_Throws(MethodArgs mode) + { + Assert.Throws(() => AddHedgingPolicy(mode, _builder, null!, HedgedTaskProvider, EmptyConfiguration, options => { })); + Assert.Throws(() => AddHedgingPolicy(mode, _builder, string.Empty, HedgedTaskProvider, EmptyConfiguration, options => { })); + } + + [MemberData(nameof(ConfigureMethodCombinations))] + [Theory] + public void AddHedgingPolicy_NullConfigureMethod_Throws(MethodArgs mode) + { + Assert.Throws(() => AddHedgingPolicy(mode, _builder, "dummy", HedgedTaskProvider, EmptyConfiguration, null!)); + + } + + [MemberData(nameof(ConfigurationCombinations))] + [Theory] + public void AddHedgingPolicy_NullConfiguration_Throws(MethodArgs mode) + { + Assert.Throws(() => AddHedgingPolicy(mode, _builder, "dummy", HedgedTaskProvider, null!, options => { })); + + } + + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddHedgingPolicy_NullHedgedTaskProvider_Throws(MethodArgs mode) + { + Assert.Throws(() => AddHedgingPolicy(mode, _builder, "dummy", null!, EmptyConfiguration, o => { })); + + } + + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddHedgingPolicy_Ok(MethodArgs mode) + { + // arrange + var expectedHedgingDelay = TimeSpan.FromMilliseconds(123); + var expectedMaxHedgedAttempts = 8; + var configuration = CreateConfiguration("HedgingDelay", expectedHedgingDelay.ToString()); + var optionsName = OptionsNameHelper.GetPolicyOptionsName(SupportedPolicies.HedgingPolicy, DefaultPipelineName, PolicyName); + + AddHedgingPolicy(mode, _builder, PolicyName, HedgedTaskProvider, configuration, options => options.MaxHedgedAttempts = expectedMaxHedgedAttempts); + + var serviceProvider = _builder.Services.BuildServiceProvider(); + var factory = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>>().Get(optionsName); + + _pipelineBuilder.Setup(v => v.AddHedgingPolicy(PolicyName, HedgedTaskProvider, options)).Returns(_pipelineBuilder.Object).Verifiable(); + _pipelineBuilder.Setup(v => v.Build()).Returns(Policy.NoOpAsync()).Verifiable(); + + // act + factory.CreatePipeline(DefaultPipelineName, DefaultPipelineKey); + + // assert + _pipelineBuilder.VerifyAll(); + + AssertOptions(options, o => o.HedgingDelay, expectedHedgingDelay, mode.HasFlag(MethodArgs.Configuration)); + AssertOptions(options, o => o.MaxHedgedAttempts, expectedMaxHedgedAttempts, mode.HasFlag(MethodArgs.ConfigureMethod)); + } + + private static void AddHedgingPolicy( + MethodArgs mode, + IResiliencePipelineBuilder builder, + string policyName, + HedgedTaskProvider provider, + IConfigurationSection configuration, + Action> configureMethod) + { + _ = mode switch + { + MethodArgs.None => builder.AddHedgingPolicy(policyName, provider), + MethodArgs.Configuration => builder.AddHedgingPolicy(policyName, provider, configuration), + MethodArgs.ConfigureMethod => builder.AddHedgingPolicy(policyName, provider, configureMethod), + MethodArgs.Configuration | MethodArgs.ConfigureMethod => builder.AddHedgingPolicy(policyName, provider, configuration, configureMethod), + _ => throw new NotSupportedException() + }; + } + + private static bool HedgedTaskProvider(HedgingTaskProviderArguments args, out Task? task) + { + task = Task.FromResult(string.Empty); + return true; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.RetryT.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.RetryT.cs new file mode 100644 index 0000000000..d804910435 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.RetryT.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Extensions.Resilience.Test.Helpers; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Test; + +public partial class ResiliencePipelineBuilderExtensionsTest +{ + [Fact] + public void AddRetryPolicy_EnsureValidation() + { + _builder.AddRetryPolicy(PolicyName, options => options.RetryCount = -2); + + Assert.Throws(() => CreatePipeline()); + } + + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddRetryPolicy_NullBuilder_Throws(MethodArgs mode) + { + IResiliencePipelineBuilder builder = null!; + + Assert.Throws(() => AddRetryPolicy(mode, builder, "dummy", EmptyConfiguration, options => { })); + } + + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddRetryPolicy_NullOrEmptyPolicyName_Throws(MethodArgs mode) + { + Assert.Throws(() => AddRetryPolicy(mode, _builder, null!, EmptyConfiguration, options => { })); + Assert.Throws(() => AddRetryPolicy(mode, _builder, string.Empty, EmptyConfiguration, options => { })); + } + + [MemberData(nameof(ConfigureMethodCombinations))] + [Theory] + public void AddRetryPolicy_NullConfigureMethod_Throws(MethodArgs mode) + { + Assert.Throws(() => AddRetryPolicy(mode, _builder, "dummy", EmptyConfiguration, null!)); + + } + + [MemberData(nameof(ConfigurationCombinations))] + [Theory] + public void AddRetryPolicy_NullConfiguration_Throws(MethodArgs mode) + { + Assert.Throws(() => AddRetryPolicy(mode, _builder, "dummy", null!, options => { })); + + } + + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddRetryPolicy_Ok(MethodArgs mode) + { + // arrange + var expectedBaseDelay = TimeSpan.FromMilliseconds(123); + var expectedRetryCount = 8; + var configuration = CreateConfiguration("BaseDelay", expectedBaseDelay.ToString()); + var optionsName = OptionsNameHelper.GetPolicyOptionsName(SupportedPolicies.RetryPolicy, DefaultPipelineName, PolicyName); + + AddRetryPolicy(mode, _builder, PolicyName, configuration, options => options.RetryCount = expectedRetryCount); + + var serviceProvider = _builder.Services.BuildServiceProvider(); + var factory = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>>().Get(optionsName); + + _pipelineBuilder.Setup(v => v.AddRetryPolicy(PolicyName, options)).Returns(_pipelineBuilder.Object).Verifiable(); + _pipelineBuilder.Setup(v => v.Build()).Returns(Policy.NoOpAsync()).Verifiable(); + + // act + factory.CreatePipeline(DefaultPipelineName, DefaultPipelineKey); + + // assert + _pipelineBuilder.VerifyAll(); + + AssertOptions(options, o => o.BaseDelay, expectedBaseDelay, mode.HasFlag(MethodArgs.Configuration)); + AssertOptions(options, o => o.RetryCount, expectedRetryCount, mode.HasFlag(MethodArgs.ConfigureMethod)); + } + + private static void AddRetryPolicy( + MethodArgs mode, + IResiliencePipelineBuilder builder, + string policyName, + IConfigurationSection configuration, + Action> configureMethod) + { + _ = mode switch + { + MethodArgs.None => builder.AddRetryPolicy(policyName), + MethodArgs.Configuration => builder.AddRetryPolicy(policyName, configuration), + MethodArgs.ConfigureMethod => builder.AddRetryPolicy(policyName, configureMethod), + MethodArgs.Configuration | MethodArgs.ConfigureMethod => builder.AddRetryPolicy(policyName, configuration, configureMethod), + _ => throw new NotSupportedException() + }; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.TimeoutT.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.TimeoutT.cs new file mode 100644 index 0000000000..ffaf7fbb60 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.TimeoutT.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Options; +using Microsoft.Extensions.Resilience.Test.Helpers; +using Polly; +using Polly.Timeout; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Test; + +public partial class ResiliencePipelineBuilderExtensionsTest +{ + [Fact] + public void AddTimeoutPolicy_EnsureValidation() + { + _builder.AddTimeoutPolicy(PolicyName, options => options.TimeoutInterval = TimeSpan.MinValue); + + Assert.Throws(() => CreatePipeline()); + } + + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddTimeoutPolicy_NullBuilder_Throws(MethodArgs mode) + { + IResiliencePipelineBuilder builder = null!; + + Assert.Throws(() => AddTimeoutPolicy(mode, builder, "dummy", EmptyConfiguration, options => { })); + } + + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddTimeoutPolicy_NullOrEmptyPolicyName_Throws(MethodArgs mode) + { + Assert.Throws(() => AddTimeoutPolicy(mode, _builder, null!, EmptyConfiguration, options => { })); + Assert.Throws(() => AddTimeoutPolicy(mode, _builder, string.Empty, EmptyConfiguration, options => { })); + } + + [MemberData(nameof(ConfigureMethodCombinations))] + [Theory] + public void AddTimeoutPolicy_NullConfigureMethod_Throws(MethodArgs mode) + { + Assert.Throws(() => AddTimeoutPolicy(mode, _builder, "dummy", EmptyConfiguration, null!)); + + } + + [MemberData(nameof(ConfigurationCombinations))] + [Theory] + public void AddTimeoutPolicy_NullConfiguration_Throws(MethodArgs mode) + { + Assert.Throws(() => AddTimeoutPolicy(mode, _builder, "dummy", null!, options => { })); + + } + + [MemberData(nameof(AllCombinations))] + [Theory] + public void AddTimeoutPolicy_Ok(MethodArgs mode) + { + // arrange + var expectedTimeoutInterval = TimeSpan.FromMilliseconds(123); + var expectedTimeoutStrategy = TimeoutStrategy.Pessimistic; + var configuration = CreateConfiguration("TimeoutInterval", expectedTimeoutInterval.ToString()); + var optionsName = OptionsNameHelper.GetPolicyOptionsName(SupportedPolicies.TimeoutPolicy, DefaultPipelineName, PolicyName); + + AddTimeoutPolicy(mode, _builder, PolicyName, configuration, options => options.TimeoutStrategy = expectedTimeoutStrategy); + + var serviceProvider = _builder.Services.BuildServiceProvider(); + var factory = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>().Get(optionsName); + + _pipelineBuilder.Setup(v => v.AddTimeoutPolicy(PolicyName, options)).Returns(_pipelineBuilder.Object).Verifiable(); + _pipelineBuilder.Setup(v => v.Build()).Returns(Policy.NoOpAsync()).Verifiable(); + + // act + factory.CreatePipeline(DefaultPipelineName, DefaultPipelineKey); + + // assert + _pipelineBuilder.VerifyAll(); + + AssertOptions(options, o => o.TimeoutInterval, expectedTimeoutInterval, mode.HasFlag(MethodArgs.Configuration)); + AssertOptions(options, o => o.TimeoutStrategy, expectedTimeoutStrategy, mode.HasFlag(MethodArgs.ConfigureMethod)); + } + + private static void AddTimeoutPolicy( + MethodArgs mode, + IResiliencePipelineBuilder builder, + string policyName, + IConfigurationSection configuration, + Action configureMethod) + { + _ = mode switch + { + MethodArgs.None => builder.AddTimeoutPolicy(policyName), + MethodArgs.Configuration => builder.AddTimeoutPolicy(policyName, configuration), + MethodArgs.ConfigureMethod => builder.AddTimeoutPolicy(policyName, configureMethod), + MethodArgs.Configuration | MethodArgs.ConfigureMethod => builder.AddTimeoutPolicy(policyName, configuration, configureMethod), + _ => throw new NotSupportedException() + }; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.cs new file mode 100644 index 0000000000..200f67e7be --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderExtensionsTest.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Test.Helpers; +using Moq; + +namespace Microsoft.Extensions.Resilience.Test; + +public partial class ResiliencePipelineBuilderExtensionsTest : ResilienceTestHelper +{ + private readonly IResiliencePipelineBuilder _builder; + private readonly Mock> _pipelineBuilder = new(MockBehavior.Strict); + + public ResiliencePipelineBuilderExtensionsTest() + { + Services.TryAddSingleton(_pipelineBuilder.Object); + + _builder = Services.AddResiliencePipeline(DefaultPipelineName); + _pipelineBuilder.Setup(b => b.Initialize(It.Is(v => v.PipelineName == DefaultPipelineName && v.PipelineKey == DefaultPipelineKey))); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderTest.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderTest.cs new file mode 100644 index 0000000000..df0a04e772 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResiliencePipelineBuilderTest.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Resilience.Internal; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Test; + +public sealed class ResiliencePipelineBuilderTest +{ + [Fact] + public void Constructor_NullArgument_ShouldThrow() + { + Assert.Throws(() => new ResiliencePipelineBuilder(null!, "test")); + Assert.Throws(() => new ResiliencePipelineBuilder(null!, string.Empty)); + Assert.Throws(() => new ResiliencePipelineBuilder(new ServiceCollection(), null!)); + Assert.Throws(() => new ResiliencePipelineBuilder(new ServiceCollection(), string.Empty)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ServiceCollectionExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000000..2896df2464 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Resilience.Test.Helpers; +using Microsoft.Extensions.Telemetry.Metering; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Resilience.Test; + +public class ServiceCollectionExtensionsTests : ResilienceTestHelper +{ + private readonly Mock> _pipelineBuilder = new(MockBehavior.Strict); + + public ServiceCollectionExtensionsTests() + { + Services.TryAddSingleton(_pipelineBuilder.Object); + } + + [Fact] + public void AddResiliencePipeline_EnsureValidationAndCorrectResult() + { + var services = new ServiceCollection(); + + Assert.Throws(() => services.AddResiliencePipeline(null!)); + Assert.Throws(() => ServiceCollectionExtensions.AddResiliencePipeline(null!, "pipelineName")); + + var result = services.AddResiliencePipeline("test"); + + Assert.NotNull(result.Services.FirstOrDefault(s => s.ServiceType == typeof(IResiliencePipelineFactory))); + Assert.Equal("test", result.PipelineName); + Assert.Equal(services, result.Services); + } + + [Fact] + public void AddResiliencePipeline_EnsureNecessaryServicesAdded() + { + var services = new ServiceCollection(); + services.AddLogging().RegisterMetering().AddSingleton(System.TimeProvider.System); + services.AddResiliencePipeline("test"); + services.AddResiliencePipeline("test"); + + var provider = services.BuildServiceProvider(); + + provider.GetRequiredService(); + + // string based + provider.GetRequiredService(); + provider.GetRequiredService>(); + Assert.NotEqual(provider.GetRequiredService>(), provider.GetRequiredService>()); + + // bool based + provider.GetRequiredService(); + provider.GetRequiredService>(); + Assert.NotEqual(provider.GetRequiredService>(), provider.GetRequiredService>()); + + Assert.NotEqual(provider.GetRequiredService(), provider.GetRequiredService()); + } + + [Fact] + public void AddResiliencePipeline_EnsureAllOptionsAutomaticallyValidated() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.RegisterMetering(); + + Assert.Throws(() => services.AddResiliencePipeline(null!)); + Assert.Throws(() => ServiceCollectionExtensions.AddResiliencePipeline(null!, "pipelineName")); + + var result = services.AddResiliencePipeline("test"); + var factory = services.BuildServiceProvider().GetRequiredService(); + + Assert.Throws(() => factory.CreatePipeline("test", string.Empty)); + var error = Assert.Throws(() => factory.CreatePipeline("not-configured", string.Empty)); +#if NETCOREAPP + Assert.Equal("BuilderActions: This resilience pipeline is not configured. Each resilience pipeline must include at least one policy. Field path: not-configured.BuilderActions", error.Message); +#endif + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/configs/appsettings.json b/test/Libraries/Microsoft.Extensions.Resilience.Tests/configs/appsettings.json new file mode 100644 index 0000000000..1dc54f3166 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/configs/appsettings.json @@ -0,0 +1,80 @@ +{ + "ChaosPolicyConfigurations": { + "ChaosPolicyOptionsGroups": { + "OptionsGroupTest": { + "HttpResponseInjectionPolicyOptions": { + "Enabled": false + }, + "ExceptionPolicyOptions": { + "Enabled": false + }, + "LatencyPolicyOptions": { + "Enabled": true + } + } + } + }, + "ChaosPolicyOptionsGroupsNegativeTest1": { + "ChaosPolicyOptionsGroups": { + "OptionsGroupTest": { + "HttpResponseInjectionPolicyOptions": { + "Enabled": true, + "FaultInjectionRate": 2 + } + } + } + }, + "ChaosPolicyOptionsGroupsNegativeTest2": { + "ChaosPolicyOptionsGroups": { + "OptionsGroupTest": { + "ExceptionPolicyOptions": { + "Enabled": true, + "FaultInjectionRate": 2 + } + } + } + }, + "ChaosPolicyOptionsGroupsNegativeTest3": { + "ChaosPolicyOptionsGroups": { + "OptionsGroupTest": { + "LatencyPolicyOptions": { + "Enabled": true, + "FaultInjectionRate": 2 + } + } + } + }, + "ChaosPolicyOptionsGroupsNegativeTest4": { + "ChaosPolicyOptionsGroups": { + "OptionsGroupTest": { + "LatencyPolicyOptions": { + "Enabled": true, + "Latency": "00:30:00" + } + } + } + }, + "ChaosPolicyOptionsGroupsNegativeTest5": { + "ChaosPolicyOptionsGroups": { + "OptionsGroupTest": { + "HttpResponseInjectionPolicyOptions": { + "Enabled": true, + "StatusCode": 123 + } + } + } + }, + "ChaosPolicyOptionsGroupsNegativeTestMultipleErrors": { + "ChaosPolicyOptionsGroups": { + "OptionsGroupTest": { + "LatencyPolicyOptions": { + "Enabled": true, + "FaultInjectionRate": 2, + "Latency": "00:30:00" + } + } + } + }, + "ChaosPolicyOptionsGroupsTestNoOptionsGroup": { + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/configs/optionsOnChangeTestNew.json b/test/Libraries/Microsoft.Extensions.Resilience.Tests/configs/optionsOnChangeTestNew.json new file mode 100644 index 0000000000..c0e5d718f6 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/configs/optionsOnChangeTestNew.json @@ -0,0 +1,12 @@ +{ + "ChaosPolicyConfigurations": { + "ReloadOnChange": true, + "ChaosPolicyOptionsGroups": { + "OptionsGroupTest": { + "LatencyPolicyOptions": { + "Enabled": false + } + } + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/configs/optionsOnChangeTestOriginal.json b/test/Libraries/Microsoft.Extensions.Resilience.Tests/configs/optionsOnChangeTestOriginal.json new file mode 100644 index 0000000000..254ee0f4b6 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/configs/optionsOnChangeTestOriginal.json @@ -0,0 +1,12 @@ +{ + "ChaosPolicyConfigurations": { + "ReloadOnChange": true, + "ChaosPolicyOptionsGroups": { + "OptionsGroupTest": { + "LatencyPolicyOptions": { + "Enabled": true + } + } + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Enrichment/EnricherExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Enrichment/EnricherExtensionsTests.cs new file mode 100644 index 0000000000..1b70b6c2f4 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Enrichment/EnricherExtensionsTests.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Enrichment.Test; + +public class EnricherExtensionsTests +{ + [Fact] + public void CreateLoggerWithNullEnricher() + { + // Assert + Assert.Throws(() => new ServiceCollection().AddLogEnricher(null!)); + } + + [Fact] + public void EnrichmentLoggingBuilder_GivenNullArguments_Throws() + { + // Arrange + var enricher = new EmptyEnricher(); + + // Act & Assert + Assert.Throws(() => + ((IServiceCollection)null!).AddLogEnricher(enricher)); + + Assert.Throws(() => + new ServiceCollection().AddLogEnricher(null!)); + } + + [Fact] + public void ServiceCollection_GivenNullArguments_Throws() + { + Assert.Throws(() => + ((IServiceCollection)null!).AddMetricEnricher()); + + Assert.Throws(() => + ((IServiceCollection)null!).AddMetricEnricher(new EmptyEnricher())); + + Assert.Throws(() => + new ServiceCollection() + .AddMetricEnricher(null!)); + } + + [Fact] + public void ServiceCollection_AddMultipleEnrichersSuccessfully() + { + var services = new ServiceCollection(); + services.AddMetricEnricher(); + services.AddMetricEnricher(new TestEnricher()); + + using var provider = services.BuildServiceProvider(); + var enrichersCollection = provider.GetServices(); + + var enricherCount = 0; + foreach (var enricher in enrichersCollection) + { + enricherCount++; + } + + Assert.Equal(2, enricherCount); + } + + internal class EmptyEnricher : IMetricEnricher, ILogEnricher + { + public void Enrich(IEnrichmentPropertyBag enrichmentBag) + { + // intentionally left empty + } + } + + internal class TestEnricher : IMetricEnricher, ILogEnricher + { + public void Enrich(IEnrichmentPropertyBag enrichmentBag) + { + enrichmentBag.Add("testKey", "testValue"); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Enrichment/TestLogEnrichmentPropertyBag.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Enrichment/TestLogEnrichmentPropertyBag.cs new file mode 100644 index 0000000000..eef49f7006 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Enrichment/TestLogEnrichmentPropertyBag.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Telemetry.Enrichment.Test; + +public class TestLogEnrichmentPropertyBag : IEnrichmentPropertyBag +{ + private readonly Dictionary _properties = new(); + + public TestLogEnrichmentPropertyBag(IEnumerable>? input = null) + { + if (input != null) + { + foreach (var kvp in input) + { + _properties.Add(kvp.Key, kvp.Value); + } + } + } + + public IReadOnlyDictionary Properties => _properties; + + public void Add(string key, object value) + { + _properties.Add(key, value); + } + + public void Add(string key, string value) + { + _properties.Add(key, value); + } + + public void Add(ReadOnlySpan> properties) + { + foreach (var p in properties) + { + _properties.Add(p.Key, p.Value); + } + } + + public void Add(ReadOnlySpan> properties) + { + foreach (var p in properties) + { + _properties.Add(p.Key, p.Value); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Enrichment/TestMetricEnrichmentPropertyBag.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Enrichment/TestMetricEnrichmentPropertyBag.cs new file mode 100644 index 0000000000..2796acfd82 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Enrichment/TestMetricEnrichmentPropertyBag.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Telemetry.Enrichment.Test; + +public class TestMetricEnrichmentPropertyBag : IEnrichmentPropertyBag +{ + private readonly Dictionary _properties = new(); + + public TestMetricEnrichmentPropertyBag(IEnumerable>? input = null) + { + if (input != null) + { + foreach (var kvp in input) + { + _properties.Add(kvp.Key, kvp.Value.ToString() ?? string.Empty); + } + } + } + + public IReadOnlyDictionary Properties => _properties; + + public void Add(string key, object value) + { + _properties.Add(key, value.ToString() ?? string.Empty); + } + + public void Add(string key, string value) + { + _properties.Add(key, value); + } + + public void Add(ReadOnlySpan> properties) + { + foreach (var p in properties) + { + _properties.Add(p.Key, p.Value.ToString() ?? string.Empty); + } + } + + public void Add(ReadOnlySpan> properties) + { + foreach (var p in properties) + { + _properties.Add(p.Key, p.Value); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Http/AbstractionTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Http/AbstractionTests.cs new file mode 100644 index 0000000000..b9334596b1 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Http/AbstractionTests.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Http.Telemetry.Test; + +public class AbstractionTests +{ + [Fact] + public void RequestMetadata_DefaultConsrtuctor_HasDefaultValues() + { + var requestMetadata = new RequestMetadata(); + + Assert.Equal("GET", requestMetadata.MethodType); + Assert.Equal(TelemetryConstants.Unknown, requestMetadata.DependencyName); + Assert.Equal(TelemetryConstants.Unknown, requestMetadata.RequestName); + Assert.Equal(TelemetryConstants.Unknown, requestMetadata.RequestRoute); + } + + [Fact] + public void RequestMetadata_ParameterizedConsrtuctor_HasProvidedValues() + { + var requestMetadata = new RequestMetadata("POST", "/v1/temp/route/{routeId}", "TestRequest") + { + DependencyName = "MyDependency" + }; + + Assert.Equal("POST", requestMetadata.MethodType); + Assert.Equal("MyDependency", requestMetadata.DependencyName); + Assert.Equal("TestRequest", requestMetadata.RequestName); + Assert.Equal("/v1/temp/route/{routeId}", requestMetadata.RequestRoute); + } + + [Fact] + public void Ensure_TelemetryConstantValuesAreNotChanged() + { + Assert.Equal("R9-RequestMetadata", TelemetryConstants.RequestMetadataKey); + Assert.Equal("unknown", TelemetryConstants.Unknown); + Assert.Equal("REDACTED", TelemetryConstants.Redacted); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/CheckpointTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/CheckpointTest.cs new file mode 100644 index 0000000000..28e4fdfa10 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/CheckpointTest.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Latency.Test; + +public class CheckpointTest +{ + [Fact] + public void Checkpoint_BasicTest() + { + string name = "Name"; + var c = new Checkpoint(name, 10_000_000_000, 10_000_000_000); + Assert.Equal(name, c.Name); + Assert.Equal(10_000_000_000, c.Elapsed); + Assert.Equal(10_000_000_000, c.Frequency); + } + + [Fact] + public void Checkpoint_EqualsCheck() + { + string name = "Name"; + var c1 = new Checkpoint(name, 1000, 1000); + var c2 = new Checkpoint(name, 1000, 1000); + var c3 = new Checkpoint("Diff", 1000, 1000); + var c4 = new Checkpoint(name, 2000, 1000); + Assert.True(c1.Equals(c2)); + Assert.True(c1.Equals((object)c2)); + Assert.False(c1.Equals(c3)); + Assert.False(c1.Equals(c4)); + Assert.False(c1.Equals(null)); + Assert.True(c1 == c2); + Assert.False(c1 != c2); + Assert.False(c1 == c3); + Assert.True(c1.GetHashCode() == c2.GetHashCode()); + } + + [Fact] + public void CheckpointToken_BasicTest() + { + string name = "Name"; + int pos = 5; + var c = new CheckpointToken(name, pos); + Assert.Equal(name, c.Name); + Assert.Equal(pos, c.Position); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/LatencyDataTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/LatencyDataTest.cs new file mode 100644 index 0000000000..1ef27ffb35 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/LatencyDataTest.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Latency.Test; + +public class LatencyDataTest +{ + [Fact] + public void LatencyData_DefaultTest() + { + var ld = default(LatencyData); + Assert.True(ld.Checkpoints.Length == 0); + Assert.True(ld.Measures.Length == 0); + Assert.True(ld.Tags.Length == 0); + } + + [Fact] + public void LatencyData_BasicTest() + { + int num = 5; + + ArraySegment checkpoints = new ArraySegment(LatencyDataTest.GetCheckpoints(num)); + ArraySegment measures = new ArraySegment(GetMeasures(num)); + ArraySegment tags = new ArraySegment(GetTags(num)); + + var ld = new LatencyData(tags, checkpoints, measures, default, default); + + Assert.True(ld.Checkpoints.Length == num); + Assert.True(ld.Measures.Length == num); + Assert.True(ld.Tags.Length == num); + } + + [Fact] + public void LatencyData_SegmentTest() + { + int num = 6; + int numCheckpoints = 3; + int numMeasures = 1; + int numTags = 2; + + ArraySegment checkpoints = new ArraySegment(LatencyDataTest.GetCheckpoints(num), 1, numCheckpoints); + ArraySegment measures = new ArraySegment(GetMeasures(num), 2, numMeasures); + ArraySegment tags = new ArraySegment(GetTags(num), 3, numTags); + + var ld = new LatencyData(tags, checkpoints, measures, default, default); + + Assert.True(ld.Checkpoints.Length == numCheckpoints); + Assert.True(ld.Measures.Length == numMeasures); + Assert.True(ld.Tags.Length == numTags); + } + + private static Checkpoint[] GetCheckpoints(int length) + { + Checkpoint[] checkpoints = new Checkpoint[length]; + for (int i = 0; i < checkpoints.Length; i++) + { + checkpoints[i] = new Checkpoint("c" + i, default, default); + } + + return checkpoints; + } + + private static Measure[] GetMeasures(int length) + { + Measure[] measures = new Measure[length]; + for (int i = 0; i < measures.Length; i++) + { + measures[i] = new Measure("m" + i, i); + } + + return measures; + } + + private static Tag[] GetTags(int length) + { + Tag[] tags = new Tag[length]; + for (int i = 0; i < tags.Length; i++) + { + tags[i] = new Tag("tk" + i, "tv" + i); + } + + return tags; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/LatencyRegistryExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/LatencyRegistryExtensionsTest.cs new file mode 100644 index 0000000000..7014a35664 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/LatencyRegistryExtensionsTest.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Latency.Test; + +public class LatencyRegistryExtensionsTest +{ + [Fact] + public void LatencyRegistryExtension_NullArguments() + { + Assert.Throws( + () => LatencyRegistryExtensions.RegisterCheckpointNames(new ServiceCollection(), null!)); + Assert.Throws( + () => LatencyRegistryExtensions.RegisterCheckpointNames(null!, new string[0])); + Assert.Throws( + () => LatencyRegistryExtensions.RegisterMeasureNames(null!, new string[0])); + Assert.Throws( + () => LatencyRegistryExtensions.RegisterMeasureNames(new ServiceCollection(), null!)); + Assert.Throws( + () => LatencyRegistryExtensions.RegisterTagNames(null!, new string[0])); + Assert.Throws( + () => LatencyRegistryExtensions.RegisterTagNames(new ServiceCollection(), null!)); + } + + [Fact] + public void LatencyRegistryExtension_BasicFunctionality() + { + var services = RegisterNames(new ServiceCollection()); + var serviceProvider = services.BuildServiceProvider(); + Assert.NotNull(serviceProvider); + + var option = serviceProvider.GetService>(); + Assert.NotNull(option); + Assert.NotNull(option!.Value); + CheckNumberOfRegisteredNames(option.Value!); + } + + [Fact] + public void LatencyRegistry_CreateOption() + { + var lcro = new LatencyContextRegistrationOptions(); + var chk = new[] { "ca", "cb", "cc" }; + var tags = new[] { "ta", "tb", "tc" }; + var measures = new[] { "ma", "mb", "mc" }; + lcro.CheckpointNames = chk; + lcro.MeasureNames = measures; + lcro.TagNames = tags; + + Assert.Equal(chk, lcro.CheckpointNames); + Assert.Equal(measures, lcro.MeasureNames); + Assert.Equal(tags, lcro.TagNames); + } + + private static IServiceCollection RegisterNames(IServiceCollection services) + { + services.RegisterCheckpointNames(new[] { "ca" }); + services.RegisterMeasureNames(new[] { "ma" }); + services.RegisterMeasureNames(new[] { "mb" }); + services.RegisterTagNames(new[] { "ta" }); + services.RegisterTagNames(new[] { "tb", "tc" }); + + return services; + } + + private static void CheckNumberOfRegisteredNames(LatencyContextRegistrationOptions lcro) + { + Assert.True(lcro.CheckpointNames.Count == 1); + Assert.True(lcro.MeasureNames.Count == 2); + Assert.True(lcro.TagNames.Count == 3); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/MeasureTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/MeasureTest.cs new file mode 100644 index 0000000000..af80b422d8 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/MeasureTest.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Latency.Test; + +public class MeasureTest +{ + [Fact] + public void Measure_BasicTest() + { + string name = "Name"; + long value = 10; + var c = new Measure(name, value); + Assert.Equal(c.Name, name); + Assert.Equal(c.Value, value); + } + + [Fact] + public void Measure_EqualsCheck() + { + string name = "Name"; + long value = 100; + var m1 = new Measure(name, value); + var m2 = new Measure(name, value); + var m3 = new Measure("Diff", value); + var m4 = new Measure(name, 150); + Assert.True(m1.Equals(m2)); + Assert.True(m1.Equals((object)m2)); + Assert.False(m1.Equals(m3)); + Assert.False(m1.Equals(m4)); + Assert.False(m1.Equals(null)); + Assert.True(m1 == m2); + Assert.False(m1 != m2); + Assert.True(m1.GetHashCode() == m2.GetHashCode()); + } + + [Fact] + public void MeasureToken_BasicTest() + { + string name = "Name"; + int pos = 10; + var c = new MeasureToken(name, pos); + Assert.Equal(c.Name, name); + Assert.Equal(c.Position, pos); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/NoopLatencyContextTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/NoopLatencyContextTest.cs new file mode 100644 index 0000000000..b27b5e6d53 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/NoopLatencyContextTest.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Latency.Test; + +public class NoopLatencyContextTest +{ + [Fact] + public void ServiceCollection_Null() + { + Assert.Throws(() => + NullLatencyContextExtensions.AddNullLatencyContext(null!)); + } + + [Fact] + public void ServiceCollection_BasicAddNoopLatencyContext() + { + using var serviceProvider = new ServiceCollection() + .AddNullLatencyContext() + .BuildServiceProvider(); + + var latencyContextProvider = serviceProvider.GetRequiredService(); + Assert.NotNull(latencyContextProvider); + Assert.IsAssignableFrom(latencyContextProvider); + } + + [Fact] + public void ServiceCollection_GivenScopes_ReturnsDifferentInstanceForEachScope() + { + using var serviceProvider = new ServiceCollection() + .AddNullLatencyContext() + .BuildServiceProvider(); + + var scope1 = serviceProvider.CreateScope(); + var scope2 = serviceProvider.CreateScope(); + + // Get same instance within single scope. + Assert.Equal(scope1.ServiceProvider.GetRequiredService(), + scope1.ServiceProvider.GetRequiredService()); + Assert.Equal(scope1.ServiceProvider.GetRequiredService(), + scope1.ServiceProvider.GetRequiredService()); + + // Get same instance between different scopes + Assert.Equal(scope1.ServiceProvider.GetRequiredService(), + scope2.ServiceProvider.GetRequiredService()); + Assert.Equal(scope1.ServiceProvider.GetRequiredService(), + scope2.ServiceProvider.GetRequiredService()); + + scope1.Dispose(); + scope2.Dispose(); + } + + [Fact] + public void NoopLatencyContext_BasicFunctionality() + { + using var np = new NullLatencyContext(); + + ILatencyContextProvider lcp = np; + Assert.NotNull(lcp.CreateContext()); + + ILatencyContext context = np; + ILatencyContextTokenIssuer issuer = np; + context.SetTag(issuer.GetTagToken(string.Empty), string.Empty); + context.AddCheckpoint(issuer.GetCheckpointToken(string.Empty)); + context.AddMeasure(issuer.GetMeasureToken(string.Empty), 0); + context.RecordMeasure(issuer.GetMeasureToken(string.Empty), 0); + var latencyData = context.LatencyData; + Assert.Equal(0, latencyData.DurationTimestamp); + Assert.True(latencyData.Checkpoints.Length == 0); + Assert.True(latencyData.Measures.Length == 0); + Assert.True(latencyData.Tags.Length == 0); + context.Freeze(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/TagTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/TagTest.cs new file mode 100644 index 0000000000..abd6a19ed2 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Latency/TagTest.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Latency.Test; + +public class TagTest +{ + [Fact] + public void Tag_BasicTest() + { + string name = "Name"; + string value = "Val"; + var t = new Tag(name, value); + Assert.Equal(t.Name, name); + Assert.Equal(t.Value, value); + } + + [Fact] + public void TagToken_BasicTest() + { + string name = "Name"; + int pos = 10; + var c = new TagToken(name, pos); + Assert.Equal(c.Name, name); + Assert.Equal(c.Position, pos); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Logging/LogMethodAttributeTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Logging/LogMethodAttributeTests.cs new file mode 100644 index 0000000000..ce7c98db00 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Logging/LogMethodAttributeTests.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Logging.Test; + +public class LogMethodAttributeTests +{ + [Fact] + public void Basic() + { + var a = new LogMethodAttribute(42, LogLevel.Trace, "Foo"); + Assert.Equal(42, a.EventId); + Assert.Equal(LogLevel.Trace, a.Level); + Assert.Equal("Foo", a.Message); + Assert.Null(a.EventName); + Assert.False(a.SkipEnabledCheck); + + a.EventName = "Name"; + Assert.Equal("Name", a.EventName); + + a.SkipEnabledCheck = true; + Assert.True(a.SkipEnabledCheck); + + a = new LogMethodAttribute(42, "Foo"); + Assert.Equal(42, a.EventId); + Assert.False(a.Level.HasValue); + Assert.Equal("Foo", a.Message); + Assert.Null(a.EventName); + + a.EventName = "Name"; + Assert.Equal("Name", a.EventName); + + a = new LogMethodAttribute("Foo"); + Assert.Equal(0, a.EventId); + Assert.False(a.Level.HasValue); + Assert.Equal("Foo", a.Message); + Assert.Null(a.EventName); + + a = new LogMethodAttribute(LogLevel.Debug); + Assert.Equal(0, a.EventId); + Assert.Equal(LogLevel.Debug, a.Level!.Value); + Assert.Equal(string.Empty, a.Message); + Assert.Null(a.EventName); + + a = new LogMethodAttribute(LogLevel.Debug, "Foo"); + Assert.Equal(0, a.EventId); + Assert.Equal(LogLevel.Debug, a.Level!.Value); + Assert.Equal("Foo", a.Message); + Assert.Null(a.EventName); + + a = new LogMethodAttribute(123); + Assert.Equal(123, a.EventId); + Assert.False(a.Level.HasValue); + Assert.Equal(string.Empty, a.Message); + Assert.Null(a.EventName); + + a = new LogMethodAttribute(123, "Foo"); + Assert.Equal(123, a.EventId); + Assert.False(a.Level.HasValue); + Assert.Equal("Foo", a.Message); + Assert.Null(a.EventName); + + a = new LogMethodAttribute(123, LogLevel.Debug); + Assert.Equal(123, a.EventId); + Assert.Equal(LogLevel.Debug, a.Level!.Value); + Assert.Equal(string.Empty, a.Message); + Assert.Null(a.EventName); + + a = new LogMethodAttribute(); + Assert.Equal(0, a.EventId); + Assert.Equal(string.Empty, a.Message); + Assert.Null(a.Level); + Assert.Null(a.EventName); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Logging/LogMethodHelperTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Logging/LogMethodHelperTests.cs new file mode 100644 index 0000000000..de83f4291e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Logging/LogMethodHelperTests.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Extensions.Telemetry.Logging; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Logging.Test; + +public static class LogMethodHelperTests +{ + [Fact] + public static void CollectorContract() + { + const string ParamName = "param_name Name"; + const string PropName = "Property Name"; + const string Value = "Value"; + + var list = new LogMethodHelper(); + Assert.Equal(list.ParameterName, string.Empty); + Assert.Empty(list); + + list.ParameterName = ParamName; + list.Add(PropName, Value); + Assert.Single(list); + Assert.Equal(ParamName, list.ParameterName); + Assert.Equal(ParamName + "_" + PropName, list[0].Key); + Assert.Equal(Value, list[0].Value); + + _ = list.TryReset(); + Assert.Empty(list); + Assert.Equal(string.Empty, list.ParameterName); + + list.Add(PropName, Value); + Assert.Single(list); + Assert.Equal(PropName, list[0].Key); + Assert.Equal(Value, list[0].Value); + + var bag = (IEnrichmentPropertyBag)list; + + _ = list.TryReset(); + bag.Add(PropName, Value); + Assert.Single(list); + Assert.Equal(PropName, list[0].Key); + Assert.Equal(Value, list[0].Value); + + _ = list.TryReset(); + bag.Add(PropName, (object)Value); + Assert.Single(list); + Assert.Equal(PropName, list[0].Key); + Assert.Equal(Value, list[0].Value); + + _ = list.TryReset(); + bag.Add(new[] { new KeyValuePair(PropName, Value) }.AsSpan()); + Assert.Single(list); + Assert.Equal(PropName, list[0].Key); + Assert.Equal(Value, list[0].Value); + + _ = list.TryReset(); + bag.Add(new[] { new KeyValuePair(PropName, Value) }.AsSpan()); + Assert.Single(list); + Assert.Equal(PropName, list[0].Key); + Assert.Equal(Value, list[0].Value); + } + + [Theory] + [InlineData(null, "null")] + [InlineData(new[] { "One" }, "[\"One\"]")] + [InlineData(new[] { "One", "Two" }, "[\"One\",\"Two\"]")] + [InlineData(new[] { "One", null }, "[\"One\",null]")] + [InlineData(new[] { 1, 2, 3 }, "[\"1\",\"2\",\"3\"]")] + public static void Enumerate(IEnumerable? enumerable, string expected) + { + Assert.Equal(expected, LogMethodHelper.Stringify(enumerable)); + } + + [Fact] + public static void EnumerateKeyValuePair() + { + Assert.Equal("null", LogMethodHelper.Stringify((IEnumerable>?)null)); + + var d0 = new Dictionary + { + { "One", "Un" } + }; + Assert.Equal("{\"One\"=\"Un\"}", LogMethodHelper.Stringify(d0)); + + var d1 = new Dictionary + { + { "One", "Un" }, + { "Two", "Deux" } + }; + Assert.Equal("{\"One\"=\"Un\",\"Two\"=\"Deux\"}", LogMethodHelper.Stringify(d1)); + + var d2 = new List> + { + new(null, "Un"), + new("Two", null), + }; + Assert.Equal("{null=\"Un\",\"Two\"=null}", LogMethodHelper.Stringify(d2)); + + var d3 = new Dictionary + { + { "Zero", 0 }, + { "One", 1 }, + { "Two", 2 } + }; + Assert.Equal("{\"Zero\"=\"0\",\"One\"=\"1\",\"Two\"=\"2\"}", LogMethodHelper.Stringify(d3)); + + var d4 = new Dictionary + { + { 0, "Zero" }, + { 1, "One" }, + { 2, "Two" } + }; + Assert.Equal("{\"0\"=\"Zero\",\"1\"=\"One\",\"2\"=\"Two\"}", LogMethodHelper.Stringify(d4)); + } + + [Fact] + public static void Pool() + { + var list = LogMethodHelper.GetHelper(); + Assert.NotNull(list); + list.Add("Foo", "Bar"); + LogMethodHelper.ReturnHelper(list); + } + +#if NET6_0_OR_GREATER + [Fact] + public static void Options() + { + var opt = LogMethodHelper.SkipEnabledCheckOptions; + Assert.True(opt.SkipEnabledCheck); + } +#endif +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Logging/LogPropertiesAttributeTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Logging/LogPropertiesAttributeTests.cs new file mode 100644 index 0000000000..0b971528d2 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Logging/LogPropertiesAttributeTests.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Logging.Test; + +public class LogPropertiesAttributeTests +{ + [Fact] + public void SkipNullProps() + { + var lpa = new LogPropertiesAttribute(); + Assert.False(lpa.SkipNullProperties); + + lpa.SkipNullProperties = true; + Assert.True(lpa.SkipNullProperties); + } + + [Fact] + public void OmitParameterName() + { + var lpa = new LogPropertiesAttribute(); + Assert.False(lpa.OmitParameterName); + + lpa.OmitParameterName = true; + Assert.True(lpa.OmitParameterName); + } + + [Fact] + public void ShouldThrow_WhenCtorArgument_IsNull() + { + Assert.Throws(() => new LogPropertiesAttribute(null!, "test")); + Assert.Throws(() => new LogPropertiesAttribute(typeof(object), null!)); + } + + [Fact] + public void ShouldThrow_WhenMethodIsEmptyOrWhitespace() + { + Assert.Throws(() => new LogPropertiesAttribute(typeof(object), string.Empty)); + Assert.Throws(() => new LogPropertiesAttribute(typeof(object), new string(' ', 3))); + } + + [Fact] + public void ShouldSet_Properties_WhenCustomProvider() + { + const string ProviderMethod = "test_method"; + + var attr = new LogPropertiesAttribute(typeof(DateTime), ProviderMethod); + Assert.Equal(typeof(DateTime), attr.ProviderType); + Assert.Equal(ProviderMethod, attr.ProviderMethod); + } + + [Fact] + public void ShouldNotSet_Properties_WhenDefaultProvider() + { + var attr = new LogPropertiesAttribute(); + Assert.Null(attr.ProviderType); + Assert.Null(attr.ProviderMethod); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Metering/MetricAttributeTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Metering/MetricAttributeTests.cs new file mode 100644 index 0000000000..d55de6f490 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Metering/MetricAttributeTests.cs @@ -0,0 +1,162 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Metering.Test; + +public class MetricAttributeTests +{ + private const string MyMetric = "MyMetric"; + + [Fact] + public void TestCounterAttribute() + { + var attribute = new CounterAttribute("d1", "d2", "d3"); + Assert.NotNull(attribute); + Assert.Null(attribute.Name); + Assert.Null(attribute.Type); + Assert.Equal(new[] { "d1", "d2", "d3" }, attribute.Dimensions); + + attribute.Name = MyMetric; + Assert.Equal(MyMetric, attribute.Name); + } + + [Fact] + public void TestHistogramAttribute() + { + var attribute = new HistogramAttribute("d1", "d2", "d3"); + Assert.NotNull(attribute); + Assert.Null(attribute.Name); + Assert.Null(attribute.Type); + Assert.Equal(new[] { "d1", "d2", "d3" }, attribute.Dimensions); + + attribute.Name = MyMetric; + Assert.Equal(MyMetric, attribute.Name); + } + + [Fact] + public void TestCounterAttributeT() + { + var attribute = new CounterAttribute("d1", "d2", "d3"); + Assert.NotNull(attribute); + Assert.Null(attribute.Name); + Assert.Null(attribute.Type); + Assert.Equal(new[] { "d1", "d2", "d3" }, attribute.Dimensions); + + attribute.Name = MyMetric; + Assert.Equal(MyMetric, attribute.Name); + } + + [Fact] + public void TestHistogramAttributeT() + { + var attribute = new HistogramAttribute("d1", "d2", "d3"); + Assert.NotNull(attribute); + Assert.Null(attribute.Name); + Assert.Null(attribute.Type); + Assert.Equal(new[] { "d1", "d2", "d3" }, attribute.Dimensions); + + attribute.Name = MyMetric; + Assert.Equal(MyMetric, attribute.Name); + } + + [Fact] + public void TestGaugeAttribute() + { + var attribute = new GaugeAttribute("d1", "d2", "d3"); + Assert.NotNull(attribute); + Assert.Null(attribute.Name); + Assert.Null(attribute.Type); + Assert.Equal(new[] { "d1", "d2", "d3" }, attribute.Dimensions); + + attribute.Name = MyMetric; + Assert.Equal(MyMetric, attribute.Name); + } + + [Fact] + public void TestStrongTypeCounterAttribute() + { + var attribute = new CounterAttribute(typeof(DimensionsTest)); + + Assert.NotNull(attribute); + Assert.Null(attribute.Name); + Assert.Null(attribute.Dimensions); + Assert.Equal(typeof(DimensionsTest), attribute.Type); + + attribute.Name = MyMetric; + Assert.Equal(MyMetric, attribute.Name); + } + + [Fact] + public void TestStrongTypeHistogramAttribute() + { + var attribute = new HistogramAttribute(typeof(DimensionsTest)); + + Assert.NotNull(attribute); + Assert.Null(attribute.Name); + Assert.Null(attribute.Dimensions); + Assert.Equal(typeof(DimensionsTest), attribute.Type); + + attribute.Name = MyMetric; + Assert.Equal(MyMetric, attribute.Name); + } + + [Fact] + public void TestStrongTypeCounterAttributeT() + { + var attribute = new CounterAttribute(typeof(DimensionsTest)); + + Assert.NotNull(attribute); + Assert.Null(attribute.Name); + Assert.Null(attribute.Dimensions); + Assert.Equal(typeof(DimensionsTest), attribute.Type); + + attribute.Name = MyMetric; + Assert.Equal(MyMetric, attribute.Name); + } + + [Fact] + public void TestStrongTypeHistogramAttributeT() + { + var attribute = new HistogramAttribute(typeof(DimensionsTest)); + + Assert.NotNull(attribute); + Assert.Null(attribute.Name); + Assert.Null(attribute.Dimensions); + Assert.Equal(typeof(DimensionsTest), attribute.Type); + + attribute.Name = MyMetric; + Assert.Equal(MyMetric, attribute.Name); + } + + [Fact] + public void TestStrongTypeGaugeAttribute() + { + var attribute = new GaugeAttribute(typeof(DimensionsTest)); + + Assert.NotNull(attribute); + Assert.Null(attribute.Name); + Assert.Null(attribute.Dimensions); + Assert.Equal(typeof(DimensionsTest), attribute.Type); + + attribute.Name = MyMetric; + Assert.Equal(MyMetric, attribute.Name); + } + + [Fact] + public void TestDimensionAttribute() + { + var attribute = new DimensionAttribute("testName"); + + Assert.NotNull(attribute); + Assert.Equal("testName", attribute.Name); + } + + public class DimensionsTest + { + public string? D1 { get; set; } + public string? D2 { get; set; } + public string? D3 { get; set; } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Microsoft.Extensions.Telemetry.Abstractions.Tests.csproj b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Microsoft.Extensions.Telemetry.Abstractions.Tests.csproj new file mode 100644 index 0000000000..d2e1cc733e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Abstractions.Tests/Microsoft.Extensions.Telemetry.Abstractions.Tests.csproj @@ -0,0 +1,14 @@ + + + Microsoft.Extensions.Telemetry + Unit tests for Microsoft.Extensions.Telemetry.Abstractions. + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Latency/LatencyConsoleExporterTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Latency/LatencyConsoleExporterTests.cs new file mode 100644 index 0000000000..b798e3e0da --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Latency/LatencyConsoleExporterTests.cs @@ -0,0 +1,223 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Telemetry.Latency; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Console.Test; + +[Collection("StdoutUsage")] +public class LatencyConsoleExporterTests +{ + private static readonly string _fullResult = NormalizeLineEndings( +@"Latency sample #0: 20ms, 3 checkpoints, 3 tags, 3 measures + Checkpoint | Value (ms) + -----------|----------- + ca | 1 + cb | 2 + cc | 3 + + Tag | Value + ----|------ + ta | t1 + tb | t2 + tc | t3 + + Measure | Value + --------|------ + ma | 1 + mb | 2 + mc | 3 +"); + + private static readonly string _longResult = NormalizeLineEndings( +@"Latency sample #0: 20ms, 3 checkpoints, 3 tags, 3 measures + Checkpoint | Value (ms) + -------------|----------- + ccccccccccca | 1 + cccccccccccb | 2 + cccccccccccc | 3 + + Tag | Value + -------------|------ + ttttttttttta | t1 + tttttttttttb | t2 + tttttttttttc | t3 + + Measure | Value + -------------|------ + mmmmmmmmmmma | 1 + mmmmmmmmmmmb | 2 + mmmmmmmmmmmc | 3 +"); + + private static readonly string _truncatedResult = NormalizeLineEndings( +@"Latency sample #0: 20ms, 3 checkpoints, 3 tags, 3 measures +"); + + private static readonly string _emptyResult = NormalizeLineEndings( +@"Latency sample #0: 20ms, 0 checkpoints, 0 tags, 0 measures +"); + + [Fact] + public async Task ConsoleExporter_Export_OutputsData() + { + using var a = new Accumulator(); + System.Console.SetOut(a); + + var ld = GetLatencyData(); + + var options = Microsoft.Extensions.Options.Options.Create(new LarencyConsoleOptions + { + OutputCheckpoints = true, + OutputTags = true, + OutputMeasures = true, + }); + + var exporter = new LatencyConsoleExporter(options); + await exporter.ExportAsync(ld, default); + a.Flush(); + var result = a.ToString(); + Assert.Equal(_fullResult, result); + } + + [Fact] + public async Task ConsoleExporter_Export_OutputsLongData() + { + using var a = new Accumulator(); + System.Console.SetOut(a); + + var ld = GetLongLatencyData(); + + var options = Microsoft.Extensions.Options.Options.Create(new LarencyConsoleOptions + { + OutputCheckpoints = true, + OutputTags = true, + OutputMeasures = true, + }); + + var exporter = new LatencyConsoleExporter(options); + await exporter.ExportAsync(ld, default); + a.Flush(); + var result = a.ToString(); + Assert.Equal(_longResult, result); + } + + [Fact] + public async Task ConsoleExporter_Export_OutputsTruncatedData() + { + using var a = new Accumulator(); + System.Console.SetOut(a); + + var ld = GetLatencyData(); + + var options = Microsoft.Extensions.Options.Options.Create(new LarencyConsoleOptions + { + OutputCheckpoints = false, + OutputTags = false, + OutputMeasures = false + }); + + var exporter = new LatencyConsoleExporter(options); + await exporter.ExportAsync(ld, default); + a.Flush(); + var result = a.ToString(); + Assert.Equal(_truncatedResult, result); + } + + [Fact] + public async Task ConsoleExporter_Export_OutputsEmptyData() + { + using var a = new Accumulator(); + System.Console.SetOut(a); + + var ld = GetEmptyLatencyData(); + + var options = Microsoft.Extensions.Options.Options.Create(new LarencyConsoleOptions + { + OutputCheckpoints = true, + OutputTags = true, + OutputMeasures = true, + }); + + var exporter = new LatencyConsoleExporter(options); + await exporter.ExportAsync(ld, default); + a.Flush(); + var result = a.ToString(); + Assert.Equal(_emptyResult, result); + } + + private static string NormalizeLineEndings(string value) => value.Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", Environment.NewLine); + + private sealed class Accumulator : TextWriter + { + private readonly StringBuilder _sb = new(); + public override Encoding Encoding => Encoding.UTF8; + public override void Write(char value) => _sb.Append(value); + public override string ToString() => _sb.ToString(); + } + + private static LatencyData GetLatencyData() + { + ArraySegment checkpoints = new(new[] + { + new Checkpoint("ca", 1, 1000), + new Checkpoint("cb", 2, 1000), + new Checkpoint("cc", 3, 1000) + }); + + ArraySegment measures = new(new[] + { + new Measure("ma", 1), + new Measure("mb", 2), + new Measure("mc", 3), + }); + + ArraySegment tags = new(new[] + { + new Tag("ta", "t1"), + new Tag("tb", "t2"), + new Tag("tc", "t3") + }); + + return new LatencyData(tags, checkpoints, measures, 20, 1000); + } + + private static LatencyData GetLongLatencyData() + { + ArraySegment checkpoints = new(new[] + { + new Checkpoint("ccccccccccca", 1, 1000), + new Checkpoint("cccccccccccb", 2, 1000), + new Checkpoint("cccccccccccc", 3, 1000) + }); + + ArraySegment measures = new(new[] + { + new Measure("mmmmmmmmmmma", 1), + new Measure("mmmmmmmmmmmb", 2), + new Measure("mmmmmmmmmmmc", 3), + }); + + ArraySegment tags = new(new[] + { + new Tag("ttttttttttta", "t1"), + new Tag("tttttttttttb", "t2"), + new Tag("tttttttttttc", "t3") + }); + + return new LatencyData(tags, checkpoints, measures, 20, 1000); + } + + private static LatencyData GetEmptyLatencyData() + { + ArraySegment checkpoints = new(Array.Empty()); + ArraySegment measures = new(Array.Empty()); + ArraySegment tags = new(Array.Empty()); + return new LatencyData(tags, checkpoints, measures, 20, 1000); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Latency/LatencyConsoleExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Latency/LatencyConsoleExtensionsTests.cs new file mode 100644 index 0000000000..46407cc5ac --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Latency/LatencyConsoleExtensionsTests.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Latency; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Console.Test; + +public class LatencyConsoleExtensionsTests +{ + [Fact] + public void ConsoleExporterExtensions_GivenNullArguments_ThrowsArgumentNullException() + { + var s = new ServiceCollection(); + Assert.Throws(() => LatencyConsoleExtensions.AddConsoleLatencyDataExporter(null!)); + Assert.Throws(() => LatencyConsoleExtensions.AddConsoleLatencyDataExporter(s, configure: null!)); + Assert.Throws(() => LatencyConsoleExtensions.AddConsoleLatencyDataExporter(s, section: null!)); + } + + [Fact] + public void ConsoleExporterExtensions_Add_AddsExporter() + { + using var serviceProvider = new ServiceCollection() + .AddConsoleLatencyDataExporter() + .BuildServiceProvider(); + + var exporter = serviceProvider.GetRequiredService(); + Assert.NotNull(exporter); + Assert.IsAssignableFrom(exporter); + } + + [Fact] + public void ConsoleExporterExtensions_Add_InvokesConfig() + { + var invoked = false; + using var serviceProvider = new ServiceCollection() + .AddConsoleLatencyDataExporter(a => { invoked = true; }) + .BuildServiceProvider(); + + var exporter = serviceProvider.GetRequiredService(); + Assert.NotNull(exporter); + Assert.True(invoked); + } + + [Fact] + public void ConsoleExporterExtensions_Add_BindsToConfigSection() + { + LarencyConsoleOptions expectedOptions = new() + { + OutputTags = true + }; + var config = GetConfigSection(expectedOptions); + + using var provider = new ServiceCollection() + .AddConsoleLatencyDataExporter(config) + .BuildServiceProvider(); + var actualOptions = provider.GetRequiredService>(); + + Assert.True(actualOptions.Value.OutputTags); + } + + private static IConfigurationSection GetConfigSection(LarencyConsoleOptions options) + { + return new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { $"{nameof(LarencyConsoleOptions)}:{nameof(options.OutputTags)}", options.OutputTags.ToString(null) }, + }) + .Build() + .GetSection($"{nameof(LarencyConsoleOptions)}"); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Latency/LatencyConsoleOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Latency/LatencyConsoleOptionsTests.cs new file mode 100644 index 0000000000..7e35d1ef7f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Latency/LatencyConsoleOptionsTests.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Console.Test; + +public class LatencyConsoleOptionsTests +{ + [Fact] + public void ConsoleExporterOptions_BasicTest() + { + var o = new LarencyConsoleOptions(); + Assert.True(o.OutputCheckpoints); + Assert.True(o.OutputTags); + Assert.True(o.OutputMeasures); + + o.OutputCheckpoints = false; + o.OutputTags = false; + o.OutputMeasures = false; + + Assert.False(o.OutputCheckpoints); + Assert.False(o.OutputTags); + Assert.False(o.OutputMeasures); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/AcceptanceTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/AcceptanceTest.cs new file mode 100644 index 0000000000..df37cb0482 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/AcceptanceTest.cs @@ -0,0 +1,253 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Console.Internal.Test; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Extensions.Telemetry.Logging; +using Xunit; + +#if NET5_0_OR_GREATER +using Microsoft.Extensions.Telemetry.Console.Internal; +using MSOptions = Microsoft.Extensions.Options.Options; +#endif + +namespace Microsoft.Extensions.Telemetry.Console.Test; + +[Collection("StdoutUsage")] +#if NET7_0_OR_GREATER +public partial class AcceptanceTest +{ + [GeneratedRegex("\u001b.*?m", RegexOptions.Compiled)] + private static partial Regex ColorsRegex(); + + private static readonly Regex _removeColorsRegex = ColorsRegex(); +#else +public class AcceptanceTest +{ + private static readonly Regex _removeColorsRegex = new("\x1B.*?m", RegexOptions.Compiled); +#endif + + [Fact] + public void UnstructuredLogExportedCorrectly() + { + CaptureAndRecoverConsoleOut(() => + { + using var writer = new StringWriter(); + System.Console.SetOut(writer); + var logger = GetLogger(); + var logMessage = "Refrigerator is broken."; + + logger.LogInformation(logMessage); + + var logs = RemoveColors(writer.ToString()); + Assert.EndsWith(GetExpectedMessage(logMessage), logs); + }); + } + + [Fact] + public void StructuredLogExportedCorrectly() + { + CaptureAndRecoverConsoleOut(() => + { + using var writer = new StringWriter(); + System.Console.SetOut(writer); + var logger = GetLogger(); + var logMessage = "{name} is broken."; + + logger.LogInformation(logMessage, "Refrigerator"); + + var logs = RemoveColors(writer.ToString()); + Assert.EndsWith(GetExpectedMessage(logMessage), logs); + }); + } + + [Fact] + public void StructuredLogWithUseFormattedMessageExportedCorrectly() + { + CaptureAndRecoverConsoleOut(() => + { + using var writer = new StringWriter(); + System.Console.SetOut(writer); + var logger = GetLogger(useFormattedMessage: true); + var logMessage = "{name} is broken."; + var name = "Refrigerator"; + var formattedMessage = $"{name} is broken."; + + logger.LogInformation(logMessage, name); + + var logs = RemoveColors(writer.ToString()); + Assert.EndsWith(GetExpectedMessage(formattedMessage), logs); + }); + } + + [Theory] + [CombinatorialData] + public void LogWithScopesExportedCorrectly(bool useFormattedMessage, bool useEnricher) + { + CaptureAndRecoverConsoleOut(() => + { + using var writer = new StringWriter(); + System.Console.SetOut(writer); + ILogEnricher? enricher = useEnricher ? new TestLogEnricher() : null; + var logger = GetLogger(includeScopes: true, useFormattedMessage, enricher); + var logMessage = "{name} is broken."; + var name = "Refrigerator"; + var formattedMessage = $"{name} is broken."; + var scope1 = "operation"; + var scope2 = "hardware"; + + using (logger.BeginScope(scope1)) + using (logger.BeginScope(scope2)) + { + logger.LogInformation(logMessage, name); + } + + var logs = RemoveColors(writer.ToString()); + + var enricherScope = useEnricher ? $" {TestLogEnricher.Key}:{TestLogEnricher.Value}" : string.Empty; + Assert.StartsWith($"Scope: {scope1} {scope2} name:{name}{enricherScope}{Environment.NewLine}", logs); + + string message = useFormattedMessage ? formattedMessage : logMessage; + Assert.EndsWith(GetExpectedMessage(message), logs); + }); + } + + [Fact] + public void ExceptionLogExportedCorrectly() + { + CaptureAndRecoverConsoleOut(() => + { + using var writer = new StringWriter(); + System.Console.SetOut(writer); + var logger = GetLogger(); + var logMessage = "Logging message for {reason}."; + try + { + throw new AggregateException("Aggregate exception message", + new DivideByZeroException("Divide by zero exception message", new IOException("IO exception message")), + new ArgumentNullException("Parameter name")); + } + catch (AggregateException ex) + { + logger.LogInformation(ex, logMessage, "testing"); + } + + var logs = RemoveColors(writer.ToString()); + Assert.Contains(GetExpectedMessage(logMessage), logs); + }); + } + + [Fact] + public void LogWithTraceIdExportedCorrectly() + { + CaptureAndRecoverConsoleOut(() => + { + using var writer = new StringWriter(); + System.Console.SetOut(writer); + var logger = GetLogger(); + var logMessage = "Refrigerator is broken."; + + logger.LogInformation(logMessage); + + var logs = RemoveColors(writer.ToString()); + Assert.EndsWith(GetExpectedMessage(logMessage), logs); + }); + } + + [Fact] + public void GetOriginalFormat_GivenNullOrNothingForOriginalFormat_ReturnsEmptyString() + { + var methodInfo = typeof(LoggingConsoleExporter).GetMethod("GetOriginalFormat", BindingFlags.NonPublic | BindingFlags.Static); +#if NET5_0_OR_GREATER + using var consoleLogExporter = new LoggingConsoleExporter(MSOptions.Create(new LoggingConsoleOptions())); +#else + using var consoleLogExporter = new LoggingConsoleExporter(); +#endif + + ReadOnlyCollection> state = new(new List> { new("{OriginalFormat}", null!) }); + object[] parameters = { state }; + var result = methodInfo?.Invoke(consoleLogExporter, parameters); + Assert.IsType(result); + Assert.Equal(string.Empty, result); + + state = new(new List>()); + parameters[0] = state; + result = methodInfo?.Invoke(consoleLogExporter, parameters); + Assert.IsType(result); + Assert.Equal(string.Empty, result); + } + + [Fact] + public void GetOriginalFormat_GivenNullStateForOriginalFormat_ReturnsEmptyString() + { + var methodInfo = typeof(LoggingConsoleExporter).GetMethod("GetOriginalFormat", BindingFlags.NonPublic | BindingFlags.Static); +#if NET5_0_OR_GREATER + using var consoleLogExporter = new LoggingConsoleExporter(MSOptions.Create(new LoggingConsoleOptions())); +#else + using var consoleLogExporter = new LoggingConsoleExporter(); +#endif + + ReadOnlyCollection>? state = null; + object?[] parameters = { state }; + var result = methodInfo?.Invoke(consoleLogExporter, parameters); + Assert.IsType(result); + Assert.Equal(string.Empty, result); + } + + private static ILogger GetLogger(bool includeScopes = false, bool useFormattedMessage = false, ILogEnricher? enricher = null) + { + using var loggerFactory = LoggerFactory.Create(builder => + { + _ = builder + .AddOpenTelemetryLogging(options => + { + options.IncludeScopes = includeScopes; + options.UseFormattedMessage = useFormattedMessage; + }) + .AddConsoleExporter(); + + if (enricher is not null) + { + _ = builder.Services.AddLogEnricher(enricher); + } + }); + + return loggerFactory.CreateLogger(); + } + + private static void CaptureAndRecoverConsoleOut(Action test) + { + var consoleOut = System.Console.Out; + + try + { + test(); + } + finally + { + System.Console.SetOut(consoleOut); + } + } + + private static string RemoveColors(string message) + { + return _removeColorsRegex.Replace(message, string.Empty); + } + + private static string GetExpectedMessage(string logMessage) + { +#if NET5_0_OR_GREATER + return $"({LogLevel.Information.InShortString()}) {default(ActivityTraceId)} {default(ActivitySpanId)} {logMessage} ({typeof(AcceptanceTest).FullName}/0){Environment.NewLine}"; +#else + return $"{LogLevel.Information} {default(ActivityTraceId)} {default(ActivitySpanId)} {logMessage} {typeof(AcceptanceTest).FullName}/0{Environment.NewLine}"; +#endif + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Helpers/TestException.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Helpers/TestException.cs new file mode 100644 index 0000000000..4d58fe7354 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Helpers/TestException.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +using System; + +namespace Microsoft.Extensions.Telemetry.Console.Test.Helpers; + +/// +/// To test exception with a stack trace. +/// +public class TestException : Exception +{ + public TestException(string message, string stackTrace) + : base(message) + { + StackTrace = stackTrace; + } + + public override string StackTrace { get; } +} +#endif diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/ColorSetTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/ColorSetTests.cs new file mode 100644 index 0000000000..f3dc187a8b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/ColorSetTests.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Console.Internal.Test; + +public class ColorSetTests +{ + [Theory] + [MemberData(nameof(DifferentSetsData))] + internal void Equals_WhenComparingTwoDifferent_ReturnsFalse(ColorSet color1, ColorSet color2) + { + Assert.NotEqual(color1, color2); + Assert.NotEqual(color1.GetHashCode(), color2.GetHashCode()); + + Assert.False(color1 == color2); + Assert.False(color1.Equals((object)color2)); + Assert.True(color1 != color2); + } + + [Fact] + public void Equals_WhenComparingTwoIdentical_ReturnsTrue() + { + var color1 = Colors.BlueOnNone; + var color2 = Colors.BlueOnNone; + + Assert.Equal(color1, color2); + Assert.Equal(color1.GetHashCode(), color2.GetHashCode()); + + Assert.True(color1 == color2); + Assert.True(color1.Equals((object)color2)); + Assert.False(color1 != color2); + } + + [Fact] + public void Equals_WhenComparingWithDifferentTypes_ReturnsFalse() + { + var color = Colors.BlackOnCyan; + Assert.False(color.Equals(null)); + Assert.False(color.Equals(string.Empty)); + Assert.False(color.Equals(new object())); + } + + [Theory] + [InlineData(ConsoleColor.Red, ConsoleColor.Green, "Red on Green")] + [InlineData(ConsoleColor.White, ConsoleColor.Blue, "White on Blue")] + [InlineData(null, ConsoleColor.Green, "None on Green")] + [InlineData(ConsoleColor.Red, null, "Red on None")] + [InlineData(null, null, "None on None")] + public void ToString_WhenCalledOnDifferentColors_ReturnsCorrect(ConsoleColor? foreground, ConsoleColor? background, string expected) + { + var colorSet = new ColorSet(foreground, background); + Assert.Equal(expected, colorSet.ToString()); + } + + public static IEnumerable DifferentSetsData => + new[] + { + new object[] { Colors.BlackOnBlue, Colors.RedOnCyan }, + new object[] { Colors.BlackOnBlue, Colors.BlackOnCyan }, + new object[] { Colors.RedOnBlack, Colors.BlueOnBlack }, + new object[] { Colors.WhiteOnCyan, Colors.YellowOnWhite } + }; +} +#endif diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/LogFormatterOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/LogFormatterOptionsTests.cs new file mode 100644 index 0000000000..f1d1044526 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/LogFormatterOptionsTests.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Console.Internal.Test; + +public class LogFormatterOptionsTests +{ + private readonly LogFormatterOptions _testClass; + + public LogFormatterOptionsTests() + { + _testClass = new LogFormatterOptions(); + } + + [Fact] + public void CanConstruct() + { + var instance = new LogFormatterOptions(); + Assert.NotNull(instance); + } + + [Fact] + public void CanSetAndGetIncludeTimestamp() + { + const bool TestValue = true; + _testClass.IncludeTimestamp = TestValue; + Assert.Equal(TestValue, _testClass.IncludeTimestamp); + } + + [Fact] + public void CanSetAndGetIncludeLogLevel() + { + const bool TestValue = true; + _testClass.IncludeLogLevel = TestValue; + Assert.Equal(TestValue, _testClass.IncludeLogLevel); + } + + [Fact] + public void CanSetAndGetIncludeCategory() + { + const bool TestValue = true; + _testClass.IncludeCategory = TestValue; + Assert.Equal(TestValue, _testClass.IncludeCategory); + } + + [Fact] + public void CanSetAndGetIncludeExceptionStacktrace() + { + const bool TestValue = true; + _testClass.IncludeExceptionStacktrace = TestValue; + Assert.Equal(TestValue, _testClass.IncludeExceptionStacktrace); + } + + [Fact] + public void CheckDefaultLogFormatterOptions() + { + var options = new LogFormatterOptions(); + Assert.True(options.IncludeScopes); + Assert.Equal("yyyy-MM-dd HH:mm:ss.fff", options.TimestampFormat); + Assert.False(options.UseUtcTimestamp); + Assert.True(options.IncludeTimestamp); + Assert.True(options.IncludeLogLevel); + Assert.True(options.IncludeCategory); + Assert.True(options.IncludeExceptionStacktrace); + } + + [Fact] + public void CheckDefaultLogFormatterTheme() + { + var theme = new LogFormatterTheme(); + Assert.True(theme.ColorsEnabled); + } +} +#endif diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/LogFormatterTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/LogFormatterTests.cs new file mode 100644 index 0000000000..007a3ba6cf --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/LogFormatterTests.cs @@ -0,0 +1,526 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Console.Test.Helpers; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Console.Internal.Test; + +[SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Justification = "Test code")] +public class LogFormatterTests +{ + private static readonly string _newLine = Environment.NewLine; + private static readonly Regex _regexToDetectTimestamp = new(@"\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d.*", RegexOptions.Compiled); + + private static void MockClock(LogFormatter formatter) + { + formatter.TimeProvider = new FakeTimeProvider(new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + } + + [Fact] + public void Ctor_ThrowsWhenAnyArgumentsValueIsNull() + { + Assert.Throws(() => new LogFormatter( + Microsoft.Extensions.Options.Options.Create(new LogFormatterOptions()), + Microsoft.Extensions.Options.Options.Create(null!))); + + Assert.Throws(() => new LogFormatter( + Microsoft.Extensions.Options.Options.Create(null!), + Microsoft.Extensions.Options.Options.Create(new LogFormatterTheme()))); + } + + [Fact] + public void Write_WhenScopeIsNullThrows() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = CreateAllLightUpOptions(); + + using var formatter = new LogFormatter(options, theme); + using var writer = new StringWriter(); + + Assert.Throws(() => + formatter.Write(default, null!, writer)); + } + + [Fact] + public void WriteScopes_WhenEmpty_ProducesEmptyString() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = CreateAllLightUpOptions(); + + using var formatter = new LogFormatter(options, theme); + + using var writer = new StringWriter(); + var scopes = new LoggerExternalScopeProvider(); + + formatter.WriteScopes(writer, scopes); + + Assert.Equal(string.Empty, writer.ToString()); + } + + [Fact] + public void WriteScopes_WhenSingleScope_ProducesCorrectOutput() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = CreateAllLightUpOptions(); + + using var formatter = new LogFormatter(options, theme); + + using var writer = new StringWriter(); + var scopes = new LoggerExternalScopeProvider(); + + scopes.Push("key1=value1"); + + formatter.WriteScopes(writer, scopes); + + var m = _newLine + "key1=value1"; + Assert.Equal(m, writer.ToString()); + } + + [Fact] + public void WriteScopes_WhenMultipleScopes_ProducesCorrectOutput() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = CreateAllLightUpOptions(); + + using var formatter = new LogFormatter(options, theme); + + using var writer = new StringWriter(); + var scopes = new LoggerExternalScopeProvider(); + + scopes.Push("key1=value1"); + scopes.Push("key2=value2"); + + formatter.WriteScopes(writer, scopes); + + var text = _newLine + "key1=value1 key2=value2"; + Assert.Equal(text, writer.ToString()); + } + + [Fact] + public void WriteException_WhenNull_DoesNothing() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = CreateAllLightUpOptions(); + + using var formatter = new LogFormatter(options, theme); + + using var writer = new StringWriter(); + + formatter.WriteException(writer, null); + + Assert.Equal(string.Empty, writer.ToString()); + } + + [Fact] + public void WriteException_WhenSingleException_ProducesCorrectOutput() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = CreateAllLightUpOptions(); + + using var formatter = new LogFormatter(options, theme); + + using var writer = new StringWriter(); + var exception = new Exception("My flowers are beautiful"); + + formatter.WriteException(writer, exception); + + var text = _newLine + "0: Exception: My flowers are beautiful (System.Exception)"; + Assert.Equal(text, writer.ToString()); + } + + [Fact] + public void WriteException_WithMultipleInnerException_ProducesCorrectOutput() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = CreateAllLightUpOptions(); + + using var formatter = new LogFormatter(options, theme); + + using var writer = new StringWriter(); + + var exception = + new Exception("I am top level", + new FormatException("I am the first inner", + new ArgumentException("I am an inner squared"))); + + formatter.WriteException(writer, exception); + + var text = + _newLine + "0: Exception: I am top level (System.Exception)" + + _newLine + "0:0: Exception: I am the first inner (System.FormatException)" + + _newLine + "0:0:0: Exception: I am an inner squared (System.ArgumentException)"; + + Assert.Equal(text, writer.ToString()); + } + + [Fact] + public void WriteException_WithAggregatedExceptions_ProducesCorrectOutput() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = CreateAllLightUpOptions(); + + using var formatter = new LogFormatter(options, theme); + + using var writer = new StringWriter(); + + var exception = + new AggregateException(new List(2) + { + new FormatException("I am the first aggregated"), + new FormatException("I am the second aggregated") + }); + + formatter.WriteException(writer, exception); + + var text = + _newLine + "0:0: Exception: I am the first aggregated (System.FormatException)" + + _newLine + "0:1: Exception: I am the second aggregated (System.FormatException)"; + + Assert.EndsWith(text, writer.ToString(), StringComparison.CurrentCulture); + } + + [Fact] + public void WriteException_WhenStackTracePresent_ProducesCorrectOutput() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = CreateAllLightUpOptions(); + + using var formatter = new LogFormatter(options, theme); + + using var writer = new StringWriter(); + var exception = new TestException("My flowers are beautiful", + "--- Lorem ipsum dolor sit amet, consectetur adipiscing elit."); + + formatter.WriteException(writer, exception); + + var text = + _newLine + "0: Exception: My flowers are beautiful (" + typeof(TestException).FullName + ")" + + _newLine + "--- Lorem ipsum dolor sit amet, consectetur adipiscing elit."; + + Assert.Equal(text, writer.ToString()); + } + + [Fact] + public void WriteException_WhenStackTracePresentAndDisabled_DoesNotPrint() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = Microsoft.Extensions.Options.Options.Create(new LogFormatterOptions + { + IncludeExceptionStacktrace = false + }); + + using var formatter = new LogFormatter(options, theme); + + using var writer = new StringWriter(); + var exception = new TestException("My flowers are beautiful", + "--- Lorem ipsum dolor sit amet, consectetur adipiscing elit."); + + formatter.WriteException(writer, exception); + + var text = + _newLine + "0: Exception: My flowers are beautiful (" + typeof(TestException).FullName + ")"; + + Assert.Equal(text, writer.ToString()); + } + + [Fact] + public void Write_WhenDefaultFormatterOptions_IncludesTimestamp() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = CreateAllLightUpOptions(); + + using var formatter = new LogFormatter(options, theme); + MockClock(formatter); + + var scopes = new LoggerExternalScopeProvider(); + using var writer = new StringWriter(); + var entry = CreateLogEntry(); + + formatter.Write(entry, scopes, writer); + + Assert.Matches(_regexToDetectTimestamp, $"{writer}"); + } + + [Fact] + public void Write_WhenLogEntryIsNull_IncludesTimestamp() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = CreateAllLightUpOptions(); + + using var formatter = new LogFormatter(options, theme); + MockClock(formatter); + + var scopes = new LoggerExternalScopeProvider(); + using var writer = new StringWriter(); + + formatter.Write(default, scopes, writer); + Assert.Matches(_regexToDetectTimestamp, $"{writer}"); + } + + [Fact] + public void Write_WhenIncludeTimestampDisabled_OmitsTimestamp() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = Microsoft.Extensions.Options.Options.Create(new LogFormatterOptions + { + IncludeTimestamp = false + }); + + using var formatter = new LogFormatter(options, theme); + + var scopes = new LoggerExternalScopeProvider(); + using var writer = new StringWriter(); + var entry = CreateLogEntry(); + + formatter.Write(entry, scopes, writer); + + var text = $"(info) {entry.State.TraceId} {entry.State.SpanId} Message (Category/0)" + _newLine; + + Assert.Equal(text, writer.ToString()); + } + + [Fact] + public void Write_WhenIncludeSpanIdDisabled_OmitsSpanId() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = Microsoft.Extensions.Options.Options.Create(new LogFormatterOptions + { + IncludeSpanId = false + }); + + using var formatter = new LogFormatter(options, theme); + + var scopes = new LoggerExternalScopeProvider(); + using var writer = new StringWriter(); + var entry = CreateLogEntry(); + + formatter.Write(entry, scopes, writer); + + var text = $"(info) {entry.State.TraceId} Message (Category/0)" + _newLine; + + Assert.EndsWith(text, writer.ToString()); + } + + [Fact] + public void Write_WhenIncludeTraceIdDisabled_OmitsTraceId() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = Microsoft.Extensions.Options.Options.Create(new LogFormatterOptions + { + IncludeTraceId = false + }); + + using var formatter = new LogFormatter(options, theme); + + var scopes = new LoggerExternalScopeProvider(); + using var writer = new StringWriter(); + var entry = CreateLogEntry(); + + formatter.Write(entry, scopes, writer); + + var text = $"(info) {entry.State.SpanId} Message (Category/0)" + _newLine; + + Assert.EndsWith(text, writer.ToString()); + } + + [Fact] + public void Write_WhenIncludeLogLevelDisabled_OmitsLogLevel() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = Microsoft.Extensions.Options.Options.Create(new LogFormatterOptions + { + IncludeLogLevel = false + }); + + using var formatter = new LogFormatter(options, theme); + MockClock(formatter); + + var scopes = new LoggerExternalScopeProvider(); + using var writer = new StringWriter(); + var entry = CreateLogEntry(); + + formatter.Write(entry, scopes, writer); + + Assert.DoesNotContain("[info]", writer.ToString(), StringComparison.CurrentCulture); + Assert.EndsWith(_newLine, writer.ToString(), StringComparison.CurrentCulture); + } + + [Fact] + public void Write_WhenIncludeCategoryDisabled_OmitsCategory() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = Microsoft.Extensions.Options.Options.Create(new LogFormatterOptions + { + IncludeCategory = false + }); + + using var formatter = new LogFormatter(options, theme); + MockClock(formatter); + + var scopes = new LoggerExternalScopeProvider(); + using var writer = new StringWriter(); + var entry = CreateLogEntry(); + + formatter.Write(entry, scopes, writer); + + Assert.DoesNotContain("(Category/0)", writer.ToString(), StringComparison.CurrentCulture); + Assert.EndsWith(_newLine, writer.ToString(), StringComparison.CurrentCulture); + } + + [Fact] + public void Write_WhenScopeExists_ProducesCorrectOutput() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = CreateAllLightUpOptions(); + + using var formatter = new LogFormatter(options, theme); + MockClock(formatter); + + var scopes = new LoggerExternalScopeProvider(); + + scopes.Push("Key1=Value1"); + scopes.Push("Key2=Value2"); + + using var writer = new StringWriter(); + var entry = CreateLogEntry(); + + formatter.Write(entry, scopes, writer); + + var text = _newLine + "Key1=Value1 Key2=Value2" + _newLine; + + Assert.StartsWith(text, writer.ToString(), StringComparison.CurrentCulture); + } + + [Fact] + public void Write_WhenExceptionPassed_ProducesCorrectOutput() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = CreateAllLightUpOptions(); + + using var formatter = new LogFormatter(options, theme); + MockClock(formatter); + + var scopes = new LoggerExternalScopeProvider(); + using var writer = new StringWriter(); + var entry = CreateLogEntry(e: new Exception("Test")); + + formatter.Write(entry, scopes, writer); + + var text = $"(info) {entry.State.TraceId} {entry.State.SpanId} Message (Category/0)" + _newLine + + _newLine + + "0: Exception: Test (System.Exception)" + + _newLine; + + Assert.EndsWith(text, writer.ToString(), StringComparison.CurrentCulture); + } + + [Fact] + public void WriteDateTime_WhenCalled_UsesFormatFromOptions() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = Microsoft.Extensions.Options.Options.Create(new LogFormatterOptions + { + TimestampFormat = "HH:mm" + }); + + using var formatter = new LogFormatter(options, theme); + MockClock(formatter); + + using var writer = new StringWriter(); + + formatter.WriteTimestamp(writer); + + Assert.Matches("[0-9][0-9]:[0-9][0-9]", writer.ToString()); + } + + [Fact] + public void WriteCategory_WhenCalled_WritesCategory() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = CreateAllLightUpOptions(); + + using var formatter = new LogFormatter(options, theme); + + using var writer = new StringWriter(); + + formatter.WriteCategory(writer, "Category", 99); + Assert.Equal("(Category/99)", writer.ToString()); + } + + [Fact] + public void Dispose_WhenCalled_DoesNothing() + { + var theme = CreateNoneColorfulLogFormatterTheme(); + var options = CreateAllLightUpOptions(); + + using var formatter = new LogFormatter(options, theme); + var exception = Record.Exception(() => formatter.Dispose()); + + Assert.Null(exception); + } + + [Fact] + public void WriteLogLevel_WhenEnabledColors_WritesCorrectly() + { + var options = CreateAllLightUpOptions(); + var theme = CreateNoneColorfulLogFormatterTheme(); + theme.Value.ColorsEnabled = true; + + using var formatter = new LogFormatter(options, theme); + + using var writer = new StringWriter(); + + formatter.WriteLogLevel(writer, LogLevel.Warning); + Assert.Equal("(warn) ", writer.ToString()); + } + + private static IOptions CreateAllLightUpOptions() + { + return Microsoft.Extensions.Options.Options.Create(new LogFormatterOptions + { + IncludeCategory = true, + IncludeExceptionStacktrace = true, + IncludeLogLevel = true, + IncludeScopes = true, + IncludeTimestamp = true, + TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff", + UseUtcTimestamp = true + }); + } + + private static IOptions CreateNoneColorfulLogFormatterTheme() + { + return Microsoft.Extensions.Options.Options.Create(new LogFormatterTheme + { + ColorsEnabled = false, + ExceptionStackTrace = Colors.None, + Exception = Colors.None, + Dimmed = Colors.None + }); + } + + private static LogEntry CreateLogEntry(Action>? amend = null, Exception? e = null) + { + static string Formatter(LogEntryCompositeState s, Exception? exception) => "Message"; + LogEntryCompositeState state = new LogEntryCompositeState(null, ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom()); + + var logEntry = new LogEntry(LogLevel.Information, "Category", 0, state, e, Formatter); + + amend?.Invoke(logEntry); + + return logEntry; + } +} +#endif diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/LogLevelExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/LogLevelExtensionsTests.cs new file mode 100644 index 0000000000..5214d26336 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/LogLevelExtensionsTests.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Console.Internal.Test; + +public class LogLevelExtensionsTests +{ + [Fact] + public void InShortString_WhenLogLevelTrace() + { + const LogLevel Level = LogLevel.Trace; + Assert.Equal("trce", Level.InShortString()); + Assert.Equal(Colors.GrayOnBlack, Level.InColor()); + } + + [Fact] + public void InShortString_WhenLogLevelDebug() + { + const LogLevel Level = LogLevel.Debug; + Assert.Equal("dbug", Level.InShortString()); + Assert.Equal(Colors.GrayOnBlack, Level.InColor()); + } + + [Fact] + public void InShortString_WhenLogLevelInformation() + { + const LogLevel Level = LogLevel.Information; + Assert.Equal("info", Level.InShortString()); + Assert.Equal(Colors.DarkGreenOnBlack, Level.InColor()); + } + + [Fact] + public void InShortString_WhenLogLevelWarning() + { + const LogLevel Level = LogLevel.Warning; + Assert.Equal("warn", Level.InShortString()); + Assert.Equal(Colors.YellowOnBlack, Level.InColor()); + } + + [Fact] + public void InShortString_WhenLogLevelError() + { + const LogLevel Level = LogLevel.Error; + Assert.Equal("eror", Level.InShortString()); + Assert.Equal(Colors.BlackOnDarkRed, Level.InColor()); + } + + [Fact] + public void InShortString_WhenLogLevelCritical() + { + const LogLevel Level = LogLevel.Critical; + Assert.Equal("crit", Level.InShortString()); + Assert.Equal(Colors.WhiteOnDarkRed, Level.InColor()); + } + + [Fact] + public void InShortString_WhenLogLevelUnrecognized_UsesSafeDefault() + { + const LogLevel Level = (LogLevel)999; + Assert.Equal("none", Level.InShortString()); + Assert.Equal(Colors.None, Level.InColor()); + } + + [Fact] + public void InColor_WhenLogLevelUnrecognized_UsesSafeDefault() + { + const LogLevel Level = (LogLevel)999; + Assert.Equal(Colors.None, Level.InColor()); + } +} +#endif diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/TestLogEnricher.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/TestLogEnricher.cs new file mode 100644 index 0000000000..8370576c0a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/TestLogEnricher.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Telemetry.Console.Internal.Test; + +internal sealed class TestLogEnricher : ILogEnricher +{ + public const string Key = "Enriched-Key"; + public const string Value = "Enriched-Value"; + + public void Enrich(IEnrichmentPropertyBag enrichmentBag) + => enrichmentBag.Add(Key, Value); +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/TextWriterExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/TextWriterExtensionsTests.cs new file mode 100644 index 0000000000..174fd08e73 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/Internal/TextWriterExtensionsTests.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +using System; +using System.IO; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Console.Internal.Test; + +/// +/// Test cases for . +/// +/// +/// Colorization produces strings that contain something like  +/// - those characters are special escape sequence to enable colouring in a console. +/// +public class TextWriterExtensionsTests +{ + [Fact] + public void Colorize_WhenSimpleExpandInMiddleOfText_IsCorrect() + { + using var writer = new StringWriter(); + + writer.Colorize("Text before {{0}} text after", Colors.BlackOnBlue, "colorized"); + const string Expected = "Text before colorized text after"; + + Assert.Equal(Expected, writer.ToString()); + } + + [Fact] + public void Colorize_WhenExpandInTheBeginning_IsCorrect() + { + using var writer = new StringWriter(); + + writer.Colorize("{{0}} text after", Colors.BlackOnBlue, "colorized"); + const string Expected = "colorized text after"; + + Assert.Equal(Expected, writer.ToString()); + } + + [Fact] + public void Colorize_WhenExpandInTheEnd_IsCorrect() + { + using var writer = new StringWriter(); + + writer.Colorize("Text before {{0}}", Colors.BlackOnBlue, "colorized"); + const string Expected = "Text before colorized"; + + Assert.Equal(Expected, writer.ToString()); + } + + [Fact] + public void Colorize_WhenUsedWithoutExpandParameter_IsCorrect() + { + using var writer = new StringWriter(); + + writer.Colorize("Text before {colorized} text after", Colors.BlackOnBlue); + const string Expected = "Text before colorized text after"; + + Assert.Equal(Expected, writer.ToString()); + } + + [Fact] + public void WriteCoordinate_WhenCalledWithEnumerable_IsCorrect() + { + using var writer = new StringWriter(); + writer.WriteCoordinate(new[] { 1, 2, 3 }, Colors.GrayOnBlack); + string expected = Environment.NewLine + "1:2:3: "; + + Assert.Equal(expected, writer.ToString()); + } + + [Theory] + [InlineData("{{{e{xpanding}}", "expanding}")] + [InlineData("test {", "test ")] + [InlineData("{} test", " test")] + [InlineData("{0}", "")] + [InlineData("{0} {1f}", " f}")] + [InlineData("{2test}", "test}")] + [InlineData("{test}", "test")] + public void Colorize_WhenUsedWithInvalidTemplate_IsCorrect(string format, string expected) + { + using var writer = new StringWriter(); + writer.Colorize(format, Colors.MagentaOnWhite); + + Assert.Equal(expected, writer.ToString()); + } +} +#endif diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/LoggingConsoleExporterTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/LoggingConsoleExporterTests.cs new file mode 100644 index 0000000000..2a946b1ef1 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/LoggingConsoleExporterTests.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +using System; +using Xunit; +using MSOPtions = Microsoft.Extensions.Options.Options; + +namespace Microsoft.Extensions.Telemetry.Console.Test; + +public class LoggingConsoleExporterTests +{ + [Fact] + public void Ctor_GivenInvalidArguments_ThrowsException() + { + Assert.Throws(() => new LoggingConsoleExporter(null!)); + Assert.Throws(() => new LoggingConsoleExporter(MSOPtions.Create((LoggingConsoleOptions)null!))); + } +} +#endif diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/LoggingConsoleExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/LoggingConsoleExtensionsTests.cs new file mode 100644 index 0000000000..c1867fc271 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/LoggingConsoleExtensionsTests.cs @@ -0,0 +1,142 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +#if NET5_0_OR_GREATER +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +#endif +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Logging; +#if NET5_0_OR_GREATER +using Microsoft.Extensions.Options; +#endif +using Microsoft.Extensions.Telemetry.Logging; +#if NET5_0_OR_GREATER +using Moq; +#endif +using OpenTelemetry; +using OpenTelemetry.Logs; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Console.Test; + +public sealed class LoggingConsoleExtensionsTests +{ + [Fact] + public void AddConsoleExporter_GivenInvalidArguments_ThrowsException() + { + Assert.Throws(() => + ((ILoggingBuilder)null!).AddConsoleExporter()); +#if NET5_0_OR_GREATER + Assert.Throws(() => + ((ILoggingBuilder)null!).AddConsoleExporter((Action)null!)); + Assert.Throws(() => + Mock.Of().AddConsoleExporter((Action)null!)); + Assert.Throws(() => + ((ILoggingBuilder)null!).AddConsoleExporter((IConfigurationSection)null!)); + Assert.Throws(() => + Mock.Of().AddConsoleExporter((IConfigurationSection)null!)); +#endif + } + + [Fact] + public void AddConsoleExporter_GivenNoArguments_RegistersRequiredServices() + { + using var host = FakeHost.CreateBuilder(options => options.FakeLogging = false) + .ConfigureLogging(builder => builder + .AddOpenTelemetryLogging() + .AddConsoleExporter()) + .Build(); + + var exporter = host.Services.GetService>(); + Assert.NotNull(exporter); + Assert.IsAssignableFrom(exporter); + + var processor = host.Services.GetService>(); + Assert.NotNull(processor); + Assert.IsAssignableFrom(processor); + } + +#if NET5_0_OR_GREATER + [Fact] + public void AddConsoleExporter_GivenConfigAction_RegistersRequiredServices() + { + using var host = FakeHost.CreateBuilder(options => options.FakeLogging = false) + .ConfigureLogging(builder => builder + .AddOpenTelemetryLogging() + .AddConsoleExporter( + exporterOptions => + { + exporterOptions.IncludeLogLevel = false; + exporterOptions.IncludeCategory = false; + exporterOptions.ColorsEnabled = false; + exporterOptions.DimmedColor = ConsoleColor.Green; + exporterOptions.ExceptionStackTraceBackgroundColor = ConsoleColor.Yellow; + })) + .Build(); + + var exporter = host.Services.GetService>(); + Assert.NotNull(exporter); + Assert.IsAssignableFrom(exporter); + + var processor = host.Services.GetService>(); + Assert.NotNull(processor); + Assert.IsAssignableFrom(processor); + + var options = host.Services.GetService>(); + Assert.NotNull(options); + Assert.NotNull(options!.Value); + Assert.False(options!.Value.IncludeLogLevel); + Assert.False(options!.Value.IncludeCategory); + Assert.False(options!.Value.ColorsEnabled); + Assert.Equal(ConsoleColor.Green, options!.Value.DimmedColor); + Assert.Equal(ConsoleColor.Yellow, options!.Value.ExceptionStackTraceBackgroundColor); + } + + [Fact] + public void AddConsoleExporter_GivenConfigSecion_RegistersRequiredServices() + { + using var host = FakeHost.CreateBuilder(options => options.FakeLogging = false) + .ConfigureLogging(builder => builder + .AddOpenTelemetryLogging() + .AddConsoleExporter(GetConsoleLogFormatterConfigSection())) + .Build(); + + var exporter = host.Services.GetService>(); + Assert.NotNull(exporter); + Assert.IsAssignableFrom(exporter); + + var processor = host.Services.GetService>(); + Assert.NotNull(processor); + Assert.IsAssignableFrom(processor); + var options = host.Services.GetService>(); + Assert.NotNull(options); + Assert.NotNull(options!.Value); + Assert.False(options!.Value.ColorsEnabled); + Assert.Equal(ConsoleColor.Cyan, options!.Value.DimmedColor); + } + + private static IConfigurationSection GetConsoleLogFormatterConfigSection() + { + LoggingConsoleOptions options; + + return new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { + $"{nameof(LoggingConsoleOptions)}:{nameof(options.ColorsEnabled)}", + "false" + }, + { + $"{nameof(LoggingConsoleOptions)}:{nameof(options.DimmedColor)}", + ConsoleColor.Cyan.ToString() + }, + }) + .Build() + .GetSection($"{nameof(LoggingConsoleOptions)}"); + } +#endif +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/LoggingConsoleOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/LoggingConsoleOptionsTests.cs new file mode 100644 index 0000000000..f369ca7389 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Logging/LoggingConsoleOptionsTests.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +using System; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Console.Test; + +public class LoggingConsoleOptionsTests +{ + private const string DefaultTimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + private const ConsoleColor DefaultDimmedTextColor = ConsoleColor.DarkGray; + private const ConsoleColor DefaultExceptionTextColor = ConsoleColor.Red; + private const ConsoleColor DefaultExceptionStackTraceTextColor = ConsoleColor.DarkRed; + + [Fact] + public void TestDefaultOptions() + { + var defaultOptions = new LoggingConsoleOptions(); + + Assert.True(defaultOptions.IncludeScopes); + Assert.True(defaultOptions.IncludeExceptionStacktrace); + Assert.True(defaultOptions.IncludeLogLevel); + Assert.True(defaultOptions.IncludeCategory); + Assert.True(defaultOptions.IncludeTimestamp); + Assert.True(defaultOptions.IncludeTraceId); + Assert.True(defaultOptions.IncludeSpanId); + Assert.False(defaultOptions.UseUtcTimestamp); + + Assert.True(defaultOptions.ColorsEnabled); + + Assert.Equal(DefaultDimmedTextColor, defaultOptions.DimmedColor); + Assert.Null(defaultOptions.DimmedBackgroundColor); + + Assert.Equal(DefaultExceptionStackTraceTextColor, defaultOptions.ExceptionStackTraceColor); + Assert.Null(defaultOptions.ExceptionStackTraceBackgroundColor); + + Assert.Equal(DefaultExceptionTextColor, defaultOptions.ExceptionColor); + Assert.Null(defaultOptions.ExceptionBackgroundColor); + + Assert.Equal(DefaultTimestampFormat, defaultOptions.TimestampFormat); + } +} +#endif diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Microsoft.Extensions.Telemetry.Console.Tests.csproj b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Microsoft.Extensions.Telemetry.Console.Tests.csproj new file mode 100644 index 0000000000..62233056b4 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/Microsoft.Extensions.Telemetry.Console.Tests.csproj @@ -0,0 +1,21 @@ + + + Microsoft.Extensions.Telemetry + Unit tests for Microsoft.Extensions.Telemetry.Console. + + + + + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/appsettings.json b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/appsettings.json new file mode 100644 index 0000000000..dab8485ab5 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Console.Tests/appsettings.json @@ -0,0 +1,12 @@ +{ + "R9": { + "Extensions.Logging.Exporters": { + "FormatterOptions": { + "SingleLine": false, + "IncludeScopes": true, + "TimestampFormat": "HH:mm:ss", + "UseUtcTimestamp": true + } + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLogCollectorOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLogCollectorOptionsTests.cs new file mode 100644 index 0000000000..4a49aa14f2 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLogCollectorOptionsTests.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Testing.Logging.Test; + +public class FakeLogCollectorOptionsTests +{ + [Fact] + public void Defaults() + { + var options = new FakeLogCollectorOptions(); + Assert.Empty(options.FilteredCategories); + Assert.Empty(options.FilteredLevels); + Assert.True(options.CollectRecordsForDisabledLogLevels); + Assert.Equal(System.TimeProvider.System, options.TimeProvider); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLogCollectorTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLogCollectorTests.cs new file mode 100644 index 0000000000..1409ad24ca --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLogCollectorTests.cs @@ -0,0 +1,183 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Microsoft.Extensions.Time.Testing; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Extensions.Telemetry.Testing.Logging.Test; + +public class FakeLogCollectorTests +{ + private class Output : ITestOutputHelper + { + public string Last { get; private set; } = string.Empty; + + public void WriteLine(string message) + { + Last = message; + } + + public void WriteLine(string format, params object[] args) => WriteLine(string.Format(CultureInfo.InvariantCulture, format, args)); + } + + [Fact] + public void Basic() + { + var output = new Output(); + + var timeProvider = new FakeTimeProvider(); + timeProvider.Advance(); + + var options = new FakeLogCollectorOptions + { + OutputSink = output.WriteLine, + TimeProvider = timeProvider, + }; + + var collector = FakeLogCollector.Create(options); + var logger = new FakeLogger(collector); + + logger.LogTrace("Hello world!"); + Assert.Equal("[00:00.001, trace] Hello world!", output.Last); + + logger.LogDebug("Hello world!"); + Assert.Equal("[00:00.001, debug] Hello world!", output.Last); + + logger.LogInformation("Hello world!"); + Assert.Equal("[00:00.001, info] Hello world!", output.Last); + + logger.LogWarning("Hello world!"); + Assert.Equal("[00:00.001, warn] Hello world!", output.Last); + + logger.LogError("Hello world!"); + Assert.Equal("[00:00.001, error] Hello world!", output.Last); + + logger.LogCritical("Hello world!"); + Assert.Equal("[00:00.001, crit] Hello world!", output.Last); + + logger.Log(LogLevel.None, "Hello world!"); + Assert.Equal("[00:00.001, none] Hello world!", output.Last); + + logger.Log((LogLevel)42, "Hello world!"); + Assert.Equal("[00:00.001, invld] Hello world!", output.Last); + } + + [Fact] + public void DIEntryPoint() + { + var output = new Output(); + + var timeProvider = new FakeTimeProvider(); + timeProvider.Advance(); + + var options = new FakeLogCollectorOptions + { + OutputSink = output.WriteLine, + TimeProvider = timeProvider, + }; + + var collector = new FakeLogCollector(Microsoft.Extensions.Options.Options.Create(options)); + var logger = new FakeLogger(collector); + + logger.LogTrace("Hello world!"); + Assert.Equal("[00:00.001, trace] Hello world!", output.Last); + + logger.LogDebug("Hello world!"); + Assert.Equal("[00:00.001, debug] Hello world!", output.Last); + + logger.LogInformation("Hello world!"); + Assert.Equal("[00:00.001, info] Hello world!", output.Last); + + logger.LogWarning("Hello world!"); + Assert.Equal("[00:00.001, warn] Hello world!", output.Last); + + logger.LogError("Hello world!"); + Assert.Equal("[00:00.001, error] Hello world!", output.Last); + + logger.LogCritical("Hello world!"); + Assert.Equal("[00:00.001, crit] Hello world!", output.Last); + + logger.Log(LogLevel.None, "Hello world!"); + Assert.Equal("[00:00.001, none] Hello world!", output.Last); + + logger.Log((LogLevel)42, "Hello world!"); + Assert.Equal("[00:00.001, invld] Hello world!", output.Last); + } + + [Fact] + public void DIEntryPoint_NullChecks() + { + Assert.Throws(() => new FakeLogCollector(null!)); + Assert.Throws(() => new FakeLogCollector(Microsoft.Extensions.Options.Options.Create((FakeLogCollectorOptions)null!))); + } + + [Fact] + public void TestOutputHelperExtensionsNonGeneric() + { + var output = new Output(); + + var logger = new FakeLogger(output.WriteLine, "Storage"); + + logger.LogTrace("Hello world!"); + Assert.Contains("trace] Hello world!", output.Last); + + logger.LogDebug("Hello world!"); + Assert.Contains("debug] Hello world!", output.Last); + + logger.LogInformation("Hello world!"); + Assert.Contains("info] Hello world!", output.Last); + + logger.LogWarning("Hello world!"); + Assert.Contains("warn] Hello world!", output.Last); + + logger.LogError("Hello world!"); + Assert.Contains("error] Hello world!", output.Last); + + logger.LogCritical("Hello world!"); + Assert.Contains("crit] Hello world!", output.Last); + + logger.Log(LogLevel.None, "Hello world!"); + Assert.Contains("none] Hello world!", output.Last); + + logger.Log((LogLevel)42, "Hello world!"); + Assert.Contains("invld] Hello world!", output.Last); + } + + [Fact] + public void TestOutputHelperExtensionsGeneric() + { + var output = new Output(); + + var logger = new FakeLogger(output.WriteLine); + + logger.LogTrace("Hello world!"); + Assert.Contains("trace] Hello world!", output.Last); + + logger.LogDebug("Hello world!"); + Assert.Contains("debug] Hello world!", output.Last); + + logger.LogInformation("Hello world!"); + Assert.Contains("info] Hello world!", output.Last); + + logger.LogWarning("Hello world!"); + Assert.Contains("warn] Hello world!", output.Last); + + logger.LogError("Hello world!"); + Assert.Contains("error] Hello world!", output.Last); + + logger.LogCritical("Hello world!"); + Assert.Contains("crit] Hello world!", output.Last); + + logger.Log(LogLevel.None, "Hello world!"); + Assert.Contains("none] Hello world!", output.Last); + + logger.Log((LogLevel)42, "Hello world!"); + Assert.Contains("invld] Hello world!", output.Last); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLoggerExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLoggerExtensionsTests.cs new file mode 100644 index 0000000000..2c0e5f744b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLoggerExtensionsTests.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Testing.Logging.Test; + +public class FakeLoggerExtensionsTests +{ + [Fact] + public void Basic() + { + using var serviceProvider = new ServiceCollection() + .AddFakeLogging() + .BuildServiceProvider(); + + var factory = serviceProvider.GetService(); + var collector = serviceProvider.GetFakeLogCollector(); + + var logger = factory!.CreateLogger("R9"); + Assert.Equal(0, collector.Count); + logger.LogError("M1"); + Assert.Equal(1, collector.Count); + } + + [Fact] + public void WithDelegate() + { + using var serviceProvider = new ServiceCollection() + .AddFakeLogging(options => options.FilteredCategories.Add("Storage")) + .BuildServiceProvider(); + + var factory = serviceProvider.GetService(); + var collector = serviceProvider.GetFakeLogCollector(); + + var logger = factory!.CreateLogger("Storage"); + Assert.Equal(0, collector.Count); + logger.LogError("M1"); + Assert.Equal(1, collector.Count); + + logger = factory.CreateLogger("Network"); + logger.LogError("M2"); + Assert.Equal(1, collector.Count); + } + + [Fact] + public void WithConfig() + { + var configRoot = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { $"Logging:{nameof(FakeLogCollectorOptions.FilteredCategories)}:0", "Storage" }, + }) + .Build(); + + var section = configRoot.GetSection("Logging"); + using var serviceProvider = new ServiceCollection() + .AddFakeLogging(section) + .BuildServiceProvider(); + + var factory = serviceProvider.GetService()!; + var collector = serviceProvider.GetFakeLogCollector(); + + var logger = factory.CreateLogger("Storage"); + Assert.Equal(0, collector.Count); + logger.LogError("M1"); + Assert.Equal(1, collector.Count); + + logger = factory.CreateLogger("Network"); + logger.LogError("M2"); + Assert.Equal(1, collector.Count); + } + + [Fact] + public void Exception() + { + using var serviceProvider = new ServiceCollection() + .BuildServiceProvider(); + + Assert.Throws(() => serviceProvider.GetFakeLogCollector()); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLoggerProviderTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLoggerProviderTests.cs new file mode 100644 index 0000000000..4313ac751f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLoggerProviderTests.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Testing.Logging.Test; + +public class FakeLoggerProviderTests +{ + [Fact] + public void Basic() + { + using var loggerProvider = new FakeLoggerProvider(); + + var logger = loggerProvider.CreateLogger("Storage"); + Assert.Equal(logger.Collector, loggerProvider.Collector); + logger.LogDebug("M1"); + Assert.Equal(1, logger.Collector.Count); + Assert.Equal("Storage", logger.LatestRecord.Category); + + logger = loggerProvider.CreateLogger(null); + Assert.Equal(logger.Collector, loggerProvider.Collector); + logger.LogDebug("M2"); + Assert.Equal(2, logger.Collector.Count); + Assert.Null(logger.LatestRecord.Category); + + logger = new FakeLogger(loggerProvider.Collector); + Assert.Equal(logger.Collector, loggerProvider.Collector); + logger.LogDebug("M3"); + Assert.Equal(3, logger.Collector.Count); + Assert.Equal("Microsoft.Extensions.Telemetry.Testing.Logging.Test.FakeLoggerProviderTests", logger.LatestRecord.Category); + } + + [Fact] + public void ScopeProvider() + { + using var provider = new FakeLoggerProvider(); + var l1 = provider.CreateLogger(null); + using var factory = new LoggerFactory(); + factory.AddProvider(provider); + var l2 = factory.CreateLogger("Storage"); + + l1.LogDebug("M1"); + l2.LogDebug("M2"); + + var records = provider.Collector.GetSnapshot(); + Assert.Equal(2, records.Count); + Assert.Null(records[0].Category); + Assert.Equal("Storage", records[1].Category); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLoggerTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLoggerTests.cs new file mode 100644 index 0000000000..7b6fbd3b06 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/FakeLoggerTests.cs @@ -0,0 +1,278 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Testing.Logging.Test; + +public class FakeLoggerTests +{ + [Fact] + public void Basic() + { + var timeProvider = new FakeTimeProvider(); + var options = new FakeLogCollectorOptions + { + TimeProvider = timeProvider, + }; + + var collector = FakeLogCollector.Create(options); + var logger = new FakeLogger(collector); + logger.LogInformation("Hello"); + logger.LogError("World"); + + var records = logger.Collector.GetSnapshot(); + Assert.Equal(2, records.Count); + Assert.Equal(2, logger.Collector.Count); + + Assert.Equal("Hello", records[0].Message); + Assert.Equal(LogLevel.Information, records[0].Level); + Assert.Null(records[0].Exception); + Assert.Null(records[0].Category); + Assert.True(records[0].LevelEnabled); + Assert.Empty(records[0].Scopes); + Assert.Equal(0, records[0].Id.Id); + Assert.Equal("[00:00.000, info] Hello", records[0].ToString()); + + Assert.Equal("World", records[1].Message); + Assert.Equal(LogLevel.Error, records[1].Level); + Assert.Null(records[0].Exception); + Assert.Null(records[0].Category); + Assert.Empty(records[0].Scopes); + Assert.True(records[0].LevelEnabled); + Assert.Equal(0, records[0].Id.Id); + + Assert.Equal("World", logger.LatestRecord.Message); + Assert.Equal(LogLevel.Error, logger.LatestRecord.Level); + Assert.Null(logger.LatestRecord.Exception); + Assert.Null(logger.LatestRecord.Category); + Assert.True(records[0].LevelEnabled); + Assert.Empty(logger.LatestRecord.Scopes); + Assert.Equal(0, logger.LatestRecord.Id.Id); + + logger.Collector.Clear(); + Assert.Equal(0, logger.Collector.Count); + Assert.Empty(logger.Collector.GetSnapshot()); + Assert.Throws(() => logger.LatestRecord.Level); + } + + [Fact] + public void State() + { + var logger = new FakeLogger(); + logger.Log(LogLevel.Error, new EventId(0), 42, null, (_, _) => "MESSAGE"); + Assert.Equal("42", (string)logger.LatestRecord.State!); + + logger = new FakeLogger(); + + var l = new List> + { + new KeyValuePair("K0", "V0"), + new KeyValuePair("K1", "V1"), + new KeyValuePair("K2", null), + new KeyValuePair("K3", new[] { 0, 1, 2 }), + }; + + logger.Log(LogLevel.Debug, new EventId(1), l, null, (_, _) => "Nothing"); + + Assert.Equal(1, logger.Collector.Count); + + var stateList = (List>)logger.LatestRecord.State!; + Assert.Equal("K0", stateList[0].Key); + Assert.Equal("V0", stateList[0].Value); + Assert.Equal("K1", stateList[1].Key); + Assert.Equal("V1", stateList[1].Value); + Assert.Equal("K2", stateList[2].Key); + Assert.Null(stateList[2].Value); + Assert.Equal("K3", stateList[3].Key); + Assert.Equal("[\"0\",\"1\",\"2\"]", stateList[3].Value); + + logger = new FakeLogger(); + logger.Log(LogLevel.Error, new EventId(0), null, null, (_, _) => "MESSAGE"); + Assert.Null(logger.LatestRecord.State); + + logger = new FakeLogger(); + TestLog.Hello(logger, "Bob"); + var ss = logger.LatestRecord.StructuredState!; + Assert.Equal("name", ss[0].Key); + Assert.Equal("Bob", ss[0].Value); + Assert.Equal("{OriginalFormat}", ss[1].Key); + Assert.Equal("Hello {name}", ss[1].Value); + } + + [Fact] + public void StateInvariant() + { + var oldCulture = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("fr-CA"); + + var dt = new DateTime(2022, 5, 22); + + try + { + var logger = new FakeLogger(); + logger.Log(LogLevel.Error, new EventId(0), dt, null, (_, _) => "MESSAGE"); + Assert.Equal(dt.ToString(CultureInfo.InvariantCulture), (string)logger.LatestRecord.State!); + + var l = new List> + { + new KeyValuePair("K0", dt), + }; + + logger.Log(LogLevel.Debug, new EventId(1), l, null, (_, _) => "Nothing"); + Assert.Equal(dt.ToString(CultureInfo.InvariantCulture), logger.LatestRecord.StructuredState![0].Value!); + } + finally + { + Thread.CurrentThread.CurrentCulture = oldCulture; + } + } + + [Fact] + public void EnableControl() + { + var logger = new FakeLogger(); + + Assert.True(logger.IsEnabled(LogLevel.Trace)); + Assert.True(logger.IsEnabled(LogLevel.Debug)); + Assert.True(logger.IsEnabled(LogLevel.Information)); + Assert.True(logger.IsEnabled(LogLevel.Warning)); + Assert.True(logger.IsEnabled(LogLevel.Error)); + Assert.True(logger.IsEnabled(LogLevel.Critical)); + Assert.True(logger.IsEnabled((LogLevel)42)); + + logger.ControlLevel(LogLevel.Debug, false); + logger.ControlLevel((LogLevel)42, false); + + Assert.True(logger.IsEnabled(LogLevel.Trace)); + Assert.False(logger.IsEnabled(LogLevel.Debug)); + Assert.True(logger.IsEnabled(LogLevel.Information)); + Assert.True(logger.IsEnabled(LogLevel.Warning)); + Assert.True(logger.IsEnabled(LogLevel.Error)); + Assert.True(logger.IsEnabled(LogLevel.Critical)); + Assert.False(logger.IsEnabled((LogLevel)42)); + + logger.LogDebug("This record should be marked as being disabled"); + Assert.Equal(1, logger.Collector.Count); + Assert.False(logger.LatestRecord.LevelEnabled); + + logger.ControlLevel(LogLevel.Debug, true); + + logger.LogDebug("This record should be marked as being enabled"); + Assert.Equal(2, logger.Collector.Count); + Assert.True(logger.LatestRecord.LevelEnabled); + } + + [Fact] + public void FilterByEnabled() + { + var options = new FakeLogCollectorOptions + { + CollectRecordsForDisabledLogLevels = false + }; + var collector = FakeLogCollector.Create(options); + var logger = new FakeLogger(collector); + + logger.LogDebug("BEFORE"); + logger.ControlLevel(LogLevel.Debug, false); + logger.LogDebug("AFTER"); + + Assert.Equal(1, logger.Collector.Count); + Assert.Equal("BEFORE", logger.LatestRecord.Message); + } + + [Fact] + public void FilterByLevel() + { + var options = new FakeLogCollectorOptions + { + FilteredLevels = new HashSet() + }; + options.FilteredLevels.Add(LogLevel.Error); + + var collector = FakeLogCollector.Create(options); + var logger = new FakeLogger(collector); + + logger.LogDebug("M1"); + logger.LogInformation("M2"); + logger.LogWarning("M3"); + logger.LogError("M4"); + logger.LogCritical("M5"); + + Assert.Equal(1, logger.Collector.Count); + Assert.Equal("M4", logger.LatestRecord.Message); + } + + [Fact] + public void FilterByCategory() + { + var options = new FakeLogCollectorOptions + { + FilteredCategories = new HashSet() + }; + options.FilteredCategories.Add("Storage"); + + var collector = FakeLogCollector.Create(options); + + var logger = new FakeLogger(collector, category: null); + logger.LogDebug("M1"); + + logger = new FakeLogger(collector, "Network"); + logger.LogDebug("M1"); + + logger = new FakeLogger(collector, "Storage"); + logger.LogDebug("M2"); + + Assert.Equal(1, logger.Collector.Count); + Assert.Equal("M2", logger.LatestRecord.Message); + } + + [Fact] + public void Clock() + { + var timeProvider = new FakeTimeProvider(); + var options = new FakeLogCollectorOptions + { + TimeProvider = timeProvider, + }; + + var start = timeProvider.GetUtcNow(); + var collector = FakeLogCollector.Create(options); + + var logger = new FakeLogger(collector); + logger.LogDebug("M1"); + logger.LogDebug("M2"); + + timeProvider.Advance(); + logger.LogDebug("M3"); + logger.LogDebug("M4"); + + var records = collector.GetSnapshot(); + Assert.Equal(start, records[0].Timestamp); + Assert.Equal(start, records[1].Timestamp); + Assert.Equal(start + TimeSpan.FromMilliseconds(1), records[2].Timestamp); + Assert.Equal(start + TimeSpan.FromMilliseconds(1), records[3].Timestamp); + } + + [Fact] + public void Scopes() + { + var logger = new FakeLogger(); + + using var s1 = logger.BeginScope(42); + using var s2 = logger.BeginScope("Hello World"); + logger.LogInformation("Main message"); + + Assert.Equal(1, logger.Collector.Count); + Assert.Equal(2, logger.LatestRecord.Scopes.Count); + Assert.Equal(42, (int)logger.LatestRecord.Scopes[0]!); + Assert.Equal("Hello World", (string)logger.LatestRecord.Scopes[1]!); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/TestLog.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/TestLog.cs new file mode 100644 index 0000000000..5cae275784 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Logging/TestLog.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Logging; + +namespace Microsoft.Extensions.Telemetry.Testing.Logging.Test; + +internal static partial class TestLog +{ + [LogMethod(0, LogLevel.Error, "Hello {name}")] + public static partial void Hello(this ILogger logger, string name); +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.Counter.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.Counter.cs new file mode 100644 index 0000000000..4a35f8782c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.Counter.cs @@ -0,0 +1,210 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Testing.Metering; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Testing.Metering.Test; + +public partial class MetricCollectorTests +{ + [Fact] + public void Counter_BasicTest() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + CounterBasicTest(meter, metricCollector, 2, 5, 10, 17); + CounterBasicTest(meter, metricCollector, 20, 50, 100, 170); + CounterBasicTest(meter, metricCollector, 200, 500, 1000, 1700); + CounterBasicTest(meter, metricCollector, 2000L, 5000L, 10000L, 17000L); + CounterBasicTest(meter, metricCollector, 1.22f, 3.44f, 0, 1.22f + 3.44f); + CounterBasicTest(meter, metricCollector, 5.22, 6.44, 10, 5.22 + 6.44 + 10); + CounterBasicTest(meter, metricCollector, 0.99m, 15, 25.99m, 41.98m); + } + + [Fact] + public void Counter_StringDimensionsAreHandled() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + CounterWithStringDimensionsTest(meter, metricCollector, byte.MinValue); + CounterWithStringDimensionsTest(meter, metricCollector, short.MaxValue); + CounterWithStringDimensionsTest(meter, metricCollector, int.MaxValue); + CounterWithStringDimensionsTest(meter, metricCollector, long.MaxValue); + CounterWithStringDimensionsTest(meter, metricCollector, float.MaxValue); + CounterWithStringDimensionsTest(meter, metricCollector, double.MaxValue); + CounterWithStringDimensionsTest(meter, metricCollector, decimal.MaxValue); + } + + [Fact] + public void Counter_NumericDimensionsAreHandled() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + CounterWithNumericDimensionsTest(meter, metricCollector, byte.MinValue); + CounterWithNumericDimensionsTest(meter, metricCollector, short.MaxValue); + CounterWithNumericDimensionsTest(meter, metricCollector, int.MaxValue); + CounterWithNumericDimensionsTest(meter, metricCollector, long.MaxValue); + CounterWithNumericDimensionsTest(meter, metricCollector, float.MaxValue); + CounterWithNumericDimensionsTest(meter, metricCollector, double.MaxValue); + CounterWithNumericDimensionsTest(meter, metricCollector, decimal.MaxValue); + } + + [Fact] + public void Counter_ArrayDimensionsAreHandled() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + CounterWithArrayDimensionsTest(meter, metricCollector, byte.MinValue); + CounterWithArrayDimensionsTest(meter, metricCollector, short.MaxValue); + CounterWithArrayDimensionsTest(meter, metricCollector, int.MaxValue); + CounterWithArrayDimensionsTest(meter, metricCollector, long.MaxValue); + CounterWithArrayDimensionsTest(meter, metricCollector, float.MaxValue); + CounterWithArrayDimensionsTest(meter, metricCollector, double.MaxValue); + CounterWithArrayDimensionsTest(meter, metricCollector, decimal.MaxValue); + } + + private static void CounterBasicTest(Meter meter, MetricCollector metricCollector, T value, T valueToAdd, T valueToAdd1, T totalSum) + where T : struct + { + var counter1 = meter.CreateCounter(Guid.NewGuid().ToString()); + var holder1 = metricCollector.GetCounterValues(counter1.Name); + + Assert.NotNull(holder1); + + counter1.Add(value); + + var recordedValue1 = metricCollector.GetCounterValue(counter1.Name); + + Assert.NotNull(recordedValue1); + Assert.Equal(value, recordedValue1.Value); + + counter1.Add(valueToAdd); + counter1.Add(valueToAdd1); + + var recordedValue2 = metricCollector.GetCounterValue(counter1.Name); + + Assert.Equal(totalSum, recordedValue2!.Value); + + var counter2 = meter.CreateCounter(Guid.NewGuid().ToString()); + var holder2 = metricCollector.GetCounterValues(counter2.Name); + + Assert.NotNull(holder2); + + counter2.Add(value); + + Assert.Equal(value, metricCollector.GetCounterValue(counter2.Name)!.Value); + + counter2.Add(valueToAdd); + counter2.Add(valueToAdd1); + + Assert.Equal(totalSum, metricCollector.GetCounterValue(counter2.Name)!.Value); + } + + private static void CounterWithStringDimensionsTest(Meter meter, MetricCollector metricCollector, T value) + where T : struct + { + var counter = meter.CreateCounter(Guid.NewGuid().ToString()); + + // No dimensions + counter.Add(value); + + Assert.Equal(value, metricCollector.GetCounterValue(counter.Name)!.Value); + + // One dimension + var dimension1 = Guid.NewGuid().ToString(); + var dimension1Val = Guid.NewGuid().ToString(); + counter.Add(value, new KeyValuePair(dimension1, dimension1Val)); + + Assert.Equal(value, metricCollector.GetCounterValue(counter.Name, new KeyValuePair(dimension1, dimension1Val))!.Value); + + // Two dimensions + var dimension2 = Guid.NewGuid().ToString(); + var dimension2Val = Guid.NewGuid().ToString(); + counter.Add(value, new KeyValuePair(dimension1, dimension1Val), new KeyValuePair(dimension2, dimension2Val)); + + Assert.Equal(value, + metricCollector.GetCounterValue(counter.Name, new KeyValuePair(dimension2, dimension2Val), new KeyValuePair(dimension1, dimension1Val))!.Value); + } + + private static void CounterWithNumericDimensionsTest(Meter meter, MetricCollector metricCollector, T value) + where T : struct + { + var counter = meter.CreateCounter(Guid.NewGuid().ToString()); + + // One dimension + var intDimension = Guid.NewGuid().ToString(); + const int IntVal = 15555; + counter.Add(value, new KeyValuePair(intDimension, IntVal)); + + Assert.Equal(value, metricCollector.GetCounterValue(counter.Name, new KeyValuePair(intDimension, IntVal))!.Value); + + // Two dimensions + var doubleDimension = Guid.NewGuid().ToString(); + const double DoubleVal = 1111.9999d; + counter.Add(value, new KeyValuePair(intDimension, IntVal), new KeyValuePair(doubleDimension, DoubleVal)); + + Assert.Equal(value, + metricCollector.GetCounterValue(counter.Name, new KeyValuePair(intDimension, IntVal), new KeyValuePair(doubleDimension, DoubleVal))!.Value); + + // Three dimensions + var longDimension = Guid.NewGuid().ToString(); + const long LongVal = 1_999_988_887_777_111L; + + counter.Add(value, new KeyValuePair(intDimension, IntVal), new KeyValuePair(longDimension, LongVal), + new KeyValuePair(doubleDimension, DoubleVal)); + + var actualValue = metricCollector.GetCounterValue(counter.Name, + new KeyValuePair(longDimension, LongVal), + new KeyValuePair(intDimension, IntVal), + new KeyValuePair(doubleDimension, DoubleVal)); + + Assert.Equal(value, actualValue!.Value); + } + + private static void CounterWithArrayDimensionsTest(Meter meter, MetricCollector metricCollector, T value) + where T : struct + { + var counter = meter.CreateCounter(Guid.NewGuid().ToString()); + + // One dimension + var intArrayDimension = Guid.NewGuid().ToString(); + int[] intArrVal = new[] { 12, 55, 2023 }; + counter.Add(value, new KeyValuePair(intArrayDimension, intArrVal)); + + Assert.Equal(value, metricCollector.GetCounterValue(counter.Name, new KeyValuePair(intArrayDimension, intArrVal))!.Value); + + // Two dimensions + var doubleArrayDimension = Guid.NewGuid().ToString(); + double[] doubleArrVal = new[] { 1111.9999d, 0, 3.1415 }; + counter.Add(value, new KeyValuePair(intArrayDimension, intArrVal), new KeyValuePair(doubleArrayDimension, doubleArrVal)); + + var actualValue = metricCollector.GetCounterValue(counter.Name, + new KeyValuePair(intArrayDimension, intArrVal), + new KeyValuePair(doubleArrayDimension, doubleArrVal)); + + Assert.Equal(value, actualValue!.Value); + + // Three dimensions + var longArrayDimension = Guid.NewGuid().ToString(); + long[] longArrVal = new[] { 1_999_988_887_777_111L, 1_111_222_333_444_555L, 1_999_988_887_777_111L, 0 }; + + counter.Add(value, new KeyValuePair(intArrayDimension, intArrVal), + new KeyValuePair(doubleArrayDimension, doubleArrVal), + new KeyValuePair(longArrayDimension, longArrVal)); + + actualValue = metricCollector.GetCounterValue(counter.Name, + new KeyValuePair(longArrayDimension, longArrVal), + new KeyValuePair(intArrayDimension, intArrVal), + new KeyValuePair(doubleArrayDimension, doubleArrVal)); + + Assert.Equal(value, actualValue!.Value); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.Histogram.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.Histogram.cs new file mode 100644 index 0000000000..e840d18651 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.Histogram.cs @@ -0,0 +1,209 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Testing.Metering; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Testing.Metering.Test; + +public partial class MetricCollectorTests +{ + [Fact] + public void Histogram_BasicTest() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + HistogramBasicTest(meter, metricCollector, 2, 5); + HistogramBasicTest(meter, metricCollector, 20, 50); + HistogramBasicTest(meter, metricCollector, 200, 500); + HistogramBasicTest(meter, metricCollector, 2000L, 5000L); + HistogramBasicTest(meter, metricCollector, 1.22f, 3.44f); + HistogramBasicTest(meter, metricCollector, 5.22, 6.44); + HistogramBasicTest(meter, metricCollector, 0.99m, 15); + } + + [Fact] + public void Histogram_StringDimensionsAreHandled() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + HistogramWithStringDimensionsTest(meter, metricCollector, byte.MinValue); + HistogramWithStringDimensionsTest(meter, metricCollector, short.MinValue); + HistogramWithStringDimensionsTest(meter, metricCollector, int.MinValue); + HistogramWithStringDimensionsTest(meter, metricCollector, long.MinValue); + HistogramWithStringDimensionsTest(meter, metricCollector, float.MinValue); + HistogramWithStringDimensionsTest(meter, metricCollector, double.MinValue); + HistogramWithStringDimensionsTest(meter, metricCollector, decimal.MinValue); + } + + [Fact] + public void Histogram_NumericDimensionsAreHandled() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + HistogramWithNumericDimensionsTest(meter, metricCollector, byte.MinValue); + HistogramWithNumericDimensionsTest(meter, metricCollector, short.MaxValue); + HistogramWithNumericDimensionsTest(meter, metricCollector, int.MaxValue); + HistogramWithNumericDimensionsTest(meter, metricCollector, long.MaxValue); + HistogramWithNumericDimensionsTest(meter, metricCollector, float.MaxValue); + HistogramWithNumericDimensionsTest(meter, metricCollector, double.MaxValue); + HistogramWithNumericDimensionsTest(meter, metricCollector, decimal.MaxValue); + } + + [Fact] + public void Histogram_ArrayDimensionsAreHandled() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + HistogramWithArrayDimensionsTest(meter, metricCollector, byte.MinValue); + HistogramWithArrayDimensionsTest(meter, metricCollector, short.MaxValue); + HistogramWithArrayDimensionsTest(meter, metricCollector, int.MaxValue); + HistogramWithArrayDimensionsTest(meter, metricCollector, long.MaxValue); + HistogramWithArrayDimensionsTest(meter, metricCollector, float.MaxValue); + HistogramWithArrayDimensionsTest(meter, metricCollector, double.MaxValue); + HistogramWithArrayDimensionsTest(meter, metricCollector, decimal.MaxValue); + } + + private static void HistogramBasicTest(Meter meter, MetricCollector metricCollector, T value, T secondValue) + where T : struct + { + var histogram = meter.CreateHistogram(Guid.NewGuid().ToString()); + var holder1 = metricCollector.GetHistogramValues(histogram.Name); + + Assert.NotNull(holder1); + + histogram.Record(value); + + var recordedValue1 = metricCollector.GetHistogramValue(histogram.Name); + + Assert.NotNull(recordedValue1); + Assert.Equal(value, recordedValue1.Value); + Assert.Equal(1, holder1.AllValues.Count); + + histogram.Record(secondValue); + + var recordedValue2 = metricCollector.GetHistogramValue(histogram.Name); + + Assert.Equal(secondValue, recordedValue2!.Value); + Assert.Equal(2, holder1.AllValues.Count); + + var histogram2 = meter.CreateHistogram(Guid.NewGuid().ToString()); + var holder2 = metricCollector.GetHistogramValues(histogram2.Name); + + Assert.NotNull(holder2); + + histogram2.Record(value); + + Assert.Equal(value, metricCollector.GetHistogramValue(histogram2.Name)!.Value); + + histogram2.Record(secondValue); + + Assert.Equal(secondValue, metricCollector.GetHistogramValue(histogram2.Name)!.Value); + } + + private static void HistogramWithStringDimensionsTest(Meter meter, MetricCollector metricCollector, T value) + where T : struct + { + var histogram = meter.CreateHistogram(Guid.NewGuid().ToString()); + + // No dimensions + histogram.Record(value); + + Assert.Equal(value, metricCollector.GetHistogramValue(histogram.Name)!.Value); + + // One dimension + var dimension1 = Guid.NewGuid().ToString(); + var dimension1Val = Guid.NewGuid().ToString(); + histogram.Record(value, new KeyValuePair(dimension1, dimension1Val)); + + Assert.Equal(value, metricCollector.GetHistogramValue(histogram.Name, new KeyValuePair(dimension1, dimension1Val))!.Value); + + // Two dimensions + var dimension2 = Guid.NewGuid().ToString(); + var dimension2Val = Guid.NewGuid().ToString(); + histogram.Record(value, new KeyValuePair(dimension1, dimension1Val), new KeyValuePair(dimension2, dimension2Val)); + + Assert.Equal(value, + metricCollector.GetHistogramValue(histogram.Name, new KeyValuePair(dimension2, dimension2Val), new KeyValuePair(dimension1, dimension1Val))!.Value); + } + + private static void HistogramWithNumericDimensionsTest(Meter meter, MetricCollector metricCollector, T value) + where T : struct + { + var histogram = meter.CreateHistogram(Guid.NewGuid().ToString()); + + // One dimension + var intDimension = Guid.NewGuid().ToString(); + int intVal = 15555; + histogram.Record(value, new KeyValuePair(intDimension, intVal)); + + Assert.Equal(value, metricCollector.GetHistogramValue(histogram.Name, new KeyValuePair(intDimension, intVal))!.Value); + + // Two dimensions + var doubleDimension = Guid.NewGuid().ToString(); + double doubleVal = 1111.9999d; + histogram.Record(value, new KeyValuePair(intDimension, intVal), new KeyValuePair(doubleDimension, doubleVal)); + + Assert.Equal(value, + metricCollector.GetHistogramValue(histogram.Name, new KeyValuePair(intDimension, intVal), new KeyValuePair(doubleDimension, doubleVal))!.Value); + + // Three dimensions + var longDimension = Guid.NewGuid().ToString(); + long longVal = 1_999_988_887_777_111L; + + histogram.Record(value, new KeyValuePair(intDimension, intVal), new KeyValuePair(longDimension, longVal), + new KeyValuePair(doubleDimension, doubleVal)); + + var actualValue = metricCollector.GetHistogramValue(histogram.Name, + new KeyValuePair(longDimension, longVal), + new KeyValuePair(intDimension, intVal), + new KeyValuePair(doubleDimension, doubleVal)); + Assert.Equal(value, actualValue!.Value); + } + + private static void HistogramWithArrayDimensionsTest(Meter meter, MetricCollector metricCollector, T value) + where T : struct + { + var histogram = meter.CreateHistogram(Guid.NewGuid().ToString()); + + // One dimension + var intArrayDimension = Guid.NewGuid().ToString(); + int[] intArrVal = new[] { 12, 55, 2023 }; + histogram.Record(value, new KeyValuePair(intArrayDimension, intArrVal)); + + Assert.Equal(value, metricCollector.GetHistogramValue(histogram.Name, new KeyValuePair(intArrayDimension, intArrVal))!.Value); + + // Two dimensions + var doubleArrayDimension = Guid.NewGuid().ToString(); + double[] doubleArrVal = new[] { 1111.9999d, 0, 3.1415 }; + histogram.Record(value, new KeyValuePair(intArrayDimension, intArrVal), new KeyValuePair(doubleArrayDimension, doubleArrVal)); + + var actualValue = metricCollector.GetHistogramValue(histogram.Name, + new KeyValuePair(intArrayDimension, intArrVal), + new KeyValuePair(doubleArrayDimension, doubleArrVal)); + + Assert.Equal(value, actualValue!.Value); + + // Three dimensions + var longArrayDimension = Guid.NewGuid().ToString(); + long[] longArrVal = new[] { 1_999_988_887_777_111L, 1_111_222_333_444_555L, 1_999_988_887_777_111L, 0 }; + + histogram.Record(value, new KeyValuePair(intArrayDimension, intArrVal), + new KeyValuePair(doubleArrayDimension, doubleArrVal), + new KeyValuePair(longArrayDimension, longArrVal)); + + actualValue = metricCollector.GetHistogramValue(histogram.Name, + new KeyValuePair(longArrayDimension, longArrVal), + new KeyValuePair(intArrayDimension, intArrVal), + new KeyValuePair(doubleArrayDimension, doubleArrVal)); + + Assert.Equal(value, actualValue!.Value); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.ObservableCounter.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.ObservableCounter.cs new file mode 100644 index 0000000000..dca620fdce --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.ObservableCounter.cs @@ -0,0 +1,298 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Testing.Metering; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Testing.Metering.Test; + +public partial class MetricCollectorTests +{ + [Fact] + public void ObservableCounter_BasicTest() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + ObservableCounterBasicTest(meter, metricCollector, 2, 5, 10, 17); + ObservableCounterBasicTest(meter, metricCollector, 20, 50, 100, 170); + ObservableCounterBasicTest(meter, metricCollector, 200, 500, 1000, 1700); + ObservableCounterBasicTest(meter, metricCollector, 2000L, 5000L, 10000L, 17000L); + ObservableCounterBasicTest(meter, metricCollector, 1.22f, 3.44f, 0, 1.22f + 3.44f); + ObservableCounterBasicTest(meter, metricCollector, 5.22, 6.44, 10, 5.22 + 6.44 + 10); + ObservableCounterBasicTest(meter, metricCollector, 0.99m, 15, 25.99m, 41.98m); + } + + [Fact] + public void ObservableCounter_StringDimensionsAreHandled() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + ObservableCounterWithStringDimensionsTest(meter, metricCollector, byte.MinValue); + ObservableCounterWithStringDimensionsTest(meter, metricCollector, short.MaxValue); + ObservableCounterWithStringDimensionsTest(meter, metricCollector, int.MaxValue); + ObservableCounterWithStringDimensionsTest(meter, metricCollector, long.MaxValue); + ObservableCounterWithStringDimensionsTest(meter, metricCollector, float.MaxValue); + ObservableCounterWithStringDimensionsTest(meter, metricCollector, double.MaxValue); + ObservableCounterWithStringDimensionsTest(meter, metricCollector, decimal.MaxValue); + } + + [Fact] + public void ObservableCounter_NumericDimensionsAreHandled() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + ObservableCounterWithNumericDimensionsTest(meter, metricCollector, byte.MinValue); + ObservableCounterWithNumericDimensionsTest(meter, metricCollector, short.MaxValue); + ObservableCounterWithNumericDimensionsTest(meter, metricCollector, int.MaxValue); + ObservableCounterWithNumericDimensionsTest(meter, metricCollector, long.MaxValue); + ObservableCounterWithNumericDimensionsTest(meter, metricCollector, float.MaxValue); + ObservableCounterWithNumericDimensionsTest(meter, metricCollector, double.MaxValue); + ObservableCounterWithNumericDimensionsTest(meter, metricCollector, decimal.MaxValue); + } + + [Fact] + public void ObservableCounter_ArrayDimensionsAreHandled() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + ObservableCounterWithArrayDimensionsTest(meter, metricCollector, byte.MinValue); + ObservableCounterWithArrayDimensionsTest(meter, metricCollector, short.MaxValue); + ObservableCounterWithArrayDimensionsTest(meter, metricCollector, int.MaxValue); + ObservableCounterWithArrayDimensionsTest(meter, metricCollector, long.MaxValue); + ObservableCounterWithArrayDimensionsTest(meter, metricCollector, float.MaxValue); + ObservableCounterWithArrayDimensionsTest(meter, metricCollector, double.MaxValue); + ObservableCounterWithArrayDimensionsTest(meter, metricCollector, decimal.MaxValue); + } + + private static void ObservableCounterBasicTest(Meter meter, MetricCollector metricCollector, T value1, T value2, T value3, T value4) + where T : struct + { + var states = new[] { value1, value2, value3, value4 }; + + int index = 0; + var observableFunc = () => + { + if (index >= states.Length) + { + index = 0; + } + + return states[index++]; + }; + + var observableCounter1 = meter.CreateObservableCounter(Guid.NewGuid().ToString(), observableFunc); + var holder1 = metricCollector.GetObservableCounterValues(observableCounter1.Name); + + Assert.NotNull(holder1); + + metricCollector.CollectObservableInstruments(); + var recordedValue1 = metricCollector.GetObservableCounterValue(observableCounter1.Name); + + Assert.NotNull(recordedValue1); + Assert.Equal(value1, recordedValue1.Value); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value2, metricCollector.GetObservableCounterValue(observableCounter1.Name)!.Value); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value3, metricCollector.GetObservableCounterValue(observableCounter1.Name)!.Value); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value4, metricCollector.GetObservableCounterValue(observableCounter1.Name)!.Value); + + int index2 = 0; + var observableFunc2 = () => + { + if (index2 >= states.Length) + { + index2 = 0; + } + + return states[index2++]; + }; + + var observableCounter2 = meter.CreateObservableCounter(Guid.NewGuid().ToString(), observableFunc2); + var holder2 = metricCollector.GetObservableCounterValues(observableCounter2.Name); + + Assert.NotNull(holder2); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value1, metricCollector.GetObservableCounterValue(observableCounter2.Name)!.Value); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value2, metricCollector.GetObservableCounterValue(observableCounter2.Name)!.Value); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value3, metricCollector.GetObservableCounterValue(observableCounter2.Name)!.Value); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value4, metricCollector.GetObservableCounterValue(observableCounter2.Name)!.Value); + } + + private static void ObservableCounterWithStringDimensionsTest(Meter meter, MetricCollector metricCollector, T value) + where T : struct + { + var dimension1 = Guid.NewGuid().ToString(); + var dimension1Val = Guid.NewGuid().ToString(); + var dimension2 = Guid.NewGuid().ToString(); + var dimension2Val = Guid.NewGuid().ToString(); + + var measurements = new[] + { + new Measurement(value), + new Measurement(value, new KeyValuePair(dimension1, dimension1Val)), + new Measurement(value, new KeyValuePair(dimension1, dimension1Val), new KeyValuePair(dimension2, dimension2Val)), + }; + + int index = 0; + var observableFunc = () => + { + if (index >= measurements.Length) + { + index = 0; + } + + return measurements[index++]; + }; + + var observableCounter = meter.CreateObservableCounter(Guid.NewGuid().ToString(), observableFunc); + + // No dimensions + metricCollector.CollectObservableInstruments(); + Assert.Equal(value, metricCollector.GetObservableCounterValue(observableCounter.Name)!.Value); + + // One dimension + metricCollector.CollectObservableInstruments(); + Assert.Equal(value, metricCollector.GetObservableCounterValue(observableCounter.Name, new KeyValuePair(dimension1, dimension1Val))!.Value); + + // Two dimensions + metricCollector.CollectObservableInstruments(); + var actualValue = metricCollector.GetObservableCounterValue( + observableCounter.Name, + new KeyValuePair(dimension2, dimension2Val), + new KeyValuePair(dimension1, dimension1Val)); + Assert.Equal(value, actualValue!.Value); + } + + private static void ObservableCounterWithNumericDimensionsTest(Meter meter, MetricCollector metricCollector, T value) + where T : struct + { + var intDimension = Guid.NewGuid().ToString(); + int intVal = 15555; + var doubleDimension = Guid.NewGuid().ToString(); + double doubleVal = 1111.9999d; + var longDimension = Guid.NewGuid().ToString(); + long longVal = 1_999_988_887_777_111L; + + var measurements = new[] + { + new Measurement(value, new KeyValuePair(intDimension, intVal)), + new Measurement(value, new KeyValuePair(intDimension, intVal), new KeyValuePair(doubleDimension, doubleVal)), + new Measurement(value, new KeyValuePair(intDimension, intVal), + new KeyValuePair(doubleDimension, doubleVal), + new KeyValuePair(longDimension, longVal)) + }; + + int index = 0; + var observableFunc = () => + { + if (index >= measurements.Length) + { + index = 0; + } + + return measurements[index++]; + }; + + var observableCounter = meter.CreateObservableCounter(Guid.NewGuid().ToString(), observableFunc); + + // One dimension + metricCollector.CollectObservableInstruments(); + Assert.Equal(value, metricCollector.GetObservableCounterValue(observableCounter.Name, new KeyValuePair(intDimension, intVal))!.Value); + + // Two dimensions + metricCollector.CollectObservableInstruments(); + var actualValue = metricCollector.GetObservableCounterValue( + observableCounter.Name, + new KeyValuePair(intDimension, intVal), + new KeyValuePair(doubleDimension, doubleVal)); + Assert.Equal(value, actualValue!.Value); + + // Three dimensions + metricCollector.CollectObservableInstruments(); + actualValue = metricCollector.GetObservableCounterValue( + observableCounter.Name, + new KeyValuePair(longDimension, longVal), + new KeyValuePair(intDimension, intVal), + new KeyValuePair(doubleDimension, doubleVal)); + Assert.Equal(value, actualValue!.Value); + } + + private static void ObservableCounterWithArrayDimensionsTest(Meter meter, MetricCollector metricCollector, T value) + where T : struct + { + var intArrayDimension = Guid.NewGuid().ToString(); + int[] intArrVal = new[] { 12, 55, 2023 }; + var doubleArrayDimension = Guid.NewGuid().ToString(); + double[] doubleArrVal = new[] { 1111.9999d, 0, 3.1415 }; + var longArrayDimension = Guid.NewGuid().ToString(); + long[] longArrVal = new[] { 1_999_988_887_777_111L, 1_111_222_333_444_555L, 1_999_988_887_777_111L, 0 }; + + var measurements = new[] + { + new Measurement(value, new KeyValuePair(intArrayDimension, intArrVal)), + new Measurement(value, new KeyValuePair(intArrayDimension, intArrVal), new KeyValuePair(doubleArrayDimension, doubleArrVal)), + new Measurement(value, new KeyValuePair(intArrayDimension, intArrVal), + new KeyValuePair(doubleArrayDimension, doubleArrVal), + new KeyValuePair(longArrayDimension, longArrVal)) + }; + + int index = 0; + var observableFunc = () => + { + if (index >= measurements.Length) + { + index = 0; + } + + return measurements[index++]; + }; + + var observableCounter = meter.CreateObservableCounter(Guid.NewGuid().ToString(), observableFunc); + + // One dimension + metricCollector.CollectObservableInstruments(); + Assert.Equal(value, metricCollector.GetObservableCounterValue(observableCounter.Name, new KeyValuePair(intArrayDimension, intArrVal))!.Value); + + // Two dimensions + metricCollector.CollectObservableInstruments(); + var actualValue = metricCollector.GetObservableCounterValue(observableCounter.Name, + new KeyValuePair(intArrayDimension, intArrVal), + new KeyValuePair(doubleArrayDimension, doubleArrVal)); + + Assert.Equal(value, actualValue!.Value); + + // Three dimensions + metricCollector.CollectObservableInstruments(); + actualValue = metricCollector.GetObservableCounterValue(observableCounter.Name, + new KeyValuePair(longArrayDimension, longArrVal), + new KeyValuePair(intArrayDimension, intArrVal), + new KeyValuePair(doubleArrayDimension, doubleArrVal)); + + Assert.Equal(value, actualValue!.Value); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.ObservableGauge.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.ObservableGauge.cs new file mode 100644 index 0000000000..e1365eb773 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.ObservableGauge.cs @@ -0,0 +1,297 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Testing.Metering; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Testing.Metering.Test; + +public partial class MetricCollectorTests +{ + [Fact] + public void ObservableGauge_BasicTest() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + ObservableGaugeBasicTest(meter, metricCollector, 2, 5, 10, 17); + ObservableGaugeBasicTest(meter, metricCollector, 20, 50, 100, 170); + ObservableGaugeBasicTest(meter, metricCollector, 200, 500, 1000, 1700); + ObservableGaugeBasicTest(meter, metricCollector, 2000L, 5000L, 10000L, 17000L); + ObservableGaugeBasicTest(meter, metricCollector, 1.22f, 3.44f, 0, 1.22f + 3.44f); + ObservableGaugeBasicTest(meter, metricCollector, 5.22, 6.44, 10, 5.22 + 6.44 + 10); + ObservableGaugeBasicTest(meter, metricCollector, 0.99m, 15, 25.99m, 41.98m); + } + + [Fact] + public void ObservableGauge_StringDimensionsAreHandled() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + ObservableGaugeWithStringDimensionsTest(meter, metricCollector, byte.MinValue); + ObservableGaugeWithStringDimensionsTest(meter, metricCollector, short.MaxValue); + ObservableGaugeWithStringDimensionsTest(meter, metricCollector, int.MaxValue); + ObservableGaugeWithStringDimensionsTest(meter, metricCollector, long.MaxValue); + ObservableGaugeWithStringDimensionsTest(meter, metricCollector, float.MaxValue); + ObservableGaugeWithStringDimensionsTest(meter, metricCollector, double.MaxValue); + ObservableGaugeWithStringDimensionsTest(meter, metricCollector, decimal.MaxValue); + } + + [Fact] + public void ObservableGauge_NumericDimensionsAreHandled() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + ObservableGaugeWithNumericDimensionsTest(meter, metricCollector, byte.MinValue); + ObservableGaugeWithNumericDimensionsTest(meter, metricCollector, short.MaxValue); + ObservableGaugeWithNumericDimensionsTest(meter, metricCollector, int.MaxValue); + ObservableGaugeWithNumericDimensionsTest(meter, metricCollector, long.MaxValue); + ObservableGaugeWithNumericDimensionsTest(meter, metricCollector, float.MaxValue); + ObservableGaugeWithNumericDimensionsTest(meter, metricCollector, double.MaxValue); + ObservableGaugeWithNumericDimensionsTest(meter, metricCollector, decimal.MaxValue); + } + + [Fact] + public void ObservableGauge_ArrayDimensionsAreHandled() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + ObservableGaugeWithArrayDimensionsTest(meter, metricCollector, byte.MinValue); + ObservableGaugeWithArrayDimensionsTest(meter, metricCollector, short.MaxValue); + ObservableGaugeWithArrayDimensionsTest(meter, metricCollector, int.MaxValue); + ObservableGaugeWithArrayDimensionsTest(meter, metricCollector, long.MaxValue); + ObservableGaugeWithArrayDimensionsTest(meter, metricCollector, float.MaxValue); + ObservableGaugeWithArrayDimensionsTest(meter, metricCollector, double.MaxValue); + ObservableGaugeWithArrayDimensionsTest(meter, metricCollector, decimal.MaxValue); + } + + private static void ObservableGaugeBasicTest(Meter meter, MetricCollector metricCollector, T value1, T value2, T value3, T value4) + where T : struct + { + var states = new[] { value1, value2, value3, value4 }; + + int index = 0; + var observableFunc = () => + { + if (index >= states.Length) + { + index = 0; + } + + return states[index++]; + }; + + var observableGauge1 = meter.CreateObservableGauge(Guid.NewGuid().ToString(), observableFunc); + var holder1 = metricCollector.GetObservableGaugeValues(observableGauge1.Name); + + Assert.NotNull(holder1); + + metricCollector.CollectObservableInstruments(); + var recordedValue1 = metricCollector.GetObservableGaugeValue(observableGauge1.Name); + + Assert.NotNull(recordedValue1); + Assert.Equal(value1, recordedValue1.Value); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value2, metricCollector.GetObservableGaugeValue(observableGauge1.Name)!.Value); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value3, metricCollector.GetObservableGaugeValue(observableGauge1.Name)!.Value); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value4, metricCollector.GetObservableGaugeValue(observableGauge1.Name)!.Value); + + int index2 = 0; + var observableFunc2 = () => + { + if (index2 >= states.Length) + { + index2 = 0; + } + + return states[index2++]; + }; + + var observableGauge2 = meter.CreateObservableGauge(Guid.NewGuid().ToString(), observableFunc2); + var holder2 = metricCollector.GetObservableGaugeValues(observableGauge2.Name); + + Assert.NotNull(holder2); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value1, metricCollector.GetObservableGaugeValue(observableGauge2.Name)!.Value); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value2, metricCollector.GetObservableGaugeValue(observableGauge2.Name)!.Value); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value3, metricCollector.GetObservableGaugeValue(observableGauge2.Name)!.Value); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value4, metricCollector.GetObservableGaugeValue(observableGauge2.Name)!.Value); + } + + private static void ObservableGaugeWithStringDimensionsTest(Meter meter, MetricCollector metricCollector, T value) + where T : struct + { + var dimension1 = Guid.NewGuid().ToString(); + var dimension1Val = Guid.NewGuid().ToString(); + var dimension2 = Guid.NewGuid().ToString(); + var dimension2Val = Guid.NewGuid().ToString(); + + var measurements = new[] + { + new Measurement(value), + new Measurement(value, new KeyValuePair(dimension1, dimension1Val)), + new Measurement(value, new KeyValuePair(dimension1, dimension1Val), new KeyValuePair(dimension2, dimension2Val)), + }; + + int index = 0; + var observableFunc = () => + { + if (index >= measurements.Length) + { + index = 0; + } + + return measurements[index++]; + }; + + var observableGauge = meter.CreateObservableGauge(Guid.NewGuid().ToString(), observableFunc); + + // No dimensions + metricCollector.CollectObservableInstruments(); + Assert.Equal(value, metricCollector.GetObservableGaugeValue(observableGauge.Name)!.Value); + + // One dimension + metricCollector.CollectObservableInstruments(); + Assert.Equal(value, metricCollector.GetObservableGaugeValue(observableGauge.Name, new KeyValuePair(dimension1, dimension1Val))!.Value); + + // Two dimensions + metricCollector.CollectObservableInstruments(); + var actualvalue = metricCollector.GetObservableGaugeValue( + observableGauge.Name, + new KeyValuePair(dimension2, dimension2Val), + new KeyValuePair(dimension1, dimension1Val)); + Assert.Equal(value, actualvalue!.Value); + } + + private static void ObservableGaugeWithNumericDimensionsTest(Meter meter, MetricCollector metricCollector, T value) + where T : struct + { + var intDimension = Guid.NewGuid().ToString(); + int intVal = 15555; + var doubleDimension = Guid.NewGuid().ToString(); + double doubleVal = 1111.9999d; + var longDimension = Guid.NewGuid().ToString(); + long longVal = 1_999_988_887_777_111L; + + var measurements = new[] + { + new Measurement(value, new KeyValuePair(intDimension, intVal)), + new Measurement(value, new KeyValuePair(intDimension, intVal), new KeyValuePair(doubleDimension, doubleVal)), + new Measurement(value, new KeyValuePair(intDimension, intVal), + new KeyValuePair(doubleDimension, doubleVal), + new KeyValuePair(longDimension, longVal)) + }; + + int index = 0; + var observableFunc = () => + { + if (index >= measurements.Length) + { + index = 0; + } + + return measurements[index++]; + }; + + var observableGauge = meter.CreateObservableGauge(Guid.NewGuid().ToString(), observableFunc); + + // One dimension + metricCollector.CollectObservableInstruments(); + Assert.Equal(value, metricCollector.GetObservableGaugeValue(observableGauge.Name, new KeyValuePair(intDimension, intVal))!.Value); + + // Two dimensions + metricCollector.CollectObservableInstruments(); + var actualValue = metricCollector.GetObservableGaugeValue( + observableGauge.Name, + new KeyValuePair(intDimension, intVal), + new KeyValuePair(doubleDimension, doubleVal)); + Assert.Equal(value, actualValue!.Value); + + // Three dimensions + metricCollector.CollectObservableInstruments(); + actualValue = metricCollector.GetObservableGaugeValue(observableGauge.Name, + new KeyValuePair(longDimension, longVal), + new KeyValuePair(intDimension, intVal), + new KeyValuePair(doubleDimension, doubleVal)); + Assert.Equal(value, actualValue!.Value); + } + + private static void ObservableGaugeWithArrayDimensionsTest(Meter meter, MetricCollector metricCollector, T value) + where T : struct + { + var intArrayDimension = Guid.NewGuid().ToString(); + int[] intArrVal = new[] { 12, 55, 2023 }; + var doubleArrayDimension = Guid.NewGuid().ToString(); + double[] doubleArrVal = new[] { 1111.9999d, 0, 3.1415 }; + var longArrayDimension = Guid.NewGuid().ToString(); + long[] longArrVal = new[] { 1_999_988_887_777_111L, 1_111_222_333_444_555L, 1_999_988_887_777_111L, 0 }; + + var measurements = new[] + { + new Measurement(value, new KeyValuePair(intArrayDimension, intArrVal)), + new Measurement(value, new KeyValuePair(intArrayDimension, intArrVal), new KeyValuePair(doubleArrayDimension, doubleArrVal)), + new Measurement(value, new KeyValuePair(intArrayDimension, intArrVal), + new KeyValuePair(doubleArrayDimension, doubleArrVal), + new KeyValuePair(longArrayDimension, longArrVal)) + }; + + int index = 0; + var observableFunc = () => + { + if (index >= measurements.Length) + { + index = 0; + } + + return measurements[index++]; + }; + + var observableGauge = meter.CreateObservableGauge(Guid.NewGuid().ToString(), observableFunc); + + // One dimension + metricCollector.CollectObservableInstruments(); + Assert.Equal(value, metricCollector.GetObservableGaugeValue(observableGauge.Name, new KeyValuePair(intArrayDimension, intArrVal))!.Value); + + // Two dimensions + metricCollector.CollectObservableInstruments(); + var actualValue = metricCollector.GetObservableGaugeValue(observableGauge.Name, + new KeyValuePair(intArrayDimension, intArrVal), + new KeyValuePair(doubleArrayDimension, doubleArrVal)); + + Assert.Equal(value, actualValue!.Value); + + // Three dimensions + metricCollector.CollectObservableInstruments(); + actualValue = metricCollector.GetObservableGaugeValue(observableGauge.Name, + new KeyValuePair(longArrayDimension, longArrVal), + new KeyValuePair(intArrayDimension, intArrVal), + new KeyValuePair(doubleArrayDimension, doubleArrVal)); + + Assert.Equal(value, actualValue!.Value); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.ObservableUpdownCounter.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.ObservableUpdownCounter.cs new file mode 100644 index 0000000000..8accf92f28 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.ObservableUpdownCounter.cs @@ -0,0 +1,297 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Testing.Metering; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Testing.Metering.Test; + +public partial class MetricCollectorTests +{ + [Fact] + public void ObservableUpDownCounter_BasicTest() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + ObservableUpDownCounterBasicTest(meter, metricCollector, 2, 5, 10, 17); + ObservableUpDownCounterBasicTest(meter, metricCollector, 20, 50, 100, 170); + ObservableUpDownCounterBasicTest(meter, metricCollector, 200, 500, 1000, 1700); + ObservableUpDownCounterBasicTest(meter, metricCollector, 2000L, 5000L, 10000L, 17000L); + ObservableUpDownCounterBasicTest(meter, metricCollector, 1.22f, 3.44f, 0, 1.22f + 3.44f); + ObservableUpDownCounterBasicTest(meter, metricCollector, 5.22, 6.44, 10, 5.22 + 6.44 + 10); + ObservableUpDownCounterBasicTest(meter, metricCollector, 0.99m, 15, 25.99m, 41.98m); + } + + [Fact] + public void ObservableUpDownCounter_StringDimensionsAreHandled() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + ObservableUpDownCounterWithStringDimensionsTest(meter, metricCollector, byte.MinValue); + ObservableUpDownCounterWithStringDimensionsTest(meter, metricCollector, short.MaxValue); + ObservableUpDownCounterWithStringDimensionsTest(meter, metricCollector, int.MaxValue); + ObservableUpDownCounterWithStringDimensionsTest(meter, metricCollector, long.MaxValue); + ObservableUpDownCounterWithStringDimensionsTest(meter, metricCollector, float.MaxValue); + ObservableUpDownCounterWithStringDimensionsTest(meter, metricCollector, double.MaxValue); + ObservableUpDownCounterWithStringDimensionsTest(meter, metricCollector, decimal.MaxValue); + } + + [Fact] + public void ObservableUpDownCounter_NumericDimensionsAreHandled() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + ObservableUpDownCounterWithNumericDimensionsTest(meter, metricCollector, byte.MinValue); + ObservableUpDownCounterWithNumericDimensionsTest(meter, metricCollector, short.MaxValue); + ObservableUpDownCounterWithNumericDimensionsTest(meter, metricCollector, int.MaxValue); + ObservableUpDownCounterWithNumericDimensionsTest(meter, metricCollector, long.MaxValue); + ObservableUpDownCounterWithNumericDimensionsTest(meter, metricCollector, float.MaxValue); + ObservableUpDownCounterWithNumericDimensionsTest(meter, metricCollector, double.MaxValue); + ObservableUpDownCounterWithNumericDimensionsTest(meter, metricCollector, decimal.MaxValue); + } + + [Fact] + public void ObservableUpDownCounter_ArrayDimensionsAreHandled() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + ObservableUpDownCounterWithArrayDimensionsTest(meter, metricCollector, byte.MinValue); + ObservableUpDownCounterWithArrayDimensionsTest(meter, metricCollector, short.MaxValue); + ObservableUpDownCounterWithArrayDimensionsTest(meter, metricCollector, int.MaxValue); + ObservableUpDownCounterWithArrayDimensionsTest(meter, metricCollector, long.MaxValue); + ObservableUpDownCounterWithArrayDimensionsTest(meter, metricCollector, float.MaxValue); + ObservableUpDownCounterWithArrayDimensionsTest(meter, metricCollector, double.MaxValue); + ObservableUpDownCounterWithArrayDimensionsTest(meter, metricCollector, decimal.MaxValue); + } + + private static void ObservableUpDownCounterBasicTest(Meter meter, MetricCollector metricCollector, T value1, T value2, T value3, T value4) + where T : struct + { + var states = new[] { value1, value2, value3, value4 }; + + int index = 0; + var observableFunc = () => + { + if (index >= states.Length) + { + index = 0; + } + + return states[index++]; + }; + + var observableUpDownCounter1 = meter.CreateObservableUpDownCounter(Guid.NewGuid().ToString(), observableFunc); + var holder1 = metricCollector.GetObservableUpDownCounterValues(observableUpDownCounter1.Name); + + Assert.NotNull(holder1); + + metricCollector.CollectObservableInstruments(); + var recordedValue1 = metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter1.Name); + + Assert.NotNull(recordedValue1); + Assert.Equal(value1, recordedValue1.Value); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value2, metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter1.Name)!.Value); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value3, metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter1.Name)!.Value); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value4, metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter1.Name)!.Value); + + int index2 = 0; + var observableFunc2 = () => + { + if (index2 >= states.Length) + { + index2 = 0; + } + + return states[index2++]; + }; + + var observableUpDownCounter2 = meter.CreateObservableUpDownCounter(Guid.NewGuid().ToString(), observableFunc2); + var holder2 = metricCollector.GetObservableUpDownCounterValues(observableUpDownCounter2.Name); + + Assert.NotNull(holder2); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value1, metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter2.Name)!.Value); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value2, metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter2.Name)!.Value); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value3, metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter2.Name)!.Value); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(value4, metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter2.Name)!.Value); + } + + private static void ObservableUpDownCounterWithStringDimensionsTest(Meter meter, MetricCollector metricCollector, T value) + where T : struct + { + var dimension1 = Guid.NewGuid().ToString(); + var dimension1Val = Guid.NewGuid().ToString(); + var dimension2 = Guid.NewGuid().ToString(); + var dimension2Val = Guid.NewGuid().ToString(); + + var measurements = new[] + { + new Measurement(value), + new Measurement(value, new KeyValuePair(dimension1, dimension1Val)), + new Measurement(value, new KeyValuePair(dimension1, dimension1Val), new KeyValuePair(dimension2, dimension2Val)), + }; + + int index = 0; + var observableFunc = () => + { + if (index >= measurements.Length) + { + index = 0; + } + + return measurements[index++]; + }; + + var observableUpDownCounter = meter.CreateObservableUpDownCounter(Guid.NewGuid().ToString(), observableFunc); + + // No dimensions + metricCollector.CollectObservableInstruments(); + Assert.Equal(value, metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter.Name)!.Value); + + // One dimension + metricCollector.CollectObservableInstruments(); + Assert.Equal(value, metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter.Name, new KeyValuePair(dimension1, dimension1Val))!.Value); + + // Two dimensions + metricCollector.CollectObservableInstruments(); + var actualValue = metricCollector.GetObservableUpDownCounterValue( + observableUpDownCounter.Name, + new KeyValuePair(dimension2, dimension2Val), + new KeyValuePair(dimension1, dimension1Val)); + Assert.Equal(value, actualValue!.Value); + } + + private static void ObservableUpDownCounterWithNumericDimensionsTest(Meter meter, MetricCollector metricCollector, T value) + where T : struct + { + var intDimension = Guid.NewGuid().ToString(); + int intVal = 15555; + var doubleDimension = Guid.NewGuid().ToString(); + double doubleVal = 1111.9999d; + var longDimension = Guid.NewGuid().ToString(); + long longVal = 1_999_988_887_777_111L; + + var measurements = new[] + { + new Measurement(value, new KeyValuePair(intDimension, intVal)), + new Measurement(value, new KeyValuePair(intDimension, intVal), new KeyValuePair(doubleDimension, doubleVal)), + new Measurement(value, new KeyValuePair(intDimension, intVal), + new KeyValuePair(doubleDimension, doubleVal), + new KeyValuePair(longDimension, longVal)) + }; + + int index = 0; + var observableFunc = () => + { + if (index >= measurements.Length) + { + index = 0; + } + + return measurements[index++]; + }; + + var observableUpDownCounter = meter.CreateObservableUpDownCounter(Guid.NewGuid().ToString(), observableFunc); + + // One dimension + metricCollector.CollectObservableInstruments(); + Assert.Equal(value, metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter.Name, new KeyValuePair(intDimension, intVal))!.Value); + + // Two dimensions + metricCollector.CollectObservableInstruments(); + var actualValue = metricCollector.GetObservableUpDownCounterValue( + observableUpDownCounter.Name, + new KeyValuePair(intDimension, intVal), + new KeyValuePair(doubleDimension, doubleVal)); + Assert.Equal(value, actualValue!.Value); + + // Three dimensions + metricCollector.CollectObservableInstruments(); + actualValue = metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter.Name, + new KeyValuePair(longDimension, longVal), + new KeyValuePair(intDimension, intVal), + new KeyValuePair(doubleDimension, doubleVal)); + Assert.Equal(value, actualValue!.Value); + } + + private static void ObservableUpDownCounterWithArrayDimensionsTest(Meter meter, MetricCollector metricCollector, T value) + where T : struct + { + var intArrayDimension = Guid.NewGuid().ToString(); + int[] intArrVal = new[] { 12, 55, 2023 }; + var doubleArrayDimension = Guid.NewGuid().ToString(); + double[] doubleArrVal = new[] { 1111.9999d, 0, 3.1415 }; + var longArrayDimension = Guid.NewGuid().ToString(); + long[] longArrVal = new[] { 1_999_988_887_777_111L, 1_111_222_333_444_555L, 1_999_988_887_777_111L, 0 }; + + var measurements = new[] + { + new Measurement(value, new KeyValuePair(intArrayDimension, intArrVal)), + new Measurement(value, new KeyValuePair(intArrayDimension, intArrVal), new KeyValuePair(doubleArrayDimension, doubleArrVal)), + new Measurement(value, new KeyValuePair(intArrayDimension, intArrVal), + new KeyValuePair(doubleArrayDimension, doubleArrVal), + new KeyValuePair(longArrayDimension, longArrVal)) + }; + + int index = 0; + var observableFunc = () => + { + if (index >= measurements.Length) + { + index = 0; + } + + return measurements[index++]; + }; + + var observableUpDownCounter = meter.CreateObservableUpDownCounter(Guid.NewGuid().ToString(), observableFunc); + + // One dimension + metricCollector.CollectObservableInstruments(); + Assert.Equal(value, metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter.Name, new KeyValuePair(intArrayDimension, intArrVal))!.Value); + + // Two dimensions + metricCollector.CollectObservableInstruments(); + var actualValue = metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter.Name, + new KeyValuePair(intArrayDimension, intArrVal), + new KeyValuePair(doubleArrayDimension, doubleArrVal)); + + Assert.Equal(value, actualValue!.Value); + + // Three dimensions + metricCollector.CollectObservableInstruments(); + actualValue = metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter.Name, + new KeyValuePair(longArrayDimension, longArrVal), + new KeyValuePair(intArrayDimension, intArrVal), + new KeyValuePair(doubleArrayDimension, doubleArrVal)); + + Assert.Equal(value, actualValue!.Value); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.UpDownCounter.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.UpDownCounter.cs new file mode 100644 index 0000000000..f4b256025b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.UpDownCounter.cs @@ -0,0 +1,215 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Testing.Metering; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Testing.Metering.Test; + +public partial class MetricCollectorTests +{ + [Fact] + public void UpDownCounter_BasicTest() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + UpDownCounterBasicTest(meter, metricCollector, 2, 5, 10, 17); + UpDownCounterBasicTest(meter, metricCollector, 20, 50, 100, 170); + UpDownCounterBasicTest(meter, metricCollector, 200, 500, 1000, 1700); + UpDownCounterBasicTest(meter, metricCollector, 2000L, 5000L, 10000L, 17000L); + UpDownCounterBasicTest(meter, metricCollector, 1.22f, 3.44f, 0, 1.22f + 3.44f); + UpDownCounterBasicTest(meter, metricCollector, 5.22, 6.44, 10, 5.22 + 6.44 + 10); + UpDownCounterBasicTest(meter, metricCollector, 0.99m, 15, 25.99m, 41.98m); + } + + [Fact] + public void UpDownCounter_StringDimensionsAreHandled() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + UpDownCounterWithStringDimensionsTest(meter, metricCollector, byte.MinValue); + UpDownCounterWithStringDimensionsTest(meter, metricCollector, short.MaxValue); + UpDownCounterWithStringDimensionsTest(meter, metricCollector, int.MaxValue); + UpDownCounterWithStringDimensionsTest(meter, metricCollector, long.MaxValue); + UpDownCounterWithStringDimensionsTest(meter, metricCollector, float.MaxValue); + UpDownCounterWithStringDimensionsTest(meter, metricCollector, double.MaxValue); + UpDownCounterWithStringDimensionsTest(meter, metricCollector, decimal.MaxValue); + } + + [Fact] + public void UpDownCounter_NumericDimensionsAreHandled() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + UpDownCounterWithNumericDimensionsTest(meter, metricCollector, byte.MinValue); + UpDownCounterWithNumericDimensionsTest(meter, metricCollector, short.MaxValue); + UpDownCounterWithNumericDimensionsTest(meter, metricCollector, int.MaxValue); + UpDownCounterWithNumericDimensionsTest(meter, metricCollector, long.MaxValue); + UpDownCounterWithNumericDimensionsTest(meter, metricCollector, float.MaxValue); + UpDownCounterWithNumericDimensionsTest(meter, metricCollector, double.MaxValue); + UpDownCounterWithNumericDimensionsTest(meter, metricCollector, decimal.MaxValue); + } + + [Fact] + public void UpDownCounter_ArrayDimensionsAreHandled() + { + using var metricCollector = new MetricCollector(); + using var meter = new Meter(string.Empty); + + UpDownCounterWithArrayDimensionsTest(meter, metricCollector, byte.MinValue); + UpDownCounterWithArrayDimensionsTest(meter, metricCollector, short.MaxValue); + UpDownCounterWithArrayDimensionsTest(meter, metricCollector, int.MaxValue); + UpDownCounterWithArrayDimensionsTest(meter, metricCollector, long.MaxValue); + UpDownCounterWithArrayDimensionsTest(meter, metricCollector, float.MaxValue); + UpDownCounterWithArrayDimensionsTest(meter, metricCollector, double.MaxValue); + UpDownCounterWithArrayDimensionsTest(meter, metricCollector, decimal.MaxValue); + } + + private static void UpDownCounterBasicTest(Meter meter, MetricCollector metricCollector, T value, T valueToAdd, T valueToAdd1, T totalSum) + where T : struct + { + var upDownCounter1 = meter.CreateUpDownCounter(Guid.NewGuid().ToString()); + var holder1 = metricCollector.GetUpDownCounterValues(upDownCounter1.Name); + + Assert.NotNull(holder1); + + upDownCounter1.Add(value); + + var recordedValue1 = metricCollector.GetUpDownCounterValue(upDownCounter1.Name); + + Assert.NotNull(recordedValue1); + Assert.Equal(value, recordedValue1.Value); + + upDownCounter1.Add(valueToAdd); + upDownCounter1.Add(valueToAdd1); + + var recordedValue2 = metricCollector.GetUpDownCounterValue(upDownCounter1.Name); + + Assert.Equal(totalSum, recordedValue2!.Value); + + var upDownCounter2 = meter.CreateUpDownCounter(Guid.NewGuid().ToString()); + var holder2 = metricCollector.GetUpDownCounterValues(upDownCounter2.Name); + + Assert.NotNull(holder2); + + upDownCounter2.Add(value); + + Assert.Equal(value, metricCollector.GetUpDownCounterValue(upDownCounter2.Name)!.Value); + + upDownCounter2.Add(valueToAdd); + upDownCounter2.Add(valueToAdd1); + + Assert.Equal(totalSum, metricCollector.GetUpDownCounterValue(upDownCounter2.Name)!.Value); + } + + private static void UpDownCounterWithStringDimensionsTest(Meter meter, MetricCollector metricCollector, T value) + where T : struct + { + var upDownCounter = meter.CreateUpDownCounter(Guid.NewGuid().ToString()); + + // No dimensions + upDownCounter.Add(value); + + Assert.Equal(value, metricCollector.GetUpDownCounterValue(upDownCounter.Name)!.Value); + + // One dimension + var dimension1 = Guid.NewGuid().ToString(); + var dimension1Val = Guid.NewGuid().ToString(); + upDownCounter.Add(value, new KeyValuePair(dimension1, dimension1Val)); + + Assert.Equal(value, metricCollector.GetUpDownCounterValue(upDownCounter.Name, new KeyValuePair(dimension1, dimension1Val))!.Value); + + // Two dimensions + var dimension2 = Guid.NewGuid().ToString(); + var dimension2Val = Guid.NewGuid().ToString(); + upDownCounter.Add(value, new KeyValuePair(dimension1, dimension1Val), new KeyValuePair(dimension2, dimension2Val)); + + var actualValue = metricCollector.GetUpDownCounterValue( + upDownCounter.Name, + new KeyValuePair(dimension2, dimension2Val), + new KeyValuePair(dimension1, dimension1Val)); + Assert.Equal(value, actualValue!.Value); + } + + private static void UpDownCounterWithNumericDimensionsTest(Meter meter, MetricCollector metricCollector, T value) + where T : struct + { + var upDownCounter = meter.CreateUpDownCounter(Guid.NewGuid().ToString()); + + // One dimension + var intDimension = Guid.NewGuid().ToString(); + int intVal = 15555; + upDownCounter.Add(value, new KeyValuePair(intDimension, intVal)); + + Assert.Equal(value, metricCollector.GetUpDownCounterValue(upDownCounter.Name, new KeyValuePair(intDimension, intVal))!.Value); + + // Two dimensions + var doubleDimension = Guid.NewGuid().ToString(); + double doubleVal = 1111.9999d; + upDownCounter.Add(value, new KeyValuePair(intDimension, intVal), new KeyValuePair(doubleDimension, doubleVal)); + + var actualValue = metricCollector.GetUpDownCounterValue( + upDownCounter.Name, + new KeyValuePair(intDimension, intVal), + new KeyValuePair(doubleDimension, doubleVal)); + Assert.Equal(value, actualValue!.Value); + + // Three dimensions + var longDimension = Guid.NewGuid().ToString(); + long longVal = 1_999_988_887_777_111L; + + upDownCounter.Add(value, new KeyValuePair(intDimension, intVal), new KeyValuePair(longDimension, longVal), + new KeyValuePair(doubleDimension, doubleVal)); + + actualValue = metricCollector.GetUpDownCounterValue(upDownCounter.Name, + new KeyValuePair(longDimension, longVal), + new KeyValuePair(intDimension, intVal), + new KeyValuePair(doubleDimension, doubleVal)); + Assert.Equal(value, actualValue!.Value); + } + + private static void UpDownCounterWithArrayDimensionsTest(Meter meter, MetricCollector metricCollector, T value) + where T : struct + { + var upDownCounter = meter.CreateUpDownCounter(Guid.NewGuid().ToString()); + + // One dimension + var intArrayDimension = Guid.NewGuid().ToString(); + int[] intArrVal = new[] { 12, 55, 2023 }; + upDownCounter.Add(value, new KeyValuePair(intArrayDimension, intArrVal)); + + Assert.Equal(value, metricCollector.GetUpDownCounterValue(upDownCounter.Name, new KeyValuePair(intArrayDimension, intArrVal))!.Value); + + // Two dimensions + var doubleArrayDimension = Guid.NewGuid().ToString(); + double[] doubleArrVal = new[] { 1111.9999d, 0, 3.1415 }; + upDownCounter.Add(value, new KeyValuePair(intArrayDimension, intArrVal), new KeyValuePair(doubleArrayDimension, doubleArrVal)); + + var actualValue = metricCollector.GetUpDownCounterValue(upDownCounter.Name, + new KeyValuePair(intArrayDimension, intArrVal), + new KeyValuePair(doubleArrayDimension, doubleArrVal)); + + Assert.Equal(value, actualValue!.Value); + + // Three dimensions + var longArrayDimension = Guid.NewGuid().ToString(); + long[] longArrVal = new[] { 1_999_988_887_777_111L, 1_111_222_333_444_555L, 1_999_988_887_777_111L, 0 }; + + upDownCounter.Add(value, new KeyValuePair(intArrayDimension, intArrVal), + new KeyValuePair(doubleArrayDimension, doubleArrVal), + new KeyValuePair(longArrayDimension, longArrVal)); + + actualValue = metricCollector.GetUpDownCounterValue(upDownCounter.Name, + new KeyValuePair(longArrayDimension, longArrVal), + new KeyValuePair(intArrayDimension, intArrVal), + new KeyValuePair(doubleArrayDimension, doubleArrVal)); + + Assert.Equal(value, actualValue!.Value); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.cs new file mode 100644 index 0000000000..5441352b6c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricCollectorTests.cs @@ -0,0 +1,589 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Testing.Metering.Test; + +public partial class MetricCollectorTests +{ + [Fact] + public void Ctor_Throws_WhenInvalidArguments() + { + Assert.Throws(() => new MetricCollector((Meter)null!)); + Assert.Throws(() => new MetricCollector((IEnumerable)null!)); + } + + [Fact] + public void Mesuarements_AreFilteredOut_WithMeterNameFilter() + { + using var meter1 = new Meter(Guid.NewGuid().ToString()); + using var meter2 = new Meter(Guid.NewGuid().ToString()); + using var meterToIgnore = new Meter(Guid.NewGuid().ToString()); + + using var metricCollector = new MetricCollector(new[] { meter1.Name, meter2.Name }); + + const int IntValue1 = 999; + meter1.CreateCounter("int_counter1").Add(IntValue1); + + // Measurement is captured + Assert.Equal(IntValue1, metricCollector.GetCounterValue("int_counter1")); + + const double DoubleValue1 = 999; + meter2.CreateHistogram("double_histogram1").Record(DoubleValue1); + + // Measurement is captured + Assert.Equal(DoubleValue1, metricCollector.GetHistogramValue("double_histogram1")); + + const int IntValue2 = 999999; + meterToIgnore.CreateCounter("int_counter2").Add(IntValue2); + + // Measurement is filtered out + Assert.Null(metricCollector.GetCounterValue("int_counter2")); + + const double DoubleValue2 = 111.2222; + meter2.CreateHistogram("double_histogram2").Record(DoubleValue2); + + // Measurement is filtered out + Assert.Null(metricCollector.GetCounterValue("double_histogram2")); + } + + [Fact] + public void Mesuarements_AreFilteredOut_WithMeterFilter() + { + var meterName = Guid.NewGuid().ToString(); + using var meter1 = new Meter(meterName); + using var meter2 = new Meter(meterName); + + using var metricCollector = new MetricCollector(meter1); + + const int IntValue1 = 123459; + meter1.CreateCounter("int_counter1").Add(IntValue1); + + // Measurement is captured + Assert.Equal(IntValue1, metricCollector.GetCounterValue("int_counter1")); + + const double DoubleValue1 = 987; + meter1.CreateHistogram("double_histogram1").Record(DoubleValue1); + + // Measurement is captured + Assert.Equal(DoubleValue1, metricCollector.GetHistogramValue("double_histogram1")); + + const int IntValue2 = 91111; + meter2.CreateCounter("int_counter2").Add(IntValue2); + + // Measurement is filtered out + Assert.Null(metricCollector.GetCounterValue("int_counter2")); + + const double DoubleValue2 = 333.7777; + meter2.CreateHistogram("double_histogram2").Record(DoubleValue2); + + // Measurement is filtered out + Assert.Null(metricCollector.GetCounterValue("double_histogram2")); + } + + [Fact] + public void GetCounterValues_ReturnsMeteringValuesHolder() + { + const long TestValue = 111; + using var meter = new Meter(Guid.NewGuid().ToString()); + + var counter = meter.CreateCounter(Guid.NewGuid().ToString()); + + using var metricCollector = new MetricCollector(meter); + + counter.Add(TestValue); + + var meteringHolder = metricCollector.GetCounterValues(counter.Name); + + Assert.NotNull(meteringHolder); + Assert.Equal(TestValue, meteringHolder.GetValue()); + Assert.Null(metricCollector.GetCounterValues(counter.Name)); + Assert.Null(metricCollector.GetCounterValues(counter.Name)); + Assert.Null(metricCollector.GetCounterValues(counter.Name)); + Assert.Null(metricCollector.GetCounterValues(counter.Name)); + Assert.Null(metricCollector.GetCounterValues(counter.Name)); + Assert.Null(metricCollector.GetCounterValues(counter.Name)); + } + + [Fact] + public void GetHistogramValues_ReturnsMeteringValuesHolder() + { + const int TestValue = 271; + using var meter = new Meter(Guid.NewGuid().ToString()); + var histogram = meter.CreateHistogram(Guid.NewGuid().ToString()); + using var metricCollector = new MetricCollector(meter); + + histogram.Record(TestValue); + + var meteringHolder = metricCollector.GetHistogramValues(histogram.Name); + + Assert.NotNull(meteringHolder); + Assert.Equal(TestValue, meteringHolder.GetValue()); + Assert.Null(metricCollector.GetHistogramValues(histogram.Name)); + Assert.Null(metricCollector.GetHistogramValues(histogram.Name)); + Assert.Null(metricCollector.GetHistogramValues(histogram.Name)); + Assert.Null(metricCollector.GetHistogramValues(histogram.Name)); + Assert.Null(metricCollector.GetHistogramValues(histogram.Name)); + Assert.Null(metricCollector.GetHistogramValues(histogram.Name)); + } + + [Fact] + public void GetObservableCounterValues_ReturnsMeteringValuesHolder() + { + const byte TestValue = 255; + using var meter = new Meter(Guid.NewGuid().ToString()); + var observableCounter = meter.CreateObservableCounter(Guid.NewGuid().ToString(), () => TestValue); + using var metricCollector = new MetricCollector(meter); + + metricCollector.CollectObservableInstruments(); + var meteringHolder = metricCollector.GetObservableCounterValues(observableCounter.Name); + + Assert.NotNull(meteringHolder); + Assert.Equal(TestValue, meteringHolder.GetValue()); + Assert.Null(metricCollector.GetObservableCounterValues(observableCounter.Name)); + Assert.Null(metricCollector.GetObservableCounterValues(observableCounter.Name)); + Assert.Null(metricCollector.GetObservableCounterValues(observableCounter.Name)); + Assert.Null(metricCollector.GetObservableCounterValues(observableCounter.Name)); + Assert.Null(metricCollector.GetObservableCounterValues(observableCounter.Name)); + Assert.Null(metricCollector.GetObservableCounterValues(observableCounter.Name)); + } + + [Fact] + public void GetUpDownCounterValues_ReturnsMeteringValuesHolder() + { + const short TestValue = 19999; + using var meter = new Meter(Guid.NewGuid().ToString()); + var upDownCounter = meter.CreateUpDownCounter(Guid.NewGuid().ToString()); + using var metricCollector = new MetricCollector(meter); + + upDownCounter.Add(TestValue); + var meteringHolder = metricCollector.GetUpDownCounterValues(upDownCounter.Name); + + Assert.NotNull(meteringHolder); + Assert.Equal(TestValue, meteringHolder.GetValue()); + Assert.Null(metricCollector.GetUpDownCounterValues(upDownCounter.Name)); + Assert.Null(metricCollector.GetUpDownCounterValues(upDownCounter.Name)); + Assert.Null(metricCollector.GetUpDownCounterValues(upDownCounter.Name)); + Assert.Null(metricCollector.GetUpDownCounterValues(upDownCounter.Name)); + Assert.Null(metricCollector.GetUpDownCounterValues(upDownCounter.Name)); + Assert.Null(metricCollector.GetUpDownCounterValues(upDownCounter.Name)); + } + + [Fact] + public void GetObservableGaugeValues_ReturnsMeteringValuesHolder() + { + const long TestValue = 11_225_599L; + using var meter = new Meter(Guid.NewGuid().ToString()); + var observableGauge = meter.CreateObservableGauge(Guid.NewGuid().ToString(), () => TestValue); + using var metricCollector = new MetricCollector(meter); + + metricCollector.CollectObservableInstruments(); + var meteringHolder = metricCollector.GetObservableGaugeValues(observableGauge.Name); + + Assert.NotNull(meteringHolder); + Assert.Equal(TestValue, meteringHolder.GetValue()); + Assert.Null(metricCollector.GetObservableGaugeValues(observableGauge.Name)); + Assert.Null(metricCollector.GetObservableGaugeValues(observableGauge.Name)); + Assert.Null(metricCollector.GetObservableGaugeValues(observableGauge.Name)); + Assert.Null(metricCollector.GetObservableGaugeValues(observableGauge.Name)); + Assert.Null(metricCollector.GetObservableGaugeValues(observableGauge.Name)); + Assert.Null(metricCollector.GetObservableGaugeValues(observableGauge.Name)); + } + + [Fact] + public void GetObservableUpDownCounterValues_ReturnsMeteringValuesHolder() + { + const int TestValue = int.MaxValue; + using var meter = new Meter(Guid.NewGuid().ToString()); + var observableUpDownCounter = meter.CreateObservableUpDownCounter(Guid.NewGuid().ToString(), () => TestValue); + using var metricCollector = new MetricCollector(meter); + + metricCollector.CollectObservableInstruments(); + var meteringHolder = metricCollector.GetObservableUpDownCounterValues(observableUpDownCounter.Name); + + Assert.NotNull(meteringHolder); + Assert.Equal(TestValue, meteringHolder.GetValue()); + Assert.Null(metricCollector.GetObservableUpDownCounterValues(observableUpDownCounter.Name)); + Assert.Null(metricCollector.GetObservableUpDownCounterValues(observableUpDownCounter.Name)); + Assert.Null(metricCollector.GetObservableUpDownCounterValues(observableUpDownCounter.Name)); + Assert.Null(metricCollector.GetObservableUpDownCounterValues(observableUpDownCounter.Name)); + Assert.Null(metricCollector.GetObservableUpDownCounterValues(observableUpDownCounter.Name)); + Assert.Null(metricCollector.GetObservableUpDownCounterValues(observableUpDownCounter.Name)); + } + + [Fact] + public void GetCounterValue_ReturnsValue() + { + const long TestValue = 111; + using var meter = new Meter(Guid.NewGuid().ToString()); + + var counter = meter.CreateCounter(Guid.NewGuid().ToString()); + + using var metricCollector = new MetricCollector(meter); + + counter.Add(TestValue); + + Assert.Equal(TestValue, metricCollector.GetCounterValue(counter.Name)); + Assert.Null(metricCollector.GetCounterValue(counter.Name)); + Assert.Null(metricCollector.GetCounterValue(counter.Name)); + Assert.Null(metricCollector.GetCounterValue(counter.Name)); + Assert.Null(metricCollector.GetCounterValue(counter.Name)); + Assert.Null(metricCollector.GetCounterValue(counter.Name)); + Assert.Null(metricCollector.GetCounterValue(counter.Name)); + } + + [Fact] + public void GetHistogramValue_ReturnsValue() + { + const int TestValue = 271; + using var meter = new Meter(Guid.NewGuid().ToString()); + var histogram = meter.CreateHistogram(Guid.NewGuid().ToString()); + using var metricCollector = new MetricCollector(meter); + + histogram.Record(TestValue); + + Assert.Equal(TestValue, metricCollector.GetHistogramValue(histogram.Name)); + Assert.Null(metricCollector.GetHistogramValue(histogram.Name)); + Assert.Null(metricCollector.GetHistogramValue(histogram.Name)); + Assert.Null(metricCollector.GetHistogramValue(histogram.Name)); + Assert.Null(metricCollector.GetHistogramValue(histogram.Name)); + Assert.Null(metricCollector.GetHistogramValue(histogram.Name)); + Assert.Null(metricCollector.GetHistogramValue(histogram.Name)); + } + + [Fact] + public void GetObservableCounterValue_ReturnsValue() + { + const byte TestValue = 255; + using var meter = new Meter(Guid.NewGuid().ToString()); + var observableCounter = meter.CreateObservableCounter(Guid.NewGuid().ToString(), () => TestValue); + using var metricCollector = new MetricCollector(meter); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(TestValue, metricCollector.GetObservableCounterValue(observableCounter.Name)); + Assert.Null(metricCollector.GetObservableCounterValue(observableCounter.Name)); + Assert.Null(metricCollector.GetObservableCounterValue(observableCounter.Name)); + Assert.Null(metricCollector.GetObservableCounterValue(observableCounter.Name)); + Assert.Null(metricCollector.GetObservableCounterValue(observableCounter.Name)); + Assert.Null(metricCollector.GetObservableCounterValue(observableCounter.Name)); + Assert.Null(metricCollector.GetObservableCounterValue(observableCounter.Name)); + } + + [Fact] + public void GetUpDownCounterValue_ReturnsValue() + { + const short TestValue = 19999; + using var meter = new Meter(Guid.NewGuid().ToString()); + var upDownCounter = meter.CreateUpDownCounter(Guid.NewGuid().ToString()); + using var metricCollector = new MetricCollector(meter); + + upDownCounter.Add(TestValue); + + Assert.Equal(TestValue, metricCollector.GetUpDownCounterValue(upDownCounter.Name)); + Assert.Null(metricCollector.GetUpDownCounterValue(upDownCounter.Name)); + Assert.Null(metricCollector.GetUpDownCounterValue(upDownCounter.Name)); + Assert.Null(metricCollector.GetUpDownCounterValue(upDownCounter.Name)); + Assert.Null(metricCollector.GetUpDownCounterValue(upDownCounter.Name)); + Assert.Null(metricCollector.GetUpDownCounterValue(upDownCounter.Name)); + Assert.Null(metricCollector.GetUpDownCounterValue(upDownCounter.Name)); + } + + [Fact] + public void GetObservableGaugeValue_ReturnsValue() + { + const long TestValue = 11_225_599L; + using var meter = new Meter(Guid.NewGuid().ToString()); + var observableGauge = meter.CreateObservableGauge(Guid.NewGuid().ToString(), () => TestValue); + using var metricCollector = new MetricCollector(meter); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(TestValue, metricCollector.GetObservableGaugeValue(observableGauge.Name)); + Assert.Null(metricCollector.GetObservableGaugeValue(observableGauge.Name)); + Assert.Null(metricCollector.GetObservableGaugeValue(observableGauge.Name)); + Assert.Null(metricCollector.GetObservableGaugeValue(observableGauge.Name)); + Assert.Null(metricCollector.GetObservableGaugeValue(observableGauge.Name)); + Assert.Null(metricCollector.GetObservableGaugeValue(observableGauge.Name)); + Assert.Null(metricCollector.GetObservableGaugeValue(observableGauge.Name)); + } + + [Fact] + public void GetObservableUpDownCounterValue_ReturnsValue() + { + const int TestValue = int.MaxValue; + using var meter = new Meter(Guid.NewGuid().ToString()); + var observableUpDownCounter = meter.CreateObservableUpDownCounter(Guid.NewGuid().ToString(), () => TestValue); + using var metricCollector = new MetricCollector(meter); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(TestValue, metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter.Name)); + Assert.Null(metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter.Name)); + Assert.Null(metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter.Name)); + Assert.Null(metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter.Name)); + Assert.Null(metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter.Name)); + Assert.Null(metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter.Name)); + Assert.Null(metricCollector.GetObservableUpDownCounterValue(observableUpDownCounter.Name)); + } + + [Fact] + public void Clear_RemovesAllMeasurements() + { + var meterName = Guid.NewGuid().ToString(); + + using var meter = new Meter(meterName); + using var metricCollector = new MetricCollector(new[] { meterName }); + + const int CounterValue = 1; + meter.CreateCounter("int_counter").Add(CounterValue); + + const int HistogramValue = 2; + meter.CreateHistogram("int_histogram").Record(HistogramValue); + + const long UpDownCounterValue = -999L; + meter.CreateUpDownCounter("long_updownCounter").Add(UpDownCounterValue); + + const short ObservableCounterValue = short.MaxValue; + meter.CreateObservableCounter("short_observable_counter", () => ObservableCounterValue); + + const decimal ObservableGaugeValue = decimal.MinValue; + meter.CreateObservableGauge("decimal_observable_gauge", () => ObservableGaugeValue); + + const double ObservableUpdownCouterValue = double.MaxValue; + meter.CreateObservableUpDownCounter("double_observable_updownCounter", () => ObservableUpdownCouterValue); + + Assert.Equal(CounterValue, metricCollector.GetCounterValue("int_counter")!.Value); + Assert.Equal(HistogramValue, metricCollector.GetHistogramValue("int_histogram")!.Value); + Assert.Equal(UpDownCounterValue, metricCollector.GetUpDownCounterValue("long_updownCounter")!.Value); + + metricCollector.CollectObservableInstruments(); + + Assert.Equal(ObservableCounterValue, metricCollector.GetObservableCounterValue("short_observable_counter")!.Value); + Assert.Equal(ObservableGaugeValue, metricCollector.GetObservableGaugeValue("decimal_observable_gauge")!.Value); + Assert.Equal(ObservableUpdownCouterValue, metricCollector.GetObservableUpDownCounterValue("double_observable_updownCounter")!.Value); + + metricCollector.Clear(); + + Assert.Null(metricCollector.GetCounterValue("int_counter")); + Assert.Null(metricCollector.GetHistogramValue("int_histogram")); + Assert.Null(metricCollector.GetUpDownCounterValue("long_updownCounter")); + Assert.Null(metricCollector.GetObservableCounterValue("short_observable_counter")); + Assert.Null(metricCollector.GetObservableGaugeValue("decimal_observable_gauge")); + Assert.Null(metricCollector.GetObservableUpDownCounterValue("double_observable_updownCounter")); + } + + [Fact] + public void CollectObservableInstruments_RecordsObservableMetrics() + { + using var meter = new Meter(Guid.NewGuid().ToString()); + using var metricCollector = new MetricCollector(meter); + + const int CounterValue = 47_382_492; + const float UpDownCounterValue = 921.342f; + const decimal GaugeValue = 12340m; + + meter.CreateObservableCounter("ObservableCounter", () => CounterValue); + meter.CreateObservableGauge("ObservableGauge", () => GaugeValue); + meter.CreateObservableUpDownCounter("ObservableUpDownCounter", () => UpDownCounterValue); + + // Observable instruments are not recorded + Assert.Null(metricCollector.GetObservableCounterValue("ObservableCounter")); + Assert.Null(metricCollector.GetObservableGaugeValue("ObservableGauge")); + Assert.Null(metricCollector.GetObservableUpDownCounterValue("ObservableUpDownCounter")); + + // Force recording of observable instruments + metricCollector.CollectObservableInstruments(); + + Assert.Equal(CounterValue, metricCollector.GetObservableCounterValue("ObservableCounter")); + Assert.Equal(GaugeValue, metricCollector.GetObservableGaugeValue("ObservableGauge")); + Assert.Equal(UpDownCounterValue, metricCollector.GetObservableUpDownCounterValue("ObservableUpDownCounter")); + } + + [Fact] + public void GetXxxValue_ThrowsWhenInvalidValueTypeIsUsed() + { + using var metricCollector = new MetricCollector(); + + var ex = Assert.Throws(() => metricCollector.GetCounterValue(string.Empty)); + Assert.Equal($"The type {typeof(ushort).FullName} is not supported as a type for a metric measurement value", ex.Message); + + var ex1 = Assert.Throws(() => metricCollector.GetHistogramValue(string.Empty)); + Assert.Equal($"The type {typeof(ulong).FullName} is not supported as a type for a metric measurement value", ex1.Message); + + var ex2 = Assert.Throws(() => metricCollector.GetUpDownCounterValue(string.Empty)); + Assert.Equal($"The type {typeof(sbyte).FullName} is not supported as a type for a metric measurement value", ex2.Message); + + var ex3 = Assert.Throws(() => metricCollector.GetObservableCounterValue(string.Empty)); + Assert.Equal($"The type {typeof(uint).FullName} is not supported as a type for a metric measurement value", ex3.Message); + + var ex4 = Assert.Throws(() => metricCollector.GetObservableGaugeValue(string.Empty)); + Assert.Equal($"The type {typeof(uint).FullName} is not supported as a type for a metric measurement value", ex4.Message); + + var ex5 = Assert.Throws(() => metricCollector.GetObservableUpDownCounterValue(string.Empty)); + Assert.Equal($"The type {typeof(uint).FullName} is not supported as a type for a metric measurement value", ex5.Message); + } + + [Fact] + public void GetXxxValues_ThrowsWhenInvalidValueTypeIsUsed() + { + using var metricCollector = new MetricCollector(); + + var ex = Assert.Throws(() => metricCollector.GetCounterValues(string.Empty)); + Assert.Equal($"The type {typeof(ushort).FullName} is not supported as a type for a metric measurement value", ex.Message); + + var ex1 = Assert.Throws(() => metricCollector.GetHistogramValues(string.Empty)); + Assert.Equal($"The type {typeof(ulong).FullName} is not supported as a type for a metric measurement value", ex1.Message); + + var ex2 = Assert.Throws(() => metricCollector.GetUpDownCounterValues(string.Empty)); + Assert.Equal($"The type {typeof(sbyte).FullName} is not supported as a type for a metric measurement value", ex2.Message); + + var ex3 = Assert.Throws(() => metricCollector.GetObservableCounterValues(string.Empty)); + Assert.Equal($"The type {typeof(uint).FullName} is not supported as a type for a metric measurement value", ex3.Message); + + var ex4 = Assert.Throws(() => metricCollector.GetObservableGaugeValues(string.Empty)); + Assert.Equal($"The type {typeof(ulong).FullName} is not supported as a type for a metric measurement value", ex4.Message); + + var ex5 = Assert.Throws(() => metricCollector.GetObservableUpDownCounterValues(string.Empty)); + Assert.Equal($"The type {typeof(ushort).FullName} is not supported as a type for a metric measurement value", ex5.Message); + } + + [Fact] + public void GenericMetricCollector_CapturesFilteredMetering() + { + const int TestValue = 10; + using var metricCollector = new MetricCollector(); + using var meter = new Meter(typeof(MetricCollectorTests).FullName!); + using var meterToIgnore = new Meter(Guid.NewGuid().ToString()); + + var counter1 = meter.CreateCounter(Guid.NewGuid().ToString()); + var counter2 = meterToIgnore.CreateCounter(Guid.NewGuid().ToString()); + + Assert.NotNull(metricCollector.GetCounterValues(counter1.Name)); + Assert.Null(metricCollector.GetCounterValues(counter2.Name)); + + counter1.Add(TestValue); + counter2.Add(TestValue); + + Assert.NotNull(metricCollector.GetCounterValue(counter1.Name)); + Assert.Null(metricCollector.GetCounterValues(counter2.Name)); + } + + [Fact] + public void GetAllCounters_ReturnsAllCounters() + { + const long TestValue = 111; + using var meter = new Meter(Guid.NewGuid().ToString()); + using var metricCollector = new MetricCollector(meter); + + var counter1 = meter.CreateCounter(Guid.NewGuid().ToString()); + var counter2 = meter.CreateCounter(Guid.NewGuid().ToString()); + var counter3 = meter.CreateCounter(Guid.NewGuid().ToString()); + + counter1.Add(TestValue); + counter2.Add(TestValue); + counter3.Add(TestValue); + + Assert.Equal(3, metricCollector.GetAllCounters()!.Count); + } + + [Fact] + public void GetAllUpDownCounters_ReturnsAllCounters() + { + const long TestValue = 111; + using var meter = new Meter(Guid.NewGuid().ToString()); + using var metricCollector = new MetricCollector(meter); + + var counter1 = meter.CreateUpDownCounter(Guid.NewGuid().ToString()); + var counter2 = meter.CreateUpDownCounter(Guid.NewGuid().ToString()); + var counter3 = meter.CreateUpDownCounter(Guid.NewGuid().ToString()); + + counter1.Add(TestValue); + counter2.Add(TestValue); + counter3.Add(TestValue); + + Assert.Equal(3, metricCollector.GetAllUpDownCounters()!.Count); + } + + [Fact] + public void GetAllHistograms_ReturnsAllHistograms() + { + const long TestValue = 111; + using var meter = new Meter(Guid.NewGuid().ToString()); + using var metricCollector = new MetricCollector(meter); + + var counter1 = meter.CreateHistogram(Guid.NewGuid().ToString()); + var counter2 = meter.CreateHistogram(Guid.NewGuid().ToString()); + var counter3 = meter.CreateHistogram(Guid.NewGuid().ToString()); + + counter1.Record(TestValue); + counter2.Record(TestValue); + counter3.Record(TestValue); + + Assert.Equal(3, metricCollector.GetAllHistograms()!.Count); + } + + [Fact] + public void GetAllGauges_ReturnsAllGauges() + { + const long TestValue = 111; + using var meter = new Meter(Guid.NewGuid().ToString()); + using var metricCollector = new MetricCollector(meter); + _ = meter.CreateObservableGauge(Guid.NewGuid().ToString(), () => TestValue); + _ = meter.CreateObservableGauge(Guid.NewGuid().ToString(), () => TestValue); + _ = meter.CreateObservableGauge(Guid.NewGuid().ToString(), () => TestValue); + + // Force recording of observable instruments + metricCollector.CollectObservableInstruments(); + + Assert.Equal(3, metricCollector.GetAllObservableGauges()!.Count); + } + + [Fact] + public void GetAllObservableCounters_ReturnsAllObservableCounters() + { + const long TestValue = 111; + using var meter = new Meter(Guid.NewGuid().ToString()); + using var metricCollector = new MetricCollector(meter); + + _ = meter.CreateObservableCounter(Guid.NewGuid().ToString(), () => TestValue); + _ = meter.CreateObservableCounter(Guid.NewGuid().ToString(), () => TestValue); + _ = meter.CreateObservableCounter(Guid.NewGuid().ToString(), () => TestValue); + + // Force recording of observable instruments + metricCollector.CollectObservableInstruments(); + + Assert.Equal(3, metricCollector.GetAllObservableCounters()!.Count); + } + + [Fact] + public void GetAllObservableUpDownCounters_ReturnsAllObservableUpDownCounters() + { + const long TestValue = 111; + using var meter = new Meter(Guid.NewGuid().ToString()); + using var metricCollector = new MetricCollector(meter); + + _ = meter.CreateObservableUpDownCounter(Guid.NewGuid().ToString(), () => TestValue); + _ = meter.CreateObservableUpDownCounter(Guid.NewGuid().ToString(), () => TestValue); + _ = meter.CreateObservableUpDownCounter(Guid.NewGuid().ToString(), () => TestValue); + + // Force recording of observable instruments + metricCollector.CollectObservableInstruments(); + + Assert.Equal(3, metricCollector.GetAllObservableUpDownCounters()!.Count); + } + + [Fact] + public void GetAllCounters_WithoutUnsupportedT_Throws() + { + using var meter = new Meter(Guid.NewGuid().ToString()); + using var metricCollector = new MetricCollector(meter); + + Assert.Throws(() => metricCollector.GetAllCounters()); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricValueTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricValueTests.cs new file mode 100644 index 0000000000..9a8c973387 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricValueTests.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Testing.Metering.Test; + +public class MetricValueTests +{ + [Fact] + public void Add_ThrowsWhenWrongValueTypeIsUsed() + { + var metricValue = new MetricValue('1', Array.Empty>(), DateTimeOffset.Now); + + var ex = Assert.Throws(() => metricValue.Add('d')); + Assert.Equal($"The type {typeof(char).FullName} is not supported as a metering measurement value type.", ex.Message); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricValuesHolderTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricValuesHolderTests.cs new file mode 100644 index 0000000000..33f2c8f56c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Metering/MetricValuesHolderTests.cs @@ -0,0 +1,215 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Testing.Metering.Internal; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Testing.Metering.Test; + +public class MetricValuesHolderTests +{ + [Fact] + public void MetricName_MatchesInstrumentName() + { + using var meter = new Meter(Guid.NewGuid().ToString()); + using var metricCollector = new MetricCollector(meter); + + var counter = meter.CreateCounter(Guid.NewGuid().ToString()); + var counterValuesHolder = metricCollector.GetCounterValues(counter.Name); + + Assert.NotNull(counterValuesHolder); + Assert.Equal(counterValuesHolder.MetricName, counter.Name); + + var histogram = meter.CreateHistogram(Guid.NewGuid().ToString()); + var histgramValuesHolder = metricCollector.GetHistogramValues(histogram.Name); + + Assert.NotNull(histgramValuesHolder); + Assert.Equal(histgramValuesHolder.MetricName, histogram.Name); + + var updownCounter = meter.CreateUpDownCounter(Guid.NewGuid().ToString()); + var updownCounterValuesHolder = metricCollector.GetUpDownCounterValues(updownCounter.Name); + + Assert.NotNull(updownCounterValuesHolder); + Assert.Equal(updownCounterValuesHolder.MetricName, updownCounter.Name); + } + + [Fact] + public void LastWrittenValue_ReturnsTheLatestMeasurementValue() + { + using var meter = new Meter(Guid.NewGuid().ToString()); + using var metricCollector = new MetricCollector(meter); + + var counter = meter.CreateCounter(Guid.NewGuid().ToString()); + var counterValuesHolder = metricCollector.GetCounterValues(counter.Name)!; + + Assert.Null(counterValuesHolder.LatestWrittenValue); + + const int CounterValue = 1; + counter.Add(CounterValue); + + Assert.Equal(CounterValue, counterValuesHolder.LatestWrittenValue!.Value); + + var histogram = meter.CreateHistogram(Guid.NewGuid().ToString()); + var histogramValuesHolder = metricCollector.GetHistogramValues(histogram.Name)!; + + Assert.Null(histogramValuesHolder.LatestWrittenValue); + + var testValues = new[] { 20, 40, 60, 100, 200, 1000, 5000 }; + + foreach (var testValue in testValues) + { + histogram.Record(testValue); + + Assert.Equal(testValue, histogramValuesHolder.LatestWrittenValue!.Value); + } + } + + [Fact] + public void LastWritten_ReturnsTheMetricValue() + { + using var meter = new Meter(Guid.NewGuid().ToString()); + using var metricCollector = new MetricCollector(meter); + + var counter = meter.CreateCounter(Guid.NewGuid().ToString()); + var counterValuesHolder = metricCollector.GetCounterValues(counter.Name)!; + + Assert.Null(counterValuesHolder.LatestWritten); + + const int CounterValue = 1; + counter.Add(CounterValue); + + Assert.Equal(CounterValue, counterValuesHolder.LatestWritten!.Value); + + var histogram = meter.CreateHistogram(Guid.NewGuid().ToString()); + var histogramValuesHolder = metricCollector.GetHistogramValues(histogram.Name)!; + + Assert.Null(histogramValuesHolder.LatestWritten); + + const int Value1 = 111; + histogram.Record(Value1); + + var metricValue1 = histogramValuesHolder.LatestWritten; + + Assert.NotNull(metricValue1); + Assert.Equal(Value1, metricValue1.Value); + Assert.Empty(metricValue1.Tags); + + const int Value2 = 2222; + var dimension21 = new KeyValuePair(Guid.NewGuid().ToString(), Guid.NewGuid().ToString()); + histogram.Record(Value2, dimension21); + + var metricValue2 = histogramValuesHolder.LatestWritten; + + Assert.NotNull(metricValue2); + Assert.Equal(Value2, metricValue2.Value); + Assert.Equal(new[] { new KeyValuePair(dimension21.Key, dimension21.Value) }, metricValue2.Tags); + + const int Value3 = 9991; + var dimension31 = new KeyValuePair(Guid.NewGuid().ToString(), Guid.NewGuid().ToString()); + var dimension32 = new KeyValuePair(Guid.NewGuid().ToString(), 1); + var dimension33 = new KeyValuePair(Guid.NewGuid().ToString(), 'c'); + var dimension34 = new KeyValuePair(Guid.NewGuid().ToString(), null); + histogram.Record(Value3, dimension31, dimension32, dimension33, dimension34); + + var metricValue3 = histogramValuesHolder.LatestWritten; + + var expectedTags = new[] + { + new KeyValuePair(dimension31.Key, dimension31.Value), + new KeyValuePair(dimension32.Key, dimension32.Value), + new KeyValuePair(dimension33.Key, dimension33.Value), + new KeyValuePair(dimension34.Key, dimension34.Value), + }; + Array.Sort(expectedTags, (x, y) => StringComparer.OrdinalIgnoreCase.Compare(x.Key, y.Key)); + + Assert.NotNull(metricValue3); + Assert.Equal(Value3, metricValue3.Value); + Assert.Equal(expectedTags, metricValue3.Tags); + } + + [Fact] + public void ReceiveValue_ThrowsWhenInvalidDimensionValue() + { + var metricValuesHolder = new MetricValuesHolder(TimeProvider.System, AggregationType.Save, Guid.NewGuid().ToString()); + + var ex = Assert.Throws(() => metricValuesHolder.ReceiveValue(int.MaxValue, new[] { new KeyValuePair("Dimension1", new object()) })); + Assert.Equal($"The type {typeof(object).FullName} is not supported as a dimension value type.", ex.Message); + + var ex1 = Assert.Throws(() => metricValuesHolder.ReceiveValue(int.MinValue, new[] { new KeyValuePair("Dimension2", new[] { new object() }) })); + Assert.Equal($"The type {typeof(object[]).FullName} is not supported as a dimension value type.", ex1.Message); + } + + [Fact] + public void GetValue_ReturnsCapturedMeasurementValue() + { + var metricValuesHolder = new MetricValuesHolder(System.TimeProvider.System, AggregationType.Save, Guid.NewGuid().ToString()); + + const int Value1 = 1; + metricValuesHolder.ReceiveValue(Value1, null); + + Assert.Equal(Value1, metricValuesHolder.GetValue()); + + const int Value2 = 20; + metricValuesHolder.ReceiveValue(Value2, null); + + Assert.Equal(Value2, metricValuesHolder.GetValue()); + } + + [Fact] + public void ReceiveValue_TimestampIsRecorded() + { + var recordTime = DateTimeOffset.UtcNow.AddDays(-1); + var timeProvider = new FakeTimeProvider(recordTime); + var metricValuesHolder = new MetricValuesHolder(timeProvider, AggregationType.Save, Guid.NewGuid().ToString()); + + metricValuesHolder.ReceiveValue(50, null); + + Assert.NotNull(metricValuesHolder.LatestWrittenValue); + Assert.Equal(recordTime, metricValuesHolder.LatestWritten!.Timestamp); + } + + [Fact] + public void GetDimension_RetursDimensionValue() + { + var metricValuesHolder = new MetricValuesHolder(TimeProvider.System, AggregationType.Save, Guid.NewGuid().ToString()); + + var intDimension = "int_dimension"; + int intVal = 11111; + var stringDimension = "string_dimension"; + var stringValue = Guid.NewGuid().ToString(); + var nullDimension = "null_dimension"; + var doubleDimension = "double_dimension"; + var doubleVal = 78.78d; + + var dimensions = new[] + { + new KeyValuePair(intDimension, intVal), + new KeyValuePair(stringDimension, stringValue), + new KeyValuePair(nullDimension, null), + new KeyValuePair(doubleDimension, doubleVal) + }; + + metricValuesHolder.ReceiveValue(50, dimensions); + + Assert.NotNull(metricValuesHolder.LatestWritten); + Assert.Equal(intVal, metricValuesHolder.LatestWritten.GetDimension(intDimension)); + Assert.Equal(stringValue, metricValuesHolder.LatestWritten.GetDimension(stringDimension)); + Assert.Equal(doubleVal, metricValuesHolder.LatestWritten.GetDimension(doubleDimension)); + Assert.Null(metricValuesHolder.LatestWritten.GetDimension(nullDimension)); + Assert.Null(metricValuesHolder.LatestWritten.GetDimension("invalid_dimension_name")); + } + + [Fact] + public void ReceiveValue_ThrowsWhenInvalidAggregationTypeIsUsed() + { + AggregationType invalidAggregationType = (AggregationType)111; + var metricValuesHolder = new MetricValuesHolder(System.TimeProvider.System, invalidAggregationType, Guid.NewGuid().ToString()); + + var ex = Assert.Throws(() => metricValuesHolder.ReceiveValue(50, new KeyValuePair[0].AsSpan())); + Assert.Equal($"Aggregation type {invalidAggregationType} is not supported.", ex.Message); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Microsoft.Extensions.Telemetry.Testing.Tests.csproj b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Microsoft.Extensions.Telemetry.Testing.Tests.csproj new file mode 100644 index 0000000000..706bf9b8ab --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Testing.Tests/Microsoft.Extensions.Telemetry.Testing.Tests.csproj @@ -0,0 +1,21 @@ + + + Microsoft.Extensions.Telemetry.Testing + Unit tests for Microsoft.Extensions.Telemetry.Testing. + + + + true + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/Internals/TestExtensions.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/Internals/TestExtensions.cs new file mode 100644 index 0000000000..bae07049cd --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/Internals/TestExtensions.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Moq; + +namespace Microsoft.Extensions.Telemetry.Enrichment.Process.Test.Internals; + +internal static class TestExtensions +{ + public static IOptions ToOptions(this T options) + where T : class, new() + { + var mock = new Mock>(); + mock.Setup(o => o.Value).Returns(options); + return mock.Object; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/ProcessEnricherDimensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/ProcessEnricherDimensionsTests.cs new file mode 100644 index 0000000000..04da89ef1a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/ProcessEnricherDimensionsTests.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using FluentAssertions; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Enrichment.Process.Test; + +public class ProcessEnricherDimensionsTests +{ + [Fact] + public void GetDimensionNames_ReturnsAnArrayOfDimensionNames() + { + IReadOnlyList dimensions = ProcessEnricherDimensions.DimensionNames; + string[] expectedDimensions = GetStringConstants(typeof(ProcessEnricherDimensions)); + dimensions.Should().BeEquivalentTo(expectedDimensions); + } + + private static string[] GetStringConstants(IReflect type) + { + FieldInfo[] fields = type.GetFields(BindingFlags.Public | BindingFlags.Static); + + return fields + .Where(f => f.IsLiteral && f.FieldType == typeof(string)) + .Select(f => (string)f.GetValue(null)!) + .ToArray(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/ProcessEnricherExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/ProcessEnricherExtensionsTests.cs new file mode 100644 index 0000000000..c8ab5a128e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/ProcessEnricherExtensionsTests.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Enrichment; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Enrichment.Process.Test; + +public class ProcessEnricherExtensionsTests +{ + [Fact] + public void ProcessLogEnricher_GivenAnyNullArgument_Throws() + { + Assert.Throws(() => + ((IServiceCollection)null!).AddProcessLogEnricher()); + + Assert.Throws(() => + ((IServiceCollection)null!).AddProcessLogEnricher(_ => { })); + + Assert.Throws(() => + ((IServiceCollection)null!).AddProcessLogEnricher(Mock.Of())); + + Assert.Throws(() => + new ServiceCollection().AddProcessLogEnricher((IConfigurationSection)null!)); + + Assert.Throws(() => + new ServiceCollection().AddProcessLogEnricher((Action)null!)); + } + + [Fact] + public void ProcessLogEnricher_GivenNoArguments_RegistersInDI() + { + // Arrange & Act + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services.AddProcessLogEnricher()) + .Build(); + + // Assert + Assert.NotNull(host.Services.GetRequiredService()); + } + + [Fact] + public void ProcessLogEnricher_GivenOptions_RegistersInDI() + { + // Arrange & Act + using var host = FakeHost.CreateBuilder() + .ConfigureLogging(builder => builder + .Services.AddProcessLogEnricher(options => + { + options.ProcessId = false; + options.ThreadId = false; + })) + .Build(); + + // Assert + Assert.NotNull(host.Services.GetRequiredService()); + var options = host.Services.GetRequiredService>().Value; + Assert.NotNull(options); + Assert.False(options.ProcessId); + Assert.False(options.ThreadId); + } + + [Fact] + public void ProcessLogEnricher_GivenConfiguration_RegistersInDI() + { + // Arrange & Act + const string TestSectionName = "processenrichersection"; + using var host = FakeHost.CreateBuilder() + .ConfigureAppConfiguration( + ($"{TestSectionName}:{nameof(ProcessLogEnricherOptions.ProcessId)}", "true"), + ($"{TestSectionName}:{nameof(ProcessLogEnricherOptions.ThreadId)}", "false")) + .ConfigureServices((context, services) => services + .AddProcessLogEnricher(context.Configuration.GetSection(TestSectionName))) + .Build(); + + // Assert + var enricher = host.Services.GetRequiredService(); + Assert.NotNull(enricher); + Assert.IsType(enricher); + var options = host.Services.GetRequiredService>().Value; + Assert.NotNull(options); + Assert.True(options.ProcessId); + Assert.False(options.ThreadId); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/ProcessLogEnricherTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/ProcessLogEnricherTests.cs new file mode 100644 index 0000000000..5e21810e1b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/ProcessLogEnricherTests.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Enrichment.Process.Test.Internals; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Enrichment.Process.Test; + +public class ProcessLogEnricherTests +{ + private readonly int _processId = System.Diagnostics.Process.GetCurrentProcess().Id; + + [Fact] + public void ProcessLogEnricher_GivenInvalidArguments_Throws() + { + // Arrange + var optionsNull = new Mock>(); + optionsNull.Setup(o => o.Value).Returns>(null!); + + // Act & Assert + Assert.Throws(() => new ProcessLogEnricher(optionsNull.Object)); + } + + [Fact] + public void ProcessLogEnricherOptions_EnabledByDefault() + { + // Arrange & Act + var options = new ProcessLogEnricherOptions(); + + // Assert + Assert.True(options.ProcessId); + Assert.False(options.ThreadId); + } + + [Fact] + public void ProcessLogEnricher_GivenEnricherOptions_Enriches() + { + // Arrange + var options = new ProcessLogEnricherOptions + { + ProcessId = true, + ThreadId = true + }; + + var enricher = new ProcessLogEnricher(options.ToOptions()); + var enrichedProperties = new TestLogEnrichmentPropertyBag(new List>()); + + // Act + enricher.Enrich(enrichedProperties); + var enrichedState = enrichedProperties.Properties; + + // Assert + if (options.ThreadId) + { + Assert.Equal(Thread.CurrentThread.ManagedThreadId.ToString(CultureInfo.InvariantCulture), enrichedState[ProcessEnricherDimensions.ThreadId]); + } + + if (options.ProcessId) + { + Assert.Equal(_processId.ToString(CultureInfo.InvariantCulture), enrichedState[ProcessEnricherDimensions.ProcessId]); + } + } + + [Fact] + public void ProcessLogEnricher_GivenDisabledEnricherOptions_DoesNotEnrich() + { + // Arrange + var options = new ProcessLogEnricherOptions + { + ProcessId = false, + ThreadId = false + }; + + var enricher = new ProcessLogEnricher(options.ToOptions()); + var enrichedProperties = new TestLogEnrichmentPropertyBag(); + + // Act + enricher.Enrich(enrichedProperties); + var enrichedState = enrichedProperties.Properties; + + // Assert + Assert.False(enrichedState.ContainsKey(ProcessEnricherDimensions.ProcessId)); + Assert.False(enrichedState.ContainsKey(ProcessEnricherDimensions.ThreadId)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/TestLogEnrichmentPropertyBag.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/TestLogEnrichmentPropertyBag.cs new file mode 100644 index 0000000000..2af9fef233 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Process/TestLogEnrichmentPropertyBag.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Telemetry.Enrichment.Process.Test; + +public class TestLogEnrichmentPropertyBag : IEnrichmentPropertyBag +{ + private readonly Dictionary _properties = new(); + + public TestLogEnrichmentPropertyBag(IEnumerable>? input = null) + { + if (input != null) + { + foreach (var kvp in input) + { + _properties.Add(kvp.Key, kvp.Value); + } + } + } + + public IReadOnlyDictionary Properties => _properties; + + public void Add(string key, object value) + { + _properties.Add(key, value); + } + + public void Add(string key, string value) + { + _properties.Add(key, value); + } + + public void Add(ReadOnlySpan> properties) + { + foreach (var p in properties) + { + _properties.Add(p.Key, p.Value); + } + } + + public void Add(ReadOnlySpan> properties) + { + foreach (var p in properties) + { + _properties.Add(p.Key, p.Value); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/Internals/TestExtensions.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/Internals/TestExtensions.cs new file mode 100644 index 0000000000..08217d10d8 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/Internals/TestExtensions.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Moq; + +namespace Microsoft.Extensions.Telemetry.Enrichment.Service.Test.Internals; + +internal static class TestExtensions +{ + public static IOptions ToOptions(this T options) + where T : class, new() + { + var mock = new Mock>(); + mock.Setup(o => o.Value).Returns(options); + return mock.Object; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/Internals/TestLogEnrichmentPropertyBag.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/Internals/TestLogEnrichmentPropertyBag.cs new file mode 100644 index 0000000000..c43943f84d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/Internals/TestLogEnrichmentPropertyBag.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Telemetry.Enrichment.Service.Test.Internals; + +public class TestLogEnrichmentPropertyBag : IEnrichmentPropertyBag +{ + private readonly Dictionary _properties = new(); + + public TestLogEnrichmentPropertyBag(IEnumerable>? input = null) + { + if (input != null) + { + foreach (var kvp in input) + { + _properties.Add(kvp.Key, kvp.Value); + } + } + } + + public IReadOnlyDictionary Properties => _properties; + + public void Add(string key, object value) + { + _properties.Add(key, value); + } + + public void Add(string key, string value) + { + _properties.Add(key, value); + } + + public void Add(ReadOnlySpan> properties) + { + foreach (var p in properties) + { + _properties.Add(p.Key, p.Value); + } + } + + public void Add(ReadOnlySpan> properties) + { + foreach (var p in properties) + { + _properties.Add(p.Key, p.Value); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/Internals/TestMetricEnrichmentPropertyBag.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/Internals/TestMetricEnrichmentPropertyBag.cs new file mode 100644 index 0000000000..cf92c1bd6b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/Internals/TestMetricEnrichmentPropertyBag.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Telemetry.Enrichment.Service.Test.Internals; + +public class TestMetricEnrichmentPropertyBag : IEnrichmentPropertyBag +{ + private readonly Dictionary _properties = new(); + + public TestMetricEnrichmentPropertyBag(IEnumerable>? input = null) + { + if (input != null) + { + foreach (var kvp in input) + { + _properties.Add(kvp.Key, kvp.Value.ToString() ?? string.Empty); + } + } + } + + public IReadOnlyDictionary Properties => _properties; + + public void Add(string key, object value) + { + _properties.Add(key, value.ToString() ?? string.Empty); + } + + public void Add(string key, string value) + { + _properties.Add(key, value); + } + + public void Add(ReadOnlySpan> properties) + { + foreach (var p in properties) + { + _properties.Add(p.Key, p.Value.ToString() ?? string.Empty); + } + } + + public void Add(ReadOnlySpan> properties) + { + foreach (var p in properties) + { + _properties.Add(p.Key, p.Value); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceEnricherDimensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceEnricherDimensionsTests.cs new file mode 100644 index 0000000000..0b5f22e136 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceEnricherDimensionsTests.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using FluentAssertions; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Enrichment.Service.Test; + +public class ServiceEnricherDimensionsTests +{ + [Fact] + public void GetDimensionNames_ReturnsAnArrayOfDimensionNames() + { + IReadOnlyList dimensions = ServiceEnricherDimensions.DimensionNames; + + string[] expectedDimensions = GetStringConstants(typeof(ServiceEnricherDimensions)); + + dimensions.Should().BeEquivalentTo(expectedDimensions); + } + + private static string[] GetStringConstants(IReflect type) + { + FieldInfo[] fields = type.GetFields(BindingFlags.Public | BindingFlags.Static); + + return fields + .Where(f => f.IsLiteral && f.FieldType == typeof(string)) + .Select(f => (string)f.GetValue(null)!) + .ToArray(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceEnricherExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceEnricherExtensionsTests.cs new file mode 100644 index 0000000000..1b026113f1 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceEnricherExtensionsTests.cs @@ -0,0 +1,279 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Enrichment; +using Moq; +using OpenTelemetry.Trace; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Enrichment.Service.Test; + +public class ServiceEnricherExtensionsTests +{ + [Fact] + public void ServiceLogEnricher_GivenAnyNullArgument_Throws() + { + Assert.Throws(() => + ((IServiceCollection)null!).AddServiceLogEnricher()); + + Assert.Throws(() => + ((IServiceCollection)null!).AddServiceLogEnricher(_ => { })); + + Assert.Throws(() => + ((IServiceCollection)null!).AddServiceLogEnricher(Mock.Of())); + + Assert.Throws(() => + new ServiceCollection().AddServiceLogEnricher((IConfigurationSection)null!)); + } + + [Fact] + public void ServiceLogEnricher_GivenNoArguments_RegistersInDI() + { + // Arrange & Act + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services.AddServiceLogEnricher()) + .Build(); + + // Assert + Assert.NotNull(host.Services.GetRequiredService()); + } + + [Fact] + public void HostLogEnricher_GivenOptions_RegistersInDI() + { + // Arrange & Act + using var host = FakeHost.CreateBuilder() + .ConfigureLogging(builder => builder + .Services.AddServiceLogEnricher(e => + { + e.ApplicationName = false; + e.EnvironmentName = false; + e.BuildVersion = false; + e.DeploymentRing = false; + })) + .Build(); + + // Assert + Assert.NotNull(host.Services.GetRequiredService()); + var options = host.Services.GetRequiredService>().Value; + Assert.NotNull(options); + Assert.False(options.ApplicationName); + Assert.False(options.EnvironmentName); + Assert.False(options.BuildVersion); + Assert.False(options.DeploymentRing); + } + + [Fact] + public void ServiceLogEnricher_GivenConfiguration_RegistersInDI() + { + // Arrange & Act + using var host = FakeHost.CreateBuilder() + .ConfigureAppConfiguration( + ("Serviceenrichersection:ApplicationName", "true"), + ("Serviceenrichersection:EnvironmentName", "false"), + ("Serviceenrichersection:BuildVersion", "true"), + ("Serviceenrichersection:DeploymentRing", "true")) + .ConfigureServices((context, services) => services + .AddServiceLogEnricher(context.Configuration.GetSection("Serviceenrichersection"))) + .Build(); + + // Assert + var enricher = host.Services.GetRequiredService(); + Assert.NotNull(enricher); + Assert.IsType(enricher); + var options = host.Services.GetRequiredService>().Value; + Assert.NotNull(options); + Assert.True(options.ApplicationName); + Assert.False(options.EnvironmentName); + Assert.True(options.BuildVersion); + Assert.True(options.DeploymentRing); + } + + [Fact] + public void ServiceMetricEnricher_GivenAnyNullArgument_Throws() + { + Assert.Throws(() => + ((IServiceCollection)null!).AddServiceMetricEnricher()); + + Assert.Throws(() => + ((IServiceCollection)null!).AddServiceMetricEnricher(_ => { })); + + Assert.Throws(() => + ((IServiceCollection)null!).AddServiceMetricEnricher(Mock.Of())); + + Assert.Throws(() => + new ServiceCollection().AddServiceMetricEnricher((IConfigurationSection)null!)); + } + + [Fact] + public void HostMetricEnricher_GivenNoArguments_RegistersInDI() + { + // Arrange & Act + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services.AddServiceMetricEnricher()) + .Build(); + + // Assert + Assert.NotNull(host.Services.GetRequiredService()); + var options = host.Services.GetRequiredService>(); + Assert.NotNull(options); + Assert.NotNull(options.Value); + Assert.True(options.Value.ApplicationName); + Assert.True(options.Value.EnvironmentName); + Assert.False(options.Value.BuildVersion); + Assert.False(options.Value.DeploymentRing); + } + + [Fact] + public void ServiceMetricEnricher_GivenOptions_RegistersInDI() + { + // Arrange & Act + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddServiceMetricEnricher(e => + { + e.ApplicationName = false; + e.EnvironmentName = false; + e.BuildVersion = false; + e.DeploymentRing = false; + })) + .Build(); + + // Assert + Assert.NotNull(host.Services.GetRequiredService()); + var options = host.Services.GetRequiredService>().Value; + Assert.NotNull(options); + Assert.False(options.ApplicationName); + Assert.False(options.EnvironmentName); + Assert.False(options.BuildVersion); + Assert.False(options.DeploymentRing); + } + + [Fact] + public void ServiceMetricEnricher_GivenConfiguration_RegistersInDI() + { + // Arrange & Act + using var host = FakeHost.CreateBuilder() + .ConfigureAppConfiguration( + ("Serviceenrichersection:ApplicationName", "true"), + ("Serviceenrichersection:EnvironmentName", "false"), + ("Serviceenrichersection:BuildVersion", "true"), + ("Serviceenrichersection:DeploymentRing", "true")) + .ConfigureServices((context, services) => services + .AddServiceMetricEnricher(context.Configuration.GetSection("Serviceenrichersection"))) + .Build(); + + // Assert + var enricher = host.Services.GetRequiredService(); + Assert.NotNull(enricher); + Assert.IsType(enricher); + var options = host.Services.GetRequiredService>().Value; + Assert.NotNull(options); + Assert.True(options.ApplicationName); + Assert.False(options.EnvironmentName); + Assert.True(options.BuildVersion); + Assert.True(options.DeploymentRing); + } + + [Fact] + public void ServiceTraceEnricher_GivenAnyNullArgument_Throws() + { + Assert.Throws(() => + ((TracerProviderBuilder)null!).AddServiceTraceEnricher()); + + Assert.Throws(() => + ((TracerProviderBuilder)null!).AddServiceTraceEnricher(_ => { })); + + Assert.Throws(() => + ((TracerProviderBuilder)null!).AddServiceTraceEnricher(Mock.Of())); + + var services = new ServiceCollection(); + Assert.Throws(() => + services.AddOpenTelemetry().WithTracing(builder => + builder.AddServiceTraceEnricher((Action)null!))); + + Assert.Throws(() => + services.AddOpenTelemetry().WithTracing(builder => + builder.AddServiceTraceEnricher((IConfigurationSection)null!))); + } + + [Fact] + public void ServiceTraceEnricher_GivenNoArguments_RegistersInDI() + { + // Arrange & Act + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => + services.AddOpenTelemetry().WithTracing(builder => + builder.AddServiceTraceEnricher())) + .Build(); + + // Assert + Assert.NotNull(host.Services.GetRequiredService()); + var options = host.Services.GetRequiredService>(); + Assert.NotNull(options); + Assert.NotNull(options.Value); + Assert.True(options.Value.ApplicationName); + Assert.True(options.Value.EnvironmentName); + Assert.False(options.Value.BuildVersion); + Assert.False(options.Value.DeploymentRing); + } + + [Fact] + public void ServiceTraceEnricher_GivenOptions_RegistersInDI() + { + // Arrange & Act + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddServiceTraceEnricher(e => + { + e.ApplicationName = false; + e.EnvironmentName = false; + e.BuildVersion = false; + e.DeploymentRing = false; + }))) + .Build(); + + // Assert + Assert.NotNull(host.Services.GetRequiredService()); + var options = host.Services.GetRequiredService>().Value; + Assert.NotNull(options); + Assert.False(options.ApplicationName); + Assert.False(options.EnvironmentName); + Assert.False(options.BuildVersion); + Assert.False(options.DeploymentRing); + } + + [Fact] + public void ServiceTraceEnricher_GivenConfiguration_RegistersInDI() + { + // Arrange & Act + using var host = FakeHost.CreateBuilder() + .ConfigureAppConfiguration( + ("Serviceenrichersection:ApplicationName", "true"), + ("Serviceenrichersection:EnvironmentName", "false"), + ("Serviceenrichersection:BuildVersion", "true"), + ("Serviceenrichersection:DeploymentRing", "true")) + .ConfigureServices((context, services) => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddServiceTraceEnricher(context.Configuration.GetSection("Serviceenrichersection")))) + .Build(); + + // Assert + var enricher = host.Services.GetRequiredService(); + Assert.NotNull(enricher); + Assert.IsType(enricher); + var options = host.Services.GetRequiredService>().Value; + Assert.NotNull(options); + Assert.True(options.ApplicationName); + Assert.False(options.EnvironmentName); + Assert.True(options.BuildVersion); + Assert.True(options.DeploymentRing); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceEnricherOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceEnricherOptionsTests.cs new file mode 100644 index 0000000000..9638575110 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceEnricherOptionsTests.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Enrichment.Service.Test; + +public class ServiceEnricherOptionsTests +{ + [Fact] + public void ServiceLogEnricherOptions_EnsureDefaultValues() + { + var options = new ServiceLogEnricherOptions(); + options.EnvironmentName.Should().BeTrue(); + options.ApplicationName.Should().BeTrue(); + options.BuildVersion.Should().BeFalse(); + options.DeploymentRing.Should().BeFalse(); + } + + [Fact] + public void ServiceMetricEnricherOptions_EnsureDefaultValues() + { + var options = new ServiceMetricEnricherOptions(); + options.EnvironmentName.Should().BeTrue(); + options.ApplicationName.Should().BeTrue(); + options.BuildVersion.Should().BeFalse(); + options.DeploymentRing.Should().BeFalse(); + } + + [Fact] + public void ServiceTraceEnricherOptions_EnsureDefaultValues() + { + var options = new ServiceTraceEnricherOptions(); + options.EnvironmentName.Should().BeTrue(); + options.ApplicationName.Should().BeTrue(); + options.BuildVersion.Should().BeFalse(); + options.DeploymentRing.Should().BeFalse(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceLogEnricherTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceLogEnricherTests.cs new file mode 100644 index 0000000000..55e57b5a89 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceLogEnricherTests.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.AmbientMetadata; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Enrichment.Service.Test.Internals; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Enrichment.Service.Test; + +public class ServiceLogEnricherTests +{ + private const string AppName = "appNameTestValue"; + private const string EnvironmentName = "environmentTestValue"; + private const string BuildVersion = "buildVersionTestValue"; + private const string DeploymentRing = "deploymentRingTestValue"; + + private readonly Mock _hostMock; + + public ServiceLogEnricherTests() + { + _hostMock = new Mock(MockBehavior.Strict); + _hostMock.SetupGet(c => c.EnvironmentName).Returns(EnvironmentName); + _hostMock.SetupGet(c => c.ApplicationName).Returns(AppName); + } + + [Fact] + public void HostLogEnricher_GivenInvalidArguments_Throws() + { + // Arrange + var options = new ServiceLogEnricherOptions + { + BuildVersion = true, + DeploymentRing = true + }.ToOptions(); + var optionsNull = new Mock>(); + optionsNull.Setup(o => o.Value).Returns>(null!); + + var serviceOptionsNull = new Mock>(); + serviceOptionsNull.Setup(o => o.Value).Returns>(null!); + + // Act & Assert + Assert.Throws(() => new ServiceLogEnricher(optionsNull.Object, null!)); + Assert.Throws(() => new ServiceLogEnricher(options, serviceOptionsNull.Object)); + } + + [Theory] + [InlineData(true, true, true, true, null, null)] + [InlineData(true, true, true, true, BuildVersion, DeploymentRing)] + [InlineData(false, false, false, false, null, null)] + [InlineData(false, false, false, false, BuildVersion, DeploymentRing)] + public void ServiceLogEnricher_Options(bool appName, bool envName, bool buildVer, bool depRing, string? buildVersion, string? deploymentRing) + { + // Arrange + var options = new ServiceLogEnricherOptions + { + ApplicationName = appName, + EnvironmentName = envName, + BuildVersion = buildVer, + DeploymentRing = depRing, + }; + + var serviceOptions = new ApplicationMetadata + { + BuildVersion = buildVersion, + DeploymentRing = deploymentRing, + ApplicationName = _hostMock.Object.ApplicationName, + EnvironmentName = _hostMock.Object.EnvironmentName + }; + + var enricher = new ServiceLogEnricher(options.ToOptions(), serviceOptions.ToOptions()); + var enrichedProperties = new TestLogEnrichmentPropertyBag(); + + // Act + enricher.Enrich(enrichedProperties); + var enrichedState = enrichedProperties.Properties; + + // Assert + if (appName) + { + Assert.Equal(AppName, enrichedState[ServiceEnricherDimensions.ApplicationName]); + } + else + { + Assert.False(enrichedState.ContainsKey(ServiceEnricherDimensions.ApplicationName)); + } + + if (envName) + { + Assert.Equal(EnvironmentName, enrichedState[ServiceEnricherDimensions.EnvironmentName]); + } + else + { + Assert.False(enrichedState.ContainsKey(ServiceEnricherDimensions.EnvironmentName)); + } + + if (buildVer && buildVersion != null) + { + Assert.Equal(BuildVersion, enrichedState[ServiceEnricherDimensions.BuildVersion]); + } + else + { + Assert.False(enrichedState.ContainsKey(ServiceEnricherDimensions.BuildVersion)); + } + + if (depRing && deploymentRing != null) + { + Assert.Equal(DeploymentRing, enrichedState[ServiceEnricherDimensions.DeploymentRing]); + } + else + { + Assert.False(enrichedState.ContainsKey(ServiceEnricherDimensions.DeploymentRing)); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceMetricEnricherTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceMetricEnricherTests.cs new file mode 100644 index 0000000000..de9b0ed6cf --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceMetricEnricherTests.cs @@ -0,0 +1,130 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.AmbientMetadata; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Enrichment.Service.Test.Internals; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Enrichment.Service.Test; + +public class ServiceMetricEnricherTests +{ + private const string AppName = "appNameTestValue"; + private const string EnvironmentName = "environmentTestValue"; + private const string BuildVersion = "buildVersionTestValue"; + private const string DeploymentRing = "deploymentRingTestValue"; + + private readonly Mock _hostMock; + + public ServiceMetricEnricherTests() + { + _hostMock = new Mock(MockBehavior.Strict); + _hostMock.SetupGet(c => c.EnvironmentName).Returns(EnvironmentName); + _hostMock.SetupGet(c => c.ApplicationName).Returns(AppName); + } + + [Fact] + public void ServiceMetricEnricher_GivenInvalidArguments_Throws() + { + // Arrange + var options = new ServiceMetricEnricherOptions + { + BuildVersion = true, + DeploymentRing = true + }.ToOptions(); + var optionsNull = new Mock>(); + optionsNull.Setup(o => o.Value).Returns>(null!); + + var serviceOptionsNull = new Mock>(); + serviceOptionsNull.Setup(o => o.Value).Returns>(null!); + + // Act & Assert + Assert.Throws(() => new ServiceMetricEnricher(optionsNull.Object, null!)); + Assert.Throws(() => new ServiceMetricEnricher(options, serviceOptionsNull.Object)); + } + + [Fact] + public void ServiceMetricEnricher_GivenEnricherOptions_Enriches() + { + // Arrange + var options = new ServiceMetricEnricherOptions + { + ApplicationName = true, + EnvironmentName = true, + BuildVersion = true, + DeploymentRing = true, + }; + + var serviceOptions = new ApplicationMetadata + { + BuildVersion = BuildVersion, + DeploymentRing = DeploymentRing, + ApplicationName = _hostMock.Object.ApplicationName, + EnvironmentName = _hostMock.Object.EnvironmentName + }; + + var enricher = new ServiceMetricEnricher(options.ToOptions(), serviceOptions.ToOptions()); + var enrichedProperties = new TestMetricEnrichmentPropertyBag(); + + // Act + enricher.Enrich(enrichedProperties); + var enrichedState = enrichedProperties.Properties; + + // Assert + if (options.ApplicationName) + { + Assert.Equal(AppName, enrichedState[ServiceEnricherDimensions.ApplicationName]); + } + + if (options.EnvironmentName) + { + Assert.Equal(EnvironmentName, enrichedState[ServiceEnricherDimensions.EnvironmentName]); + } + + if (options.BuildVersion) + { + Assert.Equal(BuildVersion, enrichedState[ServiceEnricherDimensions.BuildVersion]); + } + + if (options.DeploymentRing) + { + Assert.Equal(DeploymentRing, enrichedState[ServiceEnricherDimensions.DeploymentRing]); + } + } + + [Fact] + public void ServiceMetricEnricher_GivenDisabledEnricherOptions_DoesNotEnrich() + { + // Arrange + var options = new ServiceMetricEnricherOptions + { + ApplicationName = false, + EnvironmentName = false, + BuildVersion = false, + DeploymentRing = false, + }; + + var serviceOptions = new ApplicationMetadata + { + BuildVersion = BuildVersion, + DeploymentRing = DeploymentRing + }; + + var enricher = new ServiceMetricEnricher(options.ToOptions(), serviceOptions.ToOptions()); + var enrichedProperties = new TestMetricEnrichmentPropertyBag(); + + // Act + enricher.Enrich(enrichedProperties); + var enrichedState = enrichedProperties.Properties; + + // Assert + Assert.False(enrichedState.ContainsKey(ServiceEnricherDimensions.ApplicationName)); + Assert.False(enrichedState.ContainsKey(ServiceEnricherDimensions.EnvironmentName)); + Assert.False(enrichedState.ContainsKey(ServiceEnricherDimensions.BuildVersion)); + Assert.False(enrichedState.ContainsKey(ServiceEnricherDimensions.DeploymentRing)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceTraceEnricherTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceTraceEnricherTests.cs new file mode 100644 index 0000000000..e43f99de06 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Enrichment.Service/ServiceTraceEnricherTests.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.Extensions.AmbientMetadata; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Telemetry.Enrichment.Service.Test.Internals; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Enrichment.Service.Test; + +public class ServiceTraceEnricherTests +{ + private const string AppName = "appNameTestValue"; + private const string EnvironmentName = "environmentTestValue"; + private const string BuildVersion = "buildVersionTestValue"; + private const string DeploymentRing = "deploymentRingTestValue"; + + private readonly Mock _hostMock; + + public ServiceTraceEnricherTests() + { + _hostMock = new Mock(MockBehavior.Strict); + _hostMock.SetupGet(c => c.EnvironmentName).Returns(EnvironmentName); + _hostMock.SetupGet(c => c.ApplicationName).Returns(AppName); + } + + [Fact] + public void ServiceTraceEnricher_GivenEnricherOptions_Enriches() + { + // Arrange + var options = new ServiceTraceEnricherOptions + { + ApplicationName = true, + EnvironmentName = true, + BuildVersion = true, + DeploymentRing = true, + }; + + var serviceOptions = new ApplicationMetadata + { + BuildVersion = BuildVersion, + DeploymentRing = DeploymentRing, + ApplicationName = _hostMock.Object.ApplicationName, + EnvironmentName = _hostMock.Object.EnvironmentName + }; + + var enricher = new ServiceTraceEnricher(options.ToOptions(), serviceOptions.ToOptions()); + var enrichedProperties = new TestLogEnrichmentPropertyBag(); + using var activity = new Activity("test"); + + // Act + enricher.Enrich(activity); + var enrichedState = activity.Tags.ToDictionary(static x => x.Key, static x => x.Value); + + // Assert + Assert.Equal(AppName, enrichedState[ServiceEnricherDimensions.ApplicationName]); + Assert.Equal(EnvironmentName, enrichedState[ServiceEnricherDimensions.EnvironmentName]); + Assert.Equal(BuildVersion, enrichedState[ServiceEnricherDimensions.BuildVersion]); + Assert.Equal(DeploymentRing, enrichedState[ServiceEnricherDimensions.DeploymentRing]); + } + + [Fact] + public void ServiceTraceEnricher_GivenDisabledEnricherOptions_DoesNotEnrich() + { + // Arrange + var options = new ServiceTraceEnricherOptions + { + ApplicationName = false, + EnvironmentName = false, + BuildVersion = false, + DeploymentRing = false, + }; + + var serviceOptions = new ApplicationMetadata + { + BuildVersion = BuildVersion, + DeploymentRing = DeploymentRing + }; + + var enricher = new ServiceTraceEnricher(options.ToOptions(), serviceOptions.ToOptions()); + var enrichedProperties = new TestLogEnrichmentPropertyBag(); + using var activity = new Activity("test"); + + // Act + enricher.Enrich(activity); + IReadOnlyDictionary enrichedState = activity.Tags.ToDictionary(static x => x.Key, static x => x.Value); + + // Assert + Assert.DoesNotContain(ServiceEnricherDimensions.ApplicationName, enrichedState); + Assert.DoesNotContain(ServiceEnricherDimensions.EnvironmentName, enrichedState); + Assert.DoesNotContain(ServiceEnricherDimensions.BuildVersion, enrichedState); + Assert.DoesNotContain(ServiceEnricherDimensions.DeploymentRing, enrichedState); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/CheckpointTrackerTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/CheckpointTrackerTest.cs new file mode 100644 index 0000000000..5abb89b74e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/CheckpointTrackerTest.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.Extensions.Telemetry.Latency.Internal; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Latency.Test.Internal; + +public class CheckpointTrackerTest +{ + private static readonly Registry _checkpointsName = new(new[] { "a", "b", "c", "d" }, false); + + [Fact] + public void CheckpointTracker_AddUnregisteredName() + { + CheckpointTracker ct = new CheckpointTracker(_checkpointsName); + ct.Add(ct.GetToken("e")); + Assert.True(ct.Checkpoints.Count == 0); + } + + [Fact] + public void CheckpointTracker_AddRegisteredNames() + { + CheckpointTracker ct = new CheckpointTracker(_checkpointsName); + var t = ct.Elapsed; + string[] names = { "a", "b", "c" }; + + for (int i = 0; i < names.Length; i++) + { + ct.Add(ct.GetToken(names[i])); + } + + var c = ct.Checkpoints.ToList(); + Assert.True(c.Count == names.Length); + + for (int i = 0; i < names.Length; i++) + { + var elapsed = c[i].Elapsed; + + // Verify names are in order and timestamp ascending + Assert.True(c[i].Name == names[i]); + Assert.True(elapsed >= t); + t = elapsed; + } + } + + [Fact] + public void CheckpointTracker_AddDuplicateNames_FirstWriteWins() + { + CheckpointTracker ct = new CheckpointTracker(_checkpointsName); + ct.Add(ct.GetToken("a")); + var first = ct.Checkpoints.First(); + ct.Add(ct.GetToken("a")); + + // Verify value unchanged + var checkpoints = ct.Checkpoints.ToList(); + Assert.True(checkpoints.Count == 1); + Assert.True(first.Name == checkpoints[0].Name); + Assert.True(first.Elapsed == checkpoints[0].Elapsed); + Assert.True(first.Frequency == checkpoints[0].Frequency); + } + + [Fact] + public void CheckpointTracker_CheckReset() + { + CheckpointTracker ct = new CheckpointTracker(_checkpointsName); + string[] names = { "a", "b", "c" }; + + for (int i = 0; i < names.Length; i++) + { + ct.Add(ct.GetToken(names[i])); + } + + var c = ct.Checkpoints; + Assert.True(c.Count == names.Length); + + _ = ct.TryReset(); + c = ct.Checkpoints; + Assert.True(c.Count == 0); + + for (int i = 0; i < names.Length; i++) + { + ct.Add(ct.GetToken(names[i])); + } + + c = ct.Checkpoints; + Assert.True(c.Count == names.Length); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextExtensions.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextExtensions.cs new file mode 100644 index 0000000000..d50b526d9d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextExtensions.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Telemetry.Latency.Internal; + +namespace Microsoft.Extensions.Telemetry.Latency.Test.Internal; + +internal static class LatencyContextExtensions +{ + public static bool IsRegistered(this Registry registry, string name) + { + return registry.GetRegisteredKeyIndex(name) > -1; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextPoolTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextPoolTest.cs new file mode 100644 index 0000000000..aee8bf679c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextPoolTest.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Latency.Internal; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Latency.Test.Internal; + +public class LatencyContextPoolTest +{ + private readonly string[] _checkpoints = new[] { "ca", "cb", "cc", "cd" }; + private readonly string[] _tags = new[] { "ta", "tb", "tc", "td" }; + private readonly string[] _measures = new[] { "ma", "mb", "mc", "md" }; + + [Fact] + public void LatencyContextPool_GivesInstruments() + { + var lcp = GetLatencyContextPool(); + Assert.NotNull(lcp); + using var lc = lcp.Pool.Get(); + Assert.NotNull(lc); + } + + [Fact] + public void LatencyContextPool_DoesNotGive_ContextInUse() + { + var lcp = GetLatencyContextPool(); + + using var lc = lcp.Pool.Get(); + using var lc1 = lcp.Pool.Get(); + using var lc2 = lcp.Pool.Get(); + + Assert.NotEqual(lc, lc1); + Assert.NotEqual(lc, lc2); + Assert.NotEqual(lc1, lc2); + } + + [Fact] + public void LatencyContextPool_Get_LatencyContextCorrectState() + { + var lcp = GetLatencyContextPool(); + + var o = lcp.Pool.Get(); + Assert.True(o.IsRunning); + Assert.False(o.IsDisposed); + o.Dispose(); + Assert.False(o.IsRunning); + Assert.True(o.IsDisposed); + var o1 = lcp.Pool.Get(); + Assert.True(o1.IsRunning); + Assert.False(o.IsDisposed); + } + + [Fact] + public void RestOnGetPool_Get_CallsReset() + { + var p = new ResetOnGetObjectPool( + new NoResetPolicy()); + + var o = p.Get(); + Assert.True(o.ResetCalled == 1); + p.Return(o); + o = p.Get(); + Assert.True(o.ResetCalled == 1); + } + + private class NoResetPolicy : PooledObjectPolicy + { + public override Resettable Create() + { + return new Resettable(); + } + + public override bool Return(Resettable obj) => false; + } + + private class Resettable : IResettable + { + public int ResetCalled; + + public bool TryReset() + { + ResetCalled++; + return true; + } + } + + private LatencyContextPool GetLatencyContextPool() + { + return new LatencyContextPool(new LatencyInstrumentProvider(GetRegistry())); + } + + private LatencyContextRegistrySet GetRegistry() + { + var option = MockLatencyContextRegistrationOptions.GetLatencyContextRegistrationOptions( + _checkpoints, _measures, _tags); + + var lco = new Mock>(); + lco.Setup(a => a.Value).Returns(new LatencyContextOptions { ThrowOnUnregisteredNames = false }); + + var r = new LatencyContextRegistrySet(lco.Object, option); + + return r; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextProviderTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextProviderTest.cs new file mode 100644 index 0000000000..9a639cdb27 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextProviderTest.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Extensions.Telemetry.Latency.Internal; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Latency.Test.Internal; + +public class LatencyContextProviderTest +{ + [Fact] + public void Provider_CreateGetsNewContext() + { + var options = new LatencyContextOptions + { + ThrowOnUnregisteredNames = false + }; + + var lip = GetLatencyInstrumentProvider(options); + var lcp = new LatencyContextProvider(lip); + + Assert.NotNull(lcp.CreateContext()); + Assert.NotSame(lcp.CreateContext(), lcp.CreateContext()); + } + + [Fact] + public void Provider_NoThrowOptions() + { + var options = new LatencyContextOptions + { + ThrowOnUnregisteredNames = false + }; + + var lip = GetLatencyInstrumentProvider(options); + var lcp = new LatencyContextProvider(lip); + + var tokenissuer = GetTokenIssuer(options); + var ct = tokenissuer.GetCheckpointToken("ca"); + var mt = tokenissuer.GetMeasureToken("ma"); + var tt = tokenissuer.GetTagToken("ta"); + + var lc = lcp.CreateContext(); + lc.AddCheckpoint(ct); + lc.RecordMeasure(mt, 5); + lc.AddMeasure(mt, 10); + lc.SetTag(tt, "tag"); + + ct = tokenissuer.GetCheckpointToken("ca1"); + mt = tokenissuer.GetMeasureToken("ma1"); + tt = tokenissuer.GetTagToken("ta1"); + + lc.AddCheckpoint(ct); + lc.RecordMeasure(mt, 5); + lc.AddMeasure(mt, 10); + lc.SetTag(tt, "tag"); + + Assert.True(lc.LatencyData.Checkpoints.Length == 1); + Assert.True(lc.LatencyData.Measures.Length == 1); + Assert.True(lc.LatencyData.Tags.Length == 1); + } + + [Fact] + public void Provider_ThrowOptions() + { + LatencyContextOptions options = new LatencyContextOptions + { + ThrowOnUnregisteredNames = true + }; + + var lip = GetLatencyInstrumentProvider(options); + var lcp = new LatencyContextProvider(lip); + + var tokenissuer = GetTokenIssuer(options); + var ct = tokenissuer.GetCheckpointToken("ca"); + var mt = tokenissuer.GetMeasureToken("ma"); + var tt = tokenissuer.GetTagToken("ta"); + + var lc = lcp.CreateContext(); + lc.AddCheckpoint(ct); + lc.RecordMeasure(mt, 5); + lc.AddMeasure(mt, 10); + lc.SetTag(tt, "tag"); + + Assert.Throws(() => tokenissuer.GetCheckpointToken("ca1")); + Assert.Throws(() => tokenissuer.GetMeasureToken("ma1")); + Assert.Throws(() => tokenissuer.GetTagToken("ta1")); + + Assert.True(lc.LatencyData.Checkpoints.Length == 1); + Assert.True(lc.LatencyData.Measures.Length == 1); + Assert.True(lc.LatencyData.Tags.Length == 1); + } + + private static ILatencyContextTokenIssuer GetTokenIssuer(LatencyContextOptions options) + { + var lco = new Mock>(); + lco.Setup(a => a.Value).Returns(options); + + var lip = GetLatencyInstrumentProvider(options); + + return new LatencyContextTokenIssuer(lip); + } + + private static LatencyInstrumentProvider GetLatencyInstrumentProvider(LatencyContextOptions options) + { + var lco = new Mock>(); + lco.Setup(a => a.Value).Returns(options); + var lcr = new LatencyContextRegistrySet(lco.Object, GetRegistrationOption()); + + return new LatencyInstrumentProvider(lcr); + } + + private static IOptions GetRegistrationOption() + { + return MockLatencyContextRegistrationOptions.GetLatencyContextRegistrationOptions( + new[] { "ca" }, new[] { "ma" }, new[] { "ta" }); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextRegistrySetTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextRegistrySetTest.cs new file mode 100644 index 0000000000..aadddbb43b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextRegistrySetTest.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Latency.Internal; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Latency.Test.Internal; + +public class LatencyContextRegistrySetTest +{ + [Fact] + public void ServiceCollection_Register_DefaultOption() + { + var lco = new Mock>(); + lco.Setup(a => a.Value).Returns(new LatencyContextOptions()); + + var lcrs = new LatencyContextRegistrySet(lco.Object); + Assert.NotNull(lcrs); + Assert.NotNull(lcrs.CheckpointNameRegistry); + Assert.NotNull(lcrs.TagNameRegistry); + Assert.NotNull(lcrs.MeasureNameRegistry); + Assert.True(lcrs.CheckpointNameRegistry.KeyCount == 0); + Assert.True(lcrs.MeasureNameRegistry.KeyCount == 0); + Assert.True(lcrs.TagNameRegistry.KeyCount == 0); + } + + [Fact] + public void Registry_Add_BasicTest() + { + var s = new[] { "a", "b", "c", "d" }; + var r = GetRegistry(s, s, s); + + CheckRegistration(r.CheckpointNameRegistry, "c", "e"); + CheckRegistration(r.MeasureNameRegistry, "d", "e"); + CheckRegistration(r.TagNameRegistry, "a", "e"); + } + + [Fact] + public void ServiceCollection_Register_InvalidValues() + { +#pragma warning disable CS8619 // Nullability of reference types in value doesn't match target type. + string[] n = new[] { "a", "b", null, "d" }; +#pragma warning restore CS8619 // Nullability of reference types in value doesn't match target type. + var e = Array.Empty(); + + Assert.Throws(() => GetRegistry(n, e, e)); + Assert.Throws(() => GetRegistry(e, n, e)); + Assert.Throws(() => GetRegistry(e, e, n)); + + n = new[] { " ", "b", "c" }; + Assert.Throws(() => GetRegistry(n, e, e)); + Assert.Throws(() => GetRegistry(e, n, e)); + Assert.Throws(() => GetRegistry(e, e, n)); + } + + [Fact] + public void ServiceCollection_Register_AddsToRegistry() + { + var checkpoints = new[] { "ca", "cb" }; + var measures = new[] { "ma", "mb" }; + var tags = new[] { "ta", "tb" }; + + var lcr = GetRegistry(checkpoints, measures, tags); + + Assert.True(lcr.CheckpointNameRegistry.IsRegistered("ca")); + Assert.True(lcr.CheckpointNameRegistry.KeyCount == 2); + Assert.True(lcr.MeasureNameRegistry.IsRegistered("ma")); + Assert.True(lcr.MeasureNameRegistry.KeyCount == 2); + Assert.True(lcr.TagNameRegistry.IsRegistered("ta")); + Assert.True(lcr.TagNameRegistry.KeyCount == 2); + } + + private static void CheckRegistration(Registry registry, string registered, string notRegsitered) + { + Assert.True(registry.IsRegistered(registered)); + Assert.False(registry.IsRegistered(notRegsitered)); + } + + private static LatencyContextRegistrySet GetRegistry(string[] checkpoints, string[] measures, string[] tags) + { + var lco = new Mock>(); + lco.Setup(a => a.Value).Returns(new LatencyContextOptions()); + + var o = MockLatencyContextRegistrationOptions.GetLatencyContextRegistrationOptions(checkpoints, measures, tags); + var r = new LatencyContextRegistrySet(lco.Object, o); + return r; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextTest.cs new file mode 100644 index 0000000000..365977069c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextTest.cs @@ -0,0 +1,330 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Extensions.Telemetry.Latency.Internal; +using Microsoft.Shared.Pools; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Latency.Test.Internal; + +public class LatencyContextTest +{ + private readonly string[] _checkpoints = new[] { "ca", "cb", "lc", "cd" }; + private readonly string[] _tags = new[] { "ta", "tb", "tc", "td" }; + private readonly string[] _measures = new[] { "ma", "mb", "mc", "md" }; + + [Fact] + public void Context_Dispose_Stops_Context() + { + var latencyContext = GetContext(); + Assert.True(latencyContext.IsRunning); + latencyContext.Dispose(); + Assert.False(latencyContext.IsRunning); + } + + [Fact] + public void Context_Dispose_InvokedMulitpleTimes() + { + var latencyContext = GetContext(); + Assert.NotNull(latencyContext); + Assert.False(latencyContext.IsDisposed); + latencyContext.Dispose(); + Assert.True(latencyContext.IsDisposed); +#pragma warning disable S3966 // Objects should not be disposed more than once + latencyContext.Dispose(); + latencyContext.Dispose(); +#pragma warning restore S3966 // Objects should not be disposed more than once + } + + [Fact] + public void Context_StopOnlyOnce() + { + using var latencyContext = GetContext(); + latencyContext.Freeze(); + Assert.False(latencyContext.IsRunning); + + // Subsequent adds become no-ops + latencyContext.Freeze(); + latencyContext.Freeze(); + Assert.False(latencyContext.IsRunning); + } + + [Fact] + public void Context_Dispose_ReturnsToPool() + { + var r = GetRegistry(); + var li = new LatencyInstrumentProvider(r); + var lcp = new LatencyContextPool(li); + var pool = new MockResetOnGet(lcp); + lcp.Pool = pool; + var latencyContext = new LatencyContext(lcp); + Assert.False(latencyContext.IsDisposed); + latencyContext.Dispose(); + Assert.True(latencyContext.IsDisposed); + Assert.True(pool.ReturnCalled); + } + + [Fact] + public void Latency_Context_Is_Not_Adding_Measures_When_Frozen() + { + const string TokenName = nameof(TokenName); + + using var scope = new ServiceCollection() + .AddLatencyContext() + .BuildServiceProvider() + .CreateScope(); + + var services = scope.ServiceProvider; + var context = services.GetRequiredService().CreateContext(); + var tokenIssuer = services.GetRequiredService(); + var measureToken = tokenIssuer.GetMeasureToken(TokenName); + + context.Freeze(); + context.AddMeasure(measureToken, 1); + context.RecordMeasure(measureToken, 1); + + var measures = context.LatencyData.Measures; + + Assert.IsAssignableFrom(context); + Assert.False(((LatencyContext)context).IsRunning); + Assert.Empty(measures.ToArray()); + } + + [Fact] + public void Latency_Context_Is_Adding_Measures_When_Not_Frozen() + { + const string TokenName = nameof(TokenName); + + using var scope = new ServiceCollection() + .AddLatencyContext() + .RegisterMeasureNames(TokenName) + .BuildServiceProvider() + .CreateScope(); + + var services = scope.ServiceProvider; + var context = services.GetRequiredService().CreateContext(); + var tokenIssuer = services.GetRequiredService(); + var measureToken = tokenIssuer.GetMeasureToken(TokenName); + + context.AddMeasure(measureToken, 1); + + var measures = context.LatencyData.Measures; + + Assert.IsAssignableFrom(context); + Assert.True(((LatencyContext)context).IsRunning); + Assert.Single(measures.ToArray()); + Assert.Equal(TokenName, measures[0].Name); + } + + [Fact] + public void Latency_Context_Is_Recording_Measures_When_Not_Frozen() + { + const string TokenName = nameof(TokenName); + + using var scope = new ServiceCollection() + .AddLatencyContext() + .RegisterMeasureNames(TokenName) + .BuildServiceProvider() + .CreateScope(); + + var services = scope.ServiceProvider; + var context = services.GetRequiredService().CreateContext(); + var tokenIssuer = services.GetRequiredService(); + var measureToken = tokenIssuer.GetMeasureToken(TokenName); + + context.RecordMeasure(measureToken, 1); + + var measures = context.LatencyData.Measures; + + Assert.IsAssignableFrom(context); + Assert.True(((LatencyContext)context).IsRunning); + Assert.Single(measures.ToArray()); + Assert.Equal(TokenName, measures[0].Name); + } + + [Fact] + public void Latency_Context_Is_Not_Adding_Values_To_Tags_When_Frozen() + { + const string TokenName = nameof(TokenName); + const string Tag = nameof(Tag); + + using var scope = new ServiceCollection() + .AddLatencyContext() + .RegisterTagNames(TokenName) + .RegisterCheckpointNames(TokenName) + .BuildServiceProvider() + .CreateScope(); + + var services = scope.ServiceProvider; + var context = services.GetRequiredService().CreateContext(); + var tokenIssuer = services.GetRequiredService(); + var tagToken = tokenIssuer.GetTagToken(TokenName); + + var tags2 = context.LatencyData.Tags; + + context.Freeze(); + context.SetTag(tagToken, Tag); + + var tags = context.LatencyData.Tags.ToArray(); + + Assert.IsAssignableFrom(context); + Assert.False(((LatencyContext)context).IsRunning); + Assert.Single(tags); + Assert.Empty(tags[0].Value); + } + + [Fact] + public void Latency_Context_Is_Adding_Values_To_Tags_Tags_When_Not_Frozen() + { + const string TokenName = nameof(TokenName); + const string Tag = nameof(Tag); + + using var scope = new ServiceCollection() + .AddLatencyContext() + .RegisterTagNames(TokenName) + .BuildServiceProvider() + .CreateScope(); + + var services = scope.ServiceProvider; + var context = services.GetRequiredService().CreateContext(); + var tokenIssuer = services.GetRequiredService(); + var tagToken = tokenIssuer.GetTagToken(TokenName); + + context.SetTag(tagToken, Tag); + + var tags = context.LatencyData.Tags; + + Assert.IsAssignableFrom(context); + Assert.True(((LatencyContext)context).IsRunning); + Assert.Single(tags.ToArray()); + Assert.Equal(Tag, tags[0].Value); + } + + [Fact] + public async Task Latency_Context_Is_Returning_Const_Duration_When_Frozen() + { + const string TokenName = nameof(TokenName); + const string Tag = nameof(Tag); + + using var scope = new ServiceCollection() + .AddLatencyContext() + .BuildServiceProvider() + .CreateScope(); + + var services = scope.ServiceProvider; + var context = services.GetRequiredService().CreateContext(); + + context.Freeze(); + var afterFreezeDuration = context.LatencyData.DurationTimestamp; + + await Task.Delay(1); + + var afterDelayDuration = context.LatencyData.DurationTimestamp; + + Assert.IsAssignableFrom(context); + Assert.False(((LatencyContext)context).IsRunning); + Assert.True(afterFreezeDuration.Equals(afterDelayDuration)); + } + + [Fact] + public void Latency_Context_Is_Not_Adding_Checkpoints_When_Frozen() + { + const string TokenName = nameof(TokenName); + + using var scope = new ServiceCollection() + .AddLatencyContext() + .BuildServiceProvider() + .CreateScope(); + + var services = scope.ServiceProvider; + var context = services.GetRequiredService().CreateContext(); + var tokenIssuer = services.GetRequiredService(); + var checkpointToken = tokenIssuer.GetCheckpointToken(TokenName); + + context.Freeze(); + context.AddCheckpoint(checkpointToken); + + var checkpoints = context.LatencyData.Checkpoints; + + Assert.IsAssignableFrom(context); + Assert.False(((LatencyContext)context).IsRunning); + Assert.Empty(checkpoints.ToArray()); + } + + [Fact] + public void Latency_Context_Is_Adding_Checkpoints_When_Not_Frozen() + { + const string TokenName = nameof(TokenName); + + using var scope = new ServiceCollection() + .AddLatencyContext() + .RegisterCheckpointNames(TokenName) + .BuildServiceProvider() + .CreateScope(); + + var services = scope.ServiceProvider; + var context = services.GetRequiredService().CreateContext(); + var tokenIssuer = services.GetRequiredService(); + var checkpointToken = tokenIssuer.GetCheckpointToken(TokenName); + + context.AddCheckpoint(checkpointToken); + + var checkpoints = context.LatencyData.Checkpoints; + + Assert.IsAssignableFrom(context); + Assert.True(((LatencyContext)context).IsRunning); + Assert.Single(checkpoints.ToArray()); + Assert.Equal(TokenName, checkpoints[0].Name); + } + + private LatencyContext GetContext() + { + var r = GetRegistry(); + var li = new LatencyInstrumentProvider(r); + var lcp = new LatencyContextPool(li); + return lcp.Pool.Get(); + } + + private LatencyContextRegistrySet GetRegistry() + { + var option = MockLatencyContextRegistrationOptions.GetLatencyContextRegistrationOptions( + _checkpoints, _measures, _tags); + var lco = new Mock>(); + lco.Setup(a => a.Value).Returns(new LatencyContextOptions { ThrowOnUnregisteredNames = false }); + + var r = new LatencyContextRegistrySet(lco.Object, option); + + return r; + } + + private class MockResetOnGet : ObjectPool + { + public bool ReturnCalled; + private readonly ObjectPool _objectPool; + + public MockResetOnGet(LatencyContextPool latencyContextPool) + { + var policy = new LatencyContextPool.LatencyContextPolicy(latencyContextPool); + _objectPool = PoolFactory.CreatePool(policy); + } + + public override LatencyContext Get() + { + var o = _objectPool.Get(); + _ = o.TryReset(); + return o; + } + + public override void Return(LatencyContext obj) + { + ReturnCalled = true; + _objectPool.Return(obj); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextTokenIssuerTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextTokenIssuerTest.cs new file mode 100644 index 0000000000..05eb5695bc --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/LatencyContextTokenIssuerTest.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Latency.Internal; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Latency.Test.Internal; + +public class LatencyContextTokenIssuerTest +{ + private readonly string[] _checkpoints = new[] { "ca", "cb", "lc", "cd" }; + private readonly string[] _tags = new[] { "ta", "tb", "tc", "td" }; + private readonly string[] _measures = new[] { "ma", "mb", "mc", "md" }; + + [Fact] + public void TokenIssuer_ValidNames() + { + var lcti = GetTokenIssuer(); + + // Valid names + var ct = lcti.GetCheckpointToken("cb"); + Assert.Equal("cb", ct.Name); + Assert.True(ct.Position > -1); + + var mt = lcti.GetMeasureToken("mc"); + Assert.Equal("mc", mt.Name); + Assert.True(mt.Position > -1); + + var tt = lcti.GetTagToken("ta"); + Assert.Equal("ta", tt.Name); + Assert.True(tt.Position > -1); + } + + [Fact] + public void TokenIssuer_InvalidNames() + { + var lcti = GetTokenIssuer(); + + // Invalid names + var ct = lcti.GetCheckpointToken("ta"); + Assert.Equal("ta", ct.Name); + Assert.True(ct.Position == -1); + + var mt = lcti.GetMeasureToken("cb"); + Assert.Equal("cb", mt.Name); + Assert.True(mt.Position == -1); + + var tt = lcti.GetTagToken("mc"); + Assert.Equal("mc", tt.Name); + Assert.True(tt.Position == -1); + } + + private LatencyContextTokenIssuer GetTokenIssuer() + { + var r = GetRegistry(); + var li = new LatencyInstrumentProvider(r); + return new LatencyContextTokenIssuer(li); + } + + private LatencyContextRegistrySet GetRegistry() + { + var option = MockLatencyContextRegistrationOptions.GetLatencyContextRegistrationOptions( + _checkpoints, _measures, _tags); + var lco = new Mock>(); + lco.Setup(a => a.Value).Returns(new LatencyContextOptions { ThrowOnUnregisteredNames = false }); + + var r = new LatencyContextRegistrySet(lco.Object, option); + + return r; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/MeasureTrackerTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/MeasureTrackerTest.cs new file mode 100644 index 0000000000..6eb05a7b75 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/MeasureTrackerTest.cs @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.Extensions.Telemetry.Latency.Internal; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Latency.Test.Internal; + +public class MeasureTrackerTest +{ + private static readonly Registry _measureNames = new(new[] { "a", "b", "c", "d" }, false); + + [Fact] + public void MeasureTracker_AddUnregisteredName() + { + MeasureTracker mt = new MeasureTracker(_measureNames); + mt.AddLong(mt.GetToken("e"), 10); + Assert.True(mt.Measures.Count == 0); + mt.SetLong(mt.GetToken("e"), 10); + Assert.True(mt.Measures.Count == 0); + } + + [Fact] + public void MeasureTracker_AddRegisteredNames() + { + MeasureTracker mt = new MeasureTracker(_measureNames); + string[] names = { "a", "b", "c" }; + int times = 3; + + for (int i = 0; i < names.Length; i++) + { + for (int j = 0; j < times; j++) + { + mt.AddLong(mt.GetToken(names[i]), i); + } + } + + var measures = mt.Measures.ToList(); + Assert.True(measures.Count == names.Length); + + // Verify measures have correct values. + for (int i = 0; i < names.Length; i++) + { + var m = measures.Where(m => m.Name == names[i]).ToList(); + Assert.True(m.Count == 1); + Assert.True(m[0].Name == names[i]); + Assert.True(m[0].Value == i * times); + } + } + + [Fact] + public void MeasureTracker_SetRegisteredNames() + { + MeasureTracker mt = new MeasureTracker(_measureNames); + string[] names = { "a", "b", "c" }; + int times = 3; + + for (int i = 0; i < names.Length; i++) + { + for (int j = 0; j < times; j++) + { + mt.SetLong(mt.GetToken(names[i]), i); + } + } + + var measures = mt.Measures.ToArray(); + Assert.True(measures.Length == names.Length); + + for (int i = 0; i < names.Length; i++) + { + var m = measures.Where(m => m.Name == names[i]).ToList(); + Assert.True(m.Count == 1); + Assert.True(m[0].Name == names[i]); + Assert.True(m[0].Value == i); + } + } + + [Fact] + public void MeasureTracker_Set_LasetSetWins() + { + MeasureTracker mt = new MeasureTracker(_measureNames); + mt.AddLong(mt.GetToken("a"), 5); + mt.SetLong(mt.GetToken("a"), 10); + var measures = mt.Measures.ToList(); + Assert.NotNull(measures); + Assert.True(measures.Count == 1); + Assert.True(measures[0].Name == "a"); + Assert.True(measures[0].Value == 10); + + mt.SetLong(mt.GetToken("a"), 41); + measures = mt.Measures.ToList(); + Assert.True(measures.Count == 1); + Assert.True(measures[0].Name == "a"); + Assert.True(measures[0].Value == 41); + } + + [Fact] + public void MeasureTracker_CheckReset() + { + MeasureTracker mt = new MeasureTracker(_measureNames); + mt.AddLong(mt.GetToken("a"), 5); + mt.AddLong(mt.GetToken("b"), 3); + mt.SetLong(mt.GetToken("c"), 10); + + Assert.True(mt.Measures.Count == 3); + _ = mt.TryReset(); + Assert.True(mt.Measures.Count == 0); + + mt.AddLong(mt.GetToken("b"), 6); + Assert.True(mt.Measures.Count == 1); + var measures = mt.Measures.ToList(); + Assert.True(measures[0].Name == "b"); + Assert.True(measures[0].Value == 6); + + _ = mt.TryReset(); + Assert.True(mt.Measures.Count == 0); + + mt.AddLong(mt.GetToken("b"), 2); + Assert.True(mt.Measures.Count == 1); + measures = mt.Measures.ToList(); + Assert.True(measures[0].Name == "b"); + Assert.True(measures[0].Value == 2); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/MockLatencyContextRegistrationOptions.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/MockLatencyContextRegistrationOptions.cs new file mode 100644 index 0000000000..8d0927470c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/MockLatencyContextRegistrationOptions.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Latency; +using Moq; + +namespace Microsoft.Extensions.Telemetry.Latency.Test; +internal static class MockLatencyContextRegistrationOptions +{ + public static IOptions GetLatencyContextRegistrationOptions( + string[] checkpoints, + string[] measures, + string[] tags) + { + var options = new LatencyContextRegistrationOptions + { + CheckpointNames = checkpoints, + MeasureNames = measures, + TagNames = tags + }; + + var lcro = new Mock>(); + lcro.Setup(a => a.Value).Returns(options); + + return lcro.Object; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/RegistryTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/RegistryTest.cs new file mode 100644 index 0000000000..dbb9b70248 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/RegistryTest.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Telemetry.Latency.Internal; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Latency.Test.Internal; + +public class RegistryTest +{ + [Fact] + public void Registry_NullSet() + { + Assert.Throws(() => new Registry(null!, true)); + Assert.Throws(() => new Registry(null!, false)); + } + + [Fact] + public void Registry_SetWithNullValue() + { + var s = new[] { "a", null }; + Assert.Throws(() => new Registry(s!, true)); + Assert.Throws(() => new Registry(s!, false)); + } + + [Fact] + public void Registry_EmptySet() + { + RegistryTest.TestWithEmptySet(true); + RegistryTest.TestWithEmptySet(false); + } + + private static void TestWithEmptySet(bool throwOnUnregistered) + { + var r = new Registry(Array.Empty(), throwOnUnregistered); + Assert.True(r.KeyCount == 0); + Assert.Throws(() => r.GetRegisteredKeyIndex(null!)); + } + + [Fact] + public void Registry_SameOrderForKeys() + { + var r1 = new Registry(new[] { "a", "b", "c", "d" }, true); + var ok1 = r1.OrderedKeys; + var r2 = new Registry(new[] { "a", "b", "c", "d" }, true); + var ok2 = r2.OrderedKeys; + for (int i = 0; i < ok1.Length; i++) + { + Assert.Equal(ok1[i], ok2[i]); + } + } + + [Fact] + public void Registry_NonEmptySet() + { + var r = new Registry(new[] { "a", "b", "c", "d" }, true); + var ok = r.OrderedKeys; + Assert.True(ok.Length == 4); + var o = r.GetRegisteredKeyIndex("c"); + + for (int i = 0; i < ok.Length; i++) + { + if (ok[i] == "c") + { + Assert.True(o == i); + } + } + } + + [Fact] + public void Registry_ThrowMode_ThrowsOnUnregisteredKey() + { + var r = new Registry(new[] { "a", "b", "c", "d" }, true); + var ok = r.OrderedKeys; + Assert.True(ok.Length == 4); + r.GetRegisteredKeyIndex("a"); + Assert.Throws(() => r.GetRegisteredKeyIndex("e")); + } + + [Fact] + public void Registry_NonThrowMode_DoesNotThrow() + { + var r = new Registry(new[] { "a", "b", "c", "d" }, false); + var ok = r.OrderedKeys; + Assert.True(ok.Length == 4); + r.GetRegisteredKeyIndex("a"); + Assert.True(r.GetRegisteredKeyIndex("e") == -1); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/TagCollectionTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/TagCollectionTest.cs new file mode 100644 index 0000000000..e6713f752c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/Internal/TagCollectionTest.cs @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Extensions.Telemetry.Latency.Internal; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Latency.Test.Internal; + +public class TagCollectionTest +{ + private static readonly Registry _tagNames = new(new[] { "a", "b", "c", "d" }, false); + + [Fact] + public void TagCollection_AddUnregisteredName() + { + TagCollection tc = new TagCollection(_tagNames); + tc.Set(tc.GetToken("e"), "val"); + Assert.True(tc.Tags.Count == 4); + tc.Tags.ToList().ForEach(t => Assert.True(t.Name != "e")); + } + + [Fact] + public void TagCollection_SetRegisteredNames() + { + TagCollection tc = new TagCollection(_tagNames); + Dictionary namesNumTimes = new Dictionary + { + { "a", 0 }, + { "b", 0 }, + { "c", 0 } + }; + + foreach (var s in namesNumTimes.Keys) + { + tc.Set(tc.GetToken(s), "testVal"); + } + + var t = tc.Tags.ToList(); + Assert.True(t.Count == _tagNames.KeyCount); + + for (int i = 0; i < t.Count; i++) + { + var tagName = t[i].Name; + Assert.True(_tagNames.GetRegisteredKeyIndex(tagName) > -1); + if (namesNumTimes.ContainsKey(tagName)) + { + // If tag was set, there should be be only instance of it. + Assert.True(namesNumTimes[tagName] == 0); + Assert.True(t[i].Value == "testVal"); + namesNumTimes[tagName]++; + } + else + { + // If tag was not set, value should be empty string. + Assert.True(t[i].Value == string.Empty); + } + } + } + + [Fact] + public void TagCollection_Set_LastSetSetWins() + { + TagCollection tc = new TagCollection(_tagNames); + tc.Set(tc.GetToken("a"), "first"); + tc.Set(tc.GetToken("a"), "second"); + Assert.True(tc.Tags.Count == _tagNames.KeyCount); + var tagList = tc.Tags.ToList(); + + // Verify only tag matches and has the last value set on it + var atag = tagList.Where(t => t.Name == "a").ToList(); + Assert.True(atag.Count == 1); + Assert.True(atag[0].Name == "a"); + Assert.True(atag[0].Value == "second"); + + // All other tags must have empty string values. + var notaTag = tagList.Where(t => t.Name != "a").ToList(); + Assert.True(notaTag.Count == tagList.Count - 1); + var notaTagEmptyValues = notaTag.Where(t => t.Value == string.Empty).ToList(); + Assert.True(notaTagEmptyValues.Count == notaTag.Count); + } + + [Fact] + public void TagCollection_CheckReset() + { + TagCollection tc = new TagCollection(_tagNames); + tc.Set(tc.GetToken("a"), "first"); + tc.Set(tc.GetToken("b"), "second"); + + int numNonEmpty = GetNumberOfNonEmptyTags(tc.Tags); + Assert.True(numNonEmpty == 2); + + _ = tc.TryReset(); + numNonEmpty = GetNumberOfNonEmptyTags(tc.Tags); + Assert.True(numNonEmpty == 0); + } + + private static int GetNumberOfNonEmptyTags(ReadOnlySpan tags) + { + int numNonEmpty = 0; + for (int i = 0; i < tags.Length; i++) + { + if (tags[i].Value != string.Empty) + { + numNonEmpty++; + } + } + + return numNonEmpty; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/LatencyContextExtensionTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/LatencyContextExtensionTest.cs new file mode 100644 index 0000000000..95c25671a4 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Latency/LatencyContextExtensionTest.cs @@ -0,0 +1,131 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Extensions.Telemetry.Latency.Internal; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Latency.Test; + +public class LatencyContextExtensionTest +{ + [Fact] + public void ServiceCollection_Null() + { + Assert.Throws(() => + LatencyContextExtensions.AddLatencyContext(null!)); + } + + [Fact] + public void AddContext_BasicAddLatencyContext() + { + using var serviceProvider = new ServiceCollection() + .AddLatencyContext() + .BuildServiceProvider(); + + var latencyContextProvider = serviceProvider.GetRequiredService(); + Assert.NotNull(latencyContextProvider); + Assert.IsAssignableFrom(latencyContextProvider); + + var latencyContextTokenIssuer = serviceProvider.GetRequiredService(); + Assert.NotNull(latencyContextTokenIssuer); + Assert.IsAssignableFrom(latencyContextTokenIssuer); + } + + [Fact] + public void ServiceCollection_GivenScopes_ReturnsDifferentInstanceForEachScope() + { + using var serviceProvider = new ServiceCollection() + .AddLatencyContext() + .BuildServiceProvider(); + + var scope1 = serviceProvider.CreateScope(); + var scope2 = serviceProvider.CreateScope(); + + // Get same instance within single scope. + Assert.Equal(scope1.ServiceProvider.GetRequiredService(), + scope1.ServiceProvider.GetRequiredService()); + Assert.Equal(scope1.ServiceProvider.GetRequiredService(), + scope1.ServiceProvider.GetRequiredService()); + + // Get same instance between different scopes. + Assert.Equal(scope1.ServiceProvider.GetRequiredService(), + scope2.ServiceProvider.GetRequiredService()); + Assert.Equal(scope1.ServiceProvider.GetRequiredService(), + scope1.ServiceProvider.GetRequiredService()); + + scope1.Dispose(); + scope2.Dispose(); + } + + [Fact] + public void AddContext_InvokesConfig() + { + var invoked = false; + using var serviceProvider = new ServiceCollection() + .AddLatencyContext(a => { invoked = true; }) + .BuildServiceProvider(); + + var latencyContextProvider = serviceProvider.GetRequiredService(); + Assert.NotNull(latencyContextProvider); + Assert.IsAssignableFrom(latencyContextProvider); + + var latencyContextTokenIssuer = serviceProvider.GetRequiredService(); + Assert.NotNull(latencyContextTokenIssuer); + Assert.IsAssignableFrom(latencyContextTokenIssuer); + + Assert.True(invoked); + } + + [Fact] + public void AddContext_BindsToConfigSection() + { + LatencyContextOptions expectedOptions = new() + { + ThrowOnUnregisteredNames = false + }; + + var config = GetConfigSection(expectedOptions); + + using var provider = new ServiceCollection() + .AddLatencyContext(config) + .BuildServiceProvider(); + var actualOptions = provider.GetRequiredService>(); + + Assert.True(actualOptions.Value.ThrowOnUnregisteredNames == expectedOptions.ThrowOnUnregisteredNames); + } + + [Fact] + public void Options_BasicTest() + { + var l = new LatencyContextOptions + { + ThrowOnUnregisteredNames = true + }; + Assert.True(l.ThrowOnUnregisteredNames); + + l.ThrowOnUnregisteredNames = false; + Assert.False(l.ThrowOnUnregisteredNames); + + // Check for default values + var o = new LatencyContextOptions(); + Assert.False(o.ThrowOnUnregisteredNames); + } + + private static IConfigurationSection GetConfigSection(LatencyContextOptions options) + { + return new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { $"{nameof(LatencyContextOptions)}:{nameof(options.ThrowOnUnregisteredNames)}", options.ThrowOnUnregisteredNames.ToString(CultureInfo.InvariantCulture) } + }) + .Build() + .GetSection($"{nameof(LatencyContextOptions)}"); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/AnotherEnricher.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/AnotherEnricher.cs new file mode 100644 index 0000000000..bd60fa349b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/AnotherEnricher.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Telemetry.Logging.Test.Internals; + +internal class AnotherEnricher : ILogEnricher +{ + public void Enrich(IEnrichmentPropertyBag enrichmentBag) + { + enrichmentBag.Add("another's key", "another's value"); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/EmptyEnricher.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/EmptyEnricher.cs new file mode 100644 index 0000000000..eadd85e894 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/EmptyEnricher.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Telemetry.Logging.Test.Internals; + +internal class EmptyEnricher : ILogEnricher +{ + public void Enrich(IEnrichmentPropertyBag enrichmentBag) + { + // intentionally left empty + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/EmptyStringEnricher.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/EmptyStringEnricher.cs new file mode 100644 index 0000000000..32dd92e5ad --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/EmptyStringEnricher.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Telemetry.Logging.Test.Internals; + +internal class EmptyStringEnricher : ILogEnricher +{ + public void Enrich(IEnrichmentPropertyBag enrichmentBag) + { + enrichmentBag.Add("key1", string.Empty); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/FlexibleEnricher.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/FlexibleEnricher.cs new file mode 100644 index 0000000000..e24c522f89 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/FlexibleEnricher.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Telemetry.Logging.Test.Internals; + +internal class FlexibleEnricher : ILogEnricher +{ + private readonly string? _key; + private readonly string? _value; + + public FlexibleEnricher(string? key, string? value) + { + _key = key; + _value = value; + } + + public void Enrich(IEnrichmentPropertyBag enrichmentBag) + { + enrichmentBag.Add(_key!, _value!); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/Helpers.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/Helpers.cs new file mode 100644 index 0000000000..ead9e9c893 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/Helpers.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Logs; + +namespace Microsoft.Extensions.Telemetry.Logging.Test.Internals; + +internal static class Helpers +{ + public static bool TryGetStackTrace(this IReadOnlyCollection> stateValues, out string? stackTrace) + { + if (stateValues.Count < 1) + { + stackTrace = null; + return false; + } + + foreach (var entry in stateValues) + { + if (entry.Key == "stackTrace") + { + stackTrace = entry.Value.ToString(); + return true; + } + } + + stackTrace = null; + return false; + } + + public static bool CompareStateValues(IReadOnlyCollection> stateValues, Dictionary dictExpected) + { + if (stateValues.Count != dictExpected.Count) + { + return false; + } + + foreach (var entry in stateValues) + { + if (dictExpected.TryGetValue(entry.Key, out var value)) + { + if (entry.Value is null || value is null) + { + if (entry.Value is null && value is null) + { + return true; + } + + return false; + } + + if (entry.Value!.ToString() != value!.ToString()) + { + return false; + } + } + else + { + return false; + } + } + + return true; + } + + public static ILogger CreateLogger(Action configure, BaseExporter exporter) + { + var hostBuilder = FakeHost.CreateBuilder(options => options.FakeLogging = false) + .ConfigureLogging(builder => + { + configure.Invoke(builder); + _ = builder.AddOpenTelemetryLogging().AddProcessor(new SimpleLogRecordExportProcessor(exporter)); + }); + + var host = hostBuilder.Build(); + var logger = host.Services.GetRequiredService>(); + + return logger; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/PrimitiveValuesEnricher.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/PrimitiveValuesEnricher.cs new file mode 100644 index 0000000000..c72f12a2ab --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/PrimitiveValuesEnricher.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Telemetry.Logging.Test.Internals; + +internal class PrimitiveValuesEnricher : ILogEnricher +{ + private readonly string? _key; + private readonly int _value; + + public PrimitiveValuesEnricher(string? key, int value) + { + _key = key; + _value = value; + } + + public void Enrich(IEnrichmentPropertyBag enrichmentBag) + { + enrichmentBag.Add(_key!, _value!); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/SimpleEnricher.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/SimpleEnricher.cs new file mode 100644 index 0000000000..63d4f7b08e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/SimpleEnricher.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Telemetry.Logging.Test.Internals; + +internal class SimpleEnricher : ILogEnricher +{ + public void Enrich(IEnrichmentPropertyBag enrichmentBag) + { + enrichmentBag.Add("enriched test attribute key", "enriched test attribute value"); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/TestExceptionThrower.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/TestExceptionThrower.cs new file mode 100644 index 0000000000..592d095dcc --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/TestExceptionThrower.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Telemetry.Logging.Test.Internals; + +#pragma warning disable CA1031 // Do not catch general exception types + +internal static class TestExceptionThrower +{ + public static void ThrowExceptionWithoutInnerException() + { + new A().InvokingMethodOnClassA(); + } + + public static void ThrowExceptionWithInnerException() + { + new C().InvokingMethodOnClassC(); + } + + public static void ThrowExceptionWithMultipleLevelInnerException() + { + try + { + try + { + new C().InvokingMethodOnClassC(); + } + catch (Exception ex) + { + throw new ArgumentException("2nd level exception", innerException: ex); + } + } + catch (Exception ex) + { + throw new ArgumentException("3rd level exception", innerException: ex); + } + } + + public static void ThrowExceptionWithMultipleLevelLargeStack() + { + Exception innerException; + try + { + try + { + new C().InvokingMethodOnClassC(); + } + catch (Exception ex) + { + innerException = ex; + new F().InvokingMethodOnClassF(1); + } + } + catch (Exception ex) + { + throw new ArgumentException("top level exception", innerException: ex); + } + } + + public static void ThrowExceptionWithBigExceptionStack() + { + new F().InvokingMethodOnClassF(1); + } + + internal sealed class A + { + private readonly B _obj = new(); + + public void InvokingMethodOnClassA() + { + _obj.InvokingMethodOnClassB(); + } + } + + internal sealed class B + { + private readonly E _obj = new(); + + public void InvokingMethodOnClassB() + { + _obj.InvokingMethodOnClassE(); + } + } + + internal sealed class C + { + private readonly D _obj = new(); + + public void InvokingMethodOnClassC() + { + try + { + _obj.InvokingMethodOnClassD(); + } + catch (Exception ex) + { + throw new AggregateException("Exception caught in Class C", innerException: ex); + } + } + } + + internal sealed class D + { + private readonly E _obj = new(); + + public void InvokingMethodOnClassD() + { + _obj.InvokingMethodOnClassE(); + } + } + + internal sealed class E + { + public void InvokingMethodOnClassE() + { + throw new NotSupportedException(); + } + } + + internal sealed class F + { + private static readonly G _g = new(); + private readonly int _maxCallCount = 1000; + + public void InvokingMethodOnClassF(int callCount) + { + if (callCount > _maxCallCount) + { + throw new ArgumentException("call count exceeded max call count", nameof(callCount)); + } + + _g.InvokingMethodOnClassG(callCount + 1); + } + } + + internal sealed class G + { + private static readonly F _f = new(); + +#pragma warning disable CA1822 // Mark members as static + public void InvokingMethodOnClassG(int callCount) +#pragma warning restore CA1822 // Mark members as static + { + _f.InvokingMethodOnClassF(callCount + 1); + } + } +#pragma warning restore CA1031 // Do not catch general exception types +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/TestExporter.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/TestExporter.cs new file mode 100644 index 0000000000..582bc0a51a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/TestExporter.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using OpenTelemetry; +using OpenTelemetry.Logs; + +namespace Microsoft.Extensions.Telemetry.Logging.Test.Internals; + +internal class TestExporter : BaseExporter +{ + internal LogRecord? FirstLogRecord { get; set; } + internal KeyValuePair? FirstScope { get; set; } + internal List>? FirstState { get; set; } + + public override ExportResult Export(in Batch batch) + { + foreach (var logRecord in batch) + { + FirstLogRecord = logRecord; + FirstState = logRecord.StateValues is null ? null : new(logRecord.StateValues); + logRecord.ForEachScope(ProcessScope, this); + } + + return ExportResult.Success; + } + + private static void ProcessScope(LogRecordScope scope, TestExporter exporter) + { + using var enumerator = scope.GetEnumerator(); + enumerator.MoveNext(); + exporter.FirstScope = enumerator.Current; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/TestProcessor.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/TestProcessor.cs new file mode 100644 index 0000000000..c6a570010e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Internals/TestProcessor.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using OpenTelemetry; +using OpenTelemetry.Logs; + +namespace Microsoft.Extensions.Telemetry.Logging.Test.Internals; + +internal class TestProcessor : BaseProcessor +{ + internal int DisposeCalledTimes; + + protected override void Dispose(bool disposing) + { + DisposeCalledTimes += 1; + base.Dispose(disposing); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Log/LoggingOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Log/LoggingOptionsTests.cs new file mode 100644 index 0000000000..81d3195adf --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/Log/LoggingOptionsTests.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Logging.Test.Log; + +public class LoggingOptionsTests +{ + private readonly LoggingOptions _sut = new(); + + [Fact] + public void CanSetAndGetIncludeScopes() + { + const bool TestValue = false; + _sut.IncludeScopes = TestValue; + Assert.Equal(TestValue, _sut.IncludeScopes); + } + + [Fact] + public void CanSetAndGetUseFormattedMessage() + { + const bool TestValue = false; + _sut.UseFormattedMessage = TestValue; + Assert.Equal(TestValue, _sut.UseFormattedMessage); + } + + [Fact] + public void CanSetAndGetStackTraceLimit() + { + const int TestValue = 4000; + _sut.MaxStackTraceLength = TestValue; + Assert.Equal(TestValue, _sut.MaxStackTraceLength); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/LogEnrichmentTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/LogEnrichmentTests.cs new file mode 100644 index 0000000000..434bdaa0a7 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/LogEnrichmentTests.cs @@ -0,0 +1,254 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Extensions.Telemetry.Logging.Test.Internals; +using OpenTelemetry; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Logging.Test; + +public sealed class LogEnrichmentTests +{ + [Fact] + public void LogWithEmptyEnricher() + { + // Arrange + using var exporter = new TestExporter(); + var logMessage = "This is testing {user}"; + + var logger = Helpers.CreateLogger(builder => + builder.Services.AddLogEnricher(), exporter); + var dictExpected = new Dictionary + { + { "{OriginalFormat}", logMessage }, + { "user", "testUser" } + }; + + // Act + logger.LogError(logMessage, "testUser"); + + // Assert + Assert.True(Helpers.CompareStateValues(exporter.FirstState!, dictExpected)); + } + + [Fact] + public void LogWithSingleEnricher() + { + // Arrange + using var exporter = new TestExporter(); + var logger = Helpers.CreateLogger(b => + b.Services.AddLogEnricher(), exporter); + var logMessage = "This is testing {user}"; + var dictExpected = new Dictionary + { + { "{OriginalFormat}", logMessage }, + { "user", "testUser" }, + { "enriched test attribute key", "enriched test attribute value" }, + }; + + // Act + logger.LogError(logMessage, "testUser"); + + // Assert + Assert.True(Helpers.CompareStateValues(exporter.FirstState!, dictExpected)); + } + + [Fact] + public void LogWithEmptyStringEnricher() + { + // Arrange + using var exporter = new TestExporter(); + var logger = Helpers.CreateLogger(b => + b.Services.AddLogEnricher(), exporter); + var logMessage = "This is testing {user}"; + var dictExpected = new Dictionary + { + { "{OriginalFormat}", logMessage }, + { "user", "testUser" }, + { "key1", string.Empty } + }; + + // Act + logger.LogError(logMessage, "testUser"); + + // Assert + Assert.True(Helpers.CompareStateValues(exporter.FirstState!, dictExpected)); + } + + [Fact] + public void LogWithManyEnrichersCreatedAtSamePoint() + { + // Arrange + using var exporter = new TestExporter(); + + var hostBuilder = FakeHost.CreateBuilder(options => options.FakeLogging = false) + .ConfigureLogging(builder => + { + _ = builder.Services.AddLogEnricher(); + _ = builder + .AddOpenTelemetryLogging() + .AddProcessor(new SimpleLogRecordExportProcessor(exporter)) + .Services.AddLogEnricher(); + }); + + using var host = hostBuilder.Build(); + var logger = host.Services.GetRequiredService>(); + + var logMessage = "This is testing {user}"; + var dictExpected = new Dictionary + { + { "{OriginalFormat}", logMessage }, + { "user", "testUser" }, + { "enriched test attribute key", "enriched test attribute value" }, + { "another's key", "another's value" } + }; + + // Act + logger.LogError(logMessage, "testUser"); + + // Assert + Assert.True(Helpers.CompareStateValues(exporter.FirstState!, dictExpected)); + } + + [Fact] + public void LogWithManyEnrichersCreatedAtDifferentPoints() + { + // Arrange + using var exporter = new TestExporter(); + + var hostBuilder = FakeHost.CreateBuilder(options => options.FakeLogging = false) + .ConfigureLogging(builder => + { + _ = builder.Services.AddLogEnricher(); + _ = builder + .AddOpenTelemetryLogging() + .AddProcessor(new SimpleLogRecordExportProcessor(exporter)) + .Services.AddLogEnricher(); + }) + .ConfigureLogging(builder => builder.Services + .AddLogEnricher(new FlexibleEnricher("key1", "value")) + .AddLogEnricher(new PrimitiveValuesEnricher("key2", 1))); + + using var host = hostBuilder.Build(); + var logger = host.Services.GetRequiredService>(); + + var logMessage = "This is testing {user}"; + var dictExpected = new Dictionary + { + { "{OriginalFormat}", logMessage }, + { "user", "testUser" }, + { "enriched test attribute key", "enriched test attribute value" }, + { "another's key", "another's value" }, + { "key1", "value" }, + { "key2", 1 } + }; + + // Act + logger.LogError(logMessage, "testUser"); + + // Assert + Assert.True(Helpers.CompareStateValues(exporter.FirstState!, dictExpected)); + } + + private class CustomState + { + public string? Property { get; set; } + } + + [Fact] + public void LogWithNullState() + { + // Arrange + using var exporter = new TestExporter(); + var logger = Helpers.CreateLogger(b => + b.Services.AddLogEnricher(), exporter); + var dictExpected = new Dictionary + { + { "enriched test attribute key", "enriched test attribute value" } + }; + CustomState? state = null; + + // Act + logger.Log(LogLevel.Information, 1, state, null, (_, _) => " test formatter "); + + // Assert + Assert.True(Helpers.CompareStateValues(exporter.FirstState!, dictExpected)); + } + + [Theory] + [InlineData(null, "")] + [InlineData(null, null)] + [InlineData("", "test")] + [InlineData("", null)] + public void LogWithInvalidValues_ThrowsException(string? key, string? value) + { + // Arrange + using var exporter = new TestExporter(); + var logMessage = "This is testing {user}"; + var enricher = new FlexibleEnricher(key, value); + var enricher2 = new PrimitiveValuesEnricher(key, 1); + var logger = Helpers.CreateLogger(b => + { + b.Services.AddLogEnricher(enricher); + b.Services.AddLogEnricher(enricher2); + }, exporter); + + // Assert + Assert.Throws(() => logger.Log(LogLevel.Information, logMessage)); + } + + [Fact] + public void LogWithCustomState() + { + // Arrange + using var exporter = new TestExporter(); + var logger = Helpers.CreateLogger(b => + b.Services.AddLogEnricher(), exporter); + var state = new CustomState { Property = "custom state property" }; + var dictExpected = new Dictionary + { + { "enriched test attribute key", "enriched test attribute value" }, + { "{OriginalFormat}", state } + }; + + // Act + logger.Log(LogLevel.Information, 2, state, null, (_, _) => " test formatter "); + + // Assert + Assert.True(Helpers.CompareStateValues(exporter.FirstState!, dictExpected)); + } + + [Fact] + public void LogWithMultipleEnrichers() + { + // Arrange + using var exporter = new TestExporter(); + + var logger = Helpers.CreateLogger(b => + { + b.Services.AddLogEnricher(); + b.Services.AddLogEnricher(); + }, exporter); + var logMessage = "This is testing {user}"; + var dictExpected = new Dictionary + { + { "{OriginalFormat}", logMessage }, + { "user", "testUser" }, + { "enriched test attribute key", "enriched test attribute value" }, + { "another's key", "another's value" } + }; + + // Act + logger.LogError(logMessage, "testUser"); + + // Assert + Assert.True(Helpers.CompareStateValues(exporter.FirstState!, dictExpected)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/LoggerTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/LoggerTests.cs new file mode 100644 index 0000000000..f827cc1107 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Logging/LoggerTests.cs @@ -0,0 +1,774 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Extensions.Telemetry.Logging; +using Microsoft.Extensions.Telemetry.Logging.Test.Internals; +using OpenTelemetry; +using OpenTelemetry.Logs; +using Xunit; +using IOptions = Microsoft.Extensions.Options.Options; + +namespace Microsoft.Extensions.Telemetry.Logging.Test; + +/// +/// Test class for R9 Logger. +/// +public sealed class LoggerTests +{ + private static LoggerProvider CreateLoggerProvider( + IOptions? loggingOptions = null, + IEnumerable? enrichers = null, + IEnumerable>? processors = null) => new( + loggingOptions ?? IOptions.Create(new LoggingOptions()), + enrichers ?? Enumerable.Empty(), + processors ?? Enumerable.Empty>()); + + [Fact] + public void CreateLoggerWithNullConfigurationActionThrows() + { + Action? nullAction = null; + Assert.Throws(() => LoggerFactory.Create(builder => builder.AddOpenTelemetryLogging(nullAction!))); + } + + [Fact] + public void CreateLoggerWithNullConfigurationSectionThrows() + { + IConfigurationSection? nullSection = null; + Assert.Throws(() => LoggerFactory.Create(builder => builder.AddOpenTelemetryLogging(nullSection!))); + } + + [Fact] + public void CreateLoggerWithNullProcessor() + { + Assert.Throws(() => LoggerFactory.Create(builder => + builder.AddOpenTelemetryLogging().AddProcessor(null!))); + } + + [Fact] + public void CreateLoggerWithSingleProcessor() + { + TestExporter exporter = new TestExporter(); + using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetryLogging().AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + + const string LogMessage = "This is testing {user}"; + ILogger logger = loggerFactory.CreateLogger("R9"); + logger.LogError(LogMessage, "testUser"); + + var dictExpected = new Dictionary + { + { "{OriginalFormat}", LogMessage }, + { "user", "testUser" } + }; + + Assert.True(Helpers.CompareStateValues(exporter.FirstState!, dictExpected)); + exporter.Dispose(); + + // disposing the object again should not cause any issues + exporter.Dispose(); + } + + [Fact] + public void CreateLoggerEnablesOpenTelemetrySDK() + { + using var loggerFactory = LoggerFactory.Create(builder => + builder.AddOpenTelemetryLogging()); + + // testing side effects because it is not possible to test OpenTelemetry .NET SDK class normally. + Assert.True(Activity.ForceDefaultIdFormat); + } + + [Fact] + public void CreateLoggerWithErrorLevelLogging() + { + using var exporter = new TestExporter(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .SetMinimumLevel(LogLevel.Error) + .AddOpenTelemetryLogging() + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + + var logMessage = "This is testing {user}"; + ILogger logger = loggerFactory.CreateLogger("R9"); + + logger.LogInformation(logMessage, "testUser"); + Assert.Null(exporter.FirstLogRecord); + + logger.LogError(logMessage, "testUser"); + var dictExpected = new Dictionary + { + { "{OriginalFormat}", logMessage }, + { "user", "testUser" } + }; + + Assert.True(Helpers.CompareStateValues(exporter.FirstState!, dictExpected)); + } + + [Fact] + public void CreateLoggerWithLoggingLevelNone() + { + using var exporter = new TestExporter(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .SetMinimumLevel(LogLevel.None) + .AddOpenTelemetryLogging() + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + + var logMessage = "This is testing {user}"; + ILogger logger = loggerFactory.CreateLogger("R9"); + + logger.LogDebug(logMessage, "testUser"); + Assert.Null(exporter.FirstLogRecord); + + logger.LogInformation(logMessage, "testUser"); + Assert.Null(exporter.FirstLogRecord); + + logger.LogWarning(logMessage, "testUser"); + Assert.Null(exporter.FirstLogRecord); + + logger.LogError(logMessage, "testUser"); + Assert.Null(exporter.FirstLogRecord); + } + + [Fact] + public void CreateLoggerWithMultipleProcessor() + { + using var exporter = new TestExporter(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging() + .AddProcessor(new TestProcessor()) + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + + var logMessage = "This is testing {user}"; + ILogger logger = loggerFactory.CreateLogger("R9"); + logger.LogError(logMessage, "testUser"); + + var dictExpected = new Dictionary + { + { "{OriginalFormat}", logMessage }, + { "user", "testUser" } + }; + + Assert.True(Helpers.CompareStateValues(exporter.FirstState!, dictExpected)); + } + + [Fact] + public void CreateLoggerWithScopes() + { + using var exporter = new TestExporter(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging(options => options.IncludeScopes = true) + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + + var logMessage = "This is testing {user}"; + var scopeName = "Adding Outer Scope"; + ILogger logger = loggerFactory.CreateLogger("R9"); + var dictExpected = new Dictionary + { + { "{OriginalFormat}", logMessage }, + { "user", "testUser" }, + }; + + using (logger.BeginScope("{ScopeMessage}", scopeName)) + { + logger.LogError(logMessage, "testUser"); + } + + Assert.True(Helpers.CompareStateValues(exporter.FirstState!, dictExpected)); + Assert.Equal(scopeName, exporter.FirstScope!.Value.Value); + } + + [Fact] + public void LogDifferentTState() + { + using var exporter = new TestExporter(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging() + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + + ILogger logger = loggerFactory.CreateLogger("R9"); + + logger.Log(LogLevel.Error, default, null!, null, (_, _) => string.Empty); + Assert.Empty(exporter.FirstState!); + + logger.Log(LogLevel.Error, default, "Hello", null, (_, _) => string.Empty); + Assert.Equal("Hello", exporter.FirstState![0].Value); + Assert.Equal("{OriginalFormat}", exporter.FirstState![0].Key); + + logger.Log(LogLevel.Error, default, new[] { new KeyValuePair("Hello", "World") }, null, (_, _) => string.Empty); + Assert.Equal("Hello", exporter.FirstState[0].Key); + Assert.Equal("World", exporter.FirstState[0].Value); + + logger.Log(LogLevel.Error, default, new[] { new KeyValuePair("Hello", "World") }.Take(1), null, (_, _) => string.Empty); + Assert.Equal("Hello", exporter.FirstState[0].Key); + Assert.Equal("World", exporter.FirstState[0].Value); + + logger.Log(LogLevel.Error, default, 50, null, (_, _) => string.Empty); + Assert.Equal(50, exporter.FirstState![0].Value); + Assert.Equal("{OriginalFormat}", exporter.FirstState![0].Key); + + var helper = LogMethodHelper.GetHelper(); + helper.Add("Hello", "World"); + logger.Log(LogLevel.Error, default, helper, null, (_, _) => string.Empty); + Assert.Equal("Hello", exporter.FirstState[0].Key); + Assert.Equal("World", exporter.FirstState[0].Value); + } + + [Fact] + public void LogException_IncludeStackTrace_Disabled() + { + using var exporter = new TestExporter(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging() + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + + ILogger logger = loggerFactory.CreateLogger("R9"); + logger.LogError(new NotImplementedException(), "Method is not implemented"); + + Assert.False(exporter.FirstState!.TryGetStackTrace(out string? stackTrace)); + } + + [Fact] + public void LogException_WithoutInnerStack_IncludeStackTrace_Enabled() + { + using var exporter = new TestExporter(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging(options => options.IncludeStackTrace = true) + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + + ILogger logger = loggerFactory.CreateLogger("R9"); + + LoggerTests.ThrowExceptionAndLogError(() => TestExceptionThrower.ThrowExceptionWithoutInnerException(), logger); + + bool stackTraceReturned = exporter.FirstState!.TryGetStackTrace(out string? stackTrace); + Assert.True(stackTraceReturned); + Assert.NotEmpty(stackTrace!); + Assert.DoesNotContain("InnerException type:", stackTrace); + } + + [Fact] + public void LogException_WithInnerStack_IncludeStackTrace_Enabled() + { + using var exporter = new TestExporter(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging(options => options.IncludeStackTrace = true) + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + + ILogger logger = loggerFactory.CreateLogger("R9"); + + LoggerTests.ThrowExceptionAndLogError(() => TestExceptionThrower.ThrowExceptionWithInnerException(), logger); + + bool stackTraceReturned = exporter.FirstState!.TryGetStackTrace(out string? stackTrace); + Assert.NotEmpty(stackTrace!); + Assert.Contains("InnerException type:", stackTrace); + } + + [Fact] + public void LogException_WithInnerStack_EmptyTopException_IncludeStackTrace_Enabled() + { + using var exporter = new TestExporter(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging(options => options.IncludeStackTrace = true) + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + + ILogger logger = loggerFactory.CreateLogger("R9"); + +#pragma warning disable CA1031 // Do not catch general exception types + try + { + TestExceptionThrower.ThrowExceptionWithoutInnerException(); + } + catch (Exception ex) + { + logger.LogError(new NotSupportedException("empty stack trace", innerException: ex), "test exception"); + } +#pragma warning restore CA1031 // Do not catch general exception types + + bool stackTraceReturned = exporter.FirstState!.TryGetStackTrace(out string? stackTrace); + Assert.StartsWith($"{Environment.NewLine}InnerException type:System.NotSupportedException message:Specified method is not supported", stackTrace); + } + + [Fact] + public void LogException_WithMultipleLevelInnerStack_IncludeStackTrace_Enabled() + { + using var exporter = new TestExporter(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging(options => options.IncludeStackTrace = true) + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + + ILogger logger = loggerFactory.CreateLogger("R9"); + + LoggerTests.ThrowExceptionAndLogError(() => TestExceptionThrower.ThrowExceptionWithMultipleLevelInnerException(), logger); + + bool stackTraceReturned = exporter.FirstState!.TryGetStackTrace(out string? stackTrace); + Assert.NotEmpty(stackTrace!); + Assert.Contains("InnerException type:System.ArgumentException message:2nd level exception", stackTrace); + Assert.Contains("InnerException type:System.AggregateException message:Exception caught in Class C", stackTrace); + } + + [Fact] + public void LogException_WithMultipleLevelInnerStack_BeyondSupportedLimit_Enabled() + { + using var exporter = new TestExporter(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging(options => options.IncludeStackTrace = true) + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + + ILogger logger = loggerFactory.CreateLogger("R9"); + + LoggerTests.ThrowExceptionAndLogError(() => TestExceptionThrower.ThrowExceptionWithMultipleLevelLargeStack(), logger); + + bool stackTraceReturned = exporter.FirstState!.TryGetStackTrace(out string? stackTrace); + Assert.NotEmpty(stackTrace!); + Assert.DoesNotContain("InnerException type:System.AggregateException message:Exception caught in Class C", stackTrace); + } + + [Fact] + public void LogException_WithVerylargeStack_StackTraceTruncatedBeyondMaxSupported() + { + using var exporter = new TestExporter(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging(options => options.IncludeStackTrace = true) + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + + ILogger logger = loggerFactory.CreateLogger("R9"); + + LoggerTests.ThrowExceptionAndLogError(() => TestExceptionThrower.ThrowExceptionWithBigExceptionStack(), logger); + + bool stackTraceReturned = exporter.FirstState!.TryGetStackTrace(out string? stackTrace); + Assert.NotEmpty(stackTrace!); + Assert.Equal(4096, stackTrace!.Length); + } + + [Fact] + public void LogException_NullStack_IncludeStackTrace_Enabled() + { + using var exporter = new TestExporter(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging(options => options.IncludeStackTrace = true) + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + + ILogger logger = loggerFactory.CreateLogger("R9"); + logger.LogError(new NotImplementedException(), "Method is not implemented"); + + bool stackTraceReturned = exporter.FirstState!.TryGetStackTrace(out string? stackTrace); + Assert.True(stackTraceReturned); + Assert.Empty(stackTrace!); + } + + [Fact] + public void CreateLoggerWithFormattedMessages() + { + using var exporter = new TestExporter(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging(options => options.UseFormattedMessage = true) + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + + var logMessage = "This is testing {user}"; + var logger = loggerFactory.CreateLogger("R9"); + logger.LogError(logMessage, "testUser"); + + Assert.Equal("This is testing testUser", exporter.FirstLogRecord!.FormattedMessage); + } + + [Fact] + public void CreateMultipleLoggers() + { + using TestExporter exporter = new TestExporter(); + using var firstLoggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging() + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + + var logMessage = "This is testing {user}"; + ILogger logger = firstLoggerFactory.CreateLogger("R9"); + ILogger secondLogger = firstLoggerFactory.CreateLogger("R9Test"); + + logger.LogError(logMessage, "testUser"); + + var dictExpected = new Dictionary + { + { "{OriginalFormat}", logMessage }, + { "user", "testUser" } + }; + + Assert.True(Helpers.CompareStateValues(exporter.FirstState!, dictExpected)); + + secondLogger.LogInformation(logMessage, "testUser2"); + var dictExpected2 = new Dictionary + { + { "{OriginalFormat}", logMessage }, + { "user", "testUser2" } + }; + + Assert.True(Helpers.CompareStateValues(exporter.FirstState!, dictExpected2)); + } + + [Fact] + public void CreateLoggerReturnsExistingLoggerWhenExists() + { + using var loggerProvider = CreateLoggerProvider(); + + ILogger logger = loggerProvider.CreateLogger("R9"); + ILogger secondLogger = loggerProvider.CreateLogger("R9"); + + logger.LogInformation("test message"); + Assert.Equal(logger, secondLogger); + } + + [Fact] + public void CreateLoggerProviderWithNullOptions() + { + Assert.Throws(() => CreateLoggerProvider(IOptions.Create(null!))); + } + + [Fact] + public void CreateScopes() + { + using var loggerProvider = CreateLoggerProvider(); + + ILogger logger = loggerProvider.CreateLogger("R9"); + Assert.NotNull(logger); + loggerProvider.SetScopeProvider(new LoggerExternalScopeProvider()); + + using (logger.BeginScope("{ScopeName}", "OuterScope")) + { + logger.Log(LogLevel.None, new EventId(1001), "test message", null, null!); + } + + // Should not throw + loggerProvider.SetScopeProvider(null!); + logger.BeginScope("{ScopeName}", "NewScope"); + } + + [Fact] + public void R9LoggerWithNullAndValidParams() + { + using var loggerProvider = CreateLoggerProvider(); + + Assert.NotNull(new Logger("categoryName", loggerProvider)); + + // Validating that multiple dispose calls are safe +#pragma warning disable S3966 // Objects should not be disposed more than once + loggerProvider.Dispose(); +#pragma warning restore S3966 // Objects should not be disposed more than once + } + + [Fact] + public void NullLoggingBuilder() + { + ILoggingBuilder? loggingBuilder = null; + Assert.Throws(() => loggingBuilder!.AddOpenTelemetryLogging(_ => { })); + } + +#if false + [Fact] + public void LoggerWithSimpleProcessorsUsesPropertyBagPool() + { + using var exporter = new TestExporter(); + var processors = new List> + { + new SimpleLogRecordExportProcessor(exporter), + new SimpleLogRecordExportProcessor(exporter), + new SimpleLogRecordExportProcessor(exporter), + new TestProcessor(), + }; + var propertyBagPoolMock = new Mock>(); + propertyBagPoolMock + .Setup(o => o.Get()) + .Returns(new LogEnrichmentPropertyBag()); + propertyBagPoolMock + .Setup(o => o.Return(It.IsAny())); + + using var loggerProvider = CreateLoggerProvider(processors: processors); + var logger = new Logger("categoryName", loggerProvider, propertyBagPoolMock.Object); + var logMessage = "This is testing {user}"; + logger.LogInformation(logMessage, "testUser"); + + propertyBagPoolMock.Verify(o => o.Get(), Times.Exactly(1)); + propertyBagPoolMock.Verify(o => o.Return(It.IsAny()), Times.Exactly(1)); + Assert.NotNull(loggerProvider); + Assert.True(loggerProvider.CanUsePropertyBagPool); + } +#endif + + [Fact] + public void LoggerWithBatchProcessorsDoesntUsePropertyBagPool() + { + using var exporter = new TestExporter(); + var processors = new List> + { + new BatchLogRecordExportProcessor(exporter), + new BatchLogRecordExportProcessor(exporter), + new BatchLogRecordExportProcessor(exporter), + new TestProcessor(), + }; + + using var loggerProvider = CreateLoggerProvider(processors: processors); + + Assert.NotNull(loggerProvider); + Assert.False(loggerProvider.CanUsePropertyBagPool); + + var logger = loggerProvider.CreateLogger(Guid.NewGuid().ToString()); + Assert.NotNull(logger); + + logger.LogInformation("test message"); + } + + [Fact] + public void LoggerWithSimpleAndBatchProcessorsDoesntUsePropertyBagPool() + { + using var exporter = new TestExporter(); + var processors = new List> + { + new SimpleLogRecordExportProcessor(exporter), + new BatchLogRecordExportProcessor(exporter), + new BatchLogRecordExportProcessor(exporter), + new TestProcessor(), + }; + + using var loggerProvider = CreateLoggerProvider(processors: processors); + + Assert.NotNull(loggerProvider); + Assert.False(loggerProvider.CanUsePropertyBagPool); + } + + [Fact] + public void DependencyInjectionSetup() + { + using var host = FakeHost.CreateBuilder(options => options.FakeLogging = false) + .ConfigureLogging(loggingBuilder => loggingBuilder + .AddOpenTelemetryLogging(options => options.UseFormattedMessage = true) + .AddProcessor(new TestProcessor())) + .ConfigureServices(services => services + .AddLogEnricher() + .Configure(options => options.IncludeScopes = true)) + .Build(); + + var loggerProvider = (LoggerProvider)host.Services.GetRequiredService(); + Assert.Single(loggerProvider.Enrichers); + Assert.IsType(loggerProvider.Enrichers[0]); + Assert.NotNull(loggerProvider.Processor); + Assert.IsType(loggerProvider.Processor); + Assert.True(loggerProvider.UseFormattedMessage); + Assert.True(loggerProvider.IncludeScopes); + } + + [Fact] + public void AddCustomProcessorType() + { + using var host = FakeHost.CreateBuilder(options => options.FakeLogging = false) + .ConfigureLogging(loggingBuilder => loggingBuilder + .AddOpenTelemetryLogging() + .AddProcessor()) + .Build(); + + var loggerProvider = (LoggerProvider)host.Services.GetRequiredService(); + Assert.NotNull(loggerProvider.Processor); + Assert.IsType(loggerProvider.Processor); + } + + [Fact] + public void AddMultipleCustomProcessorTypes() + { + using var host = FakeHost.CreateBuilder(options => options.FakeLogging = false) + .ConfigureLogging(loggingBuilder => loggingBuilder + .AddOpenTelemetryLogging() + .AddProcessor() + .AddProcessor()) + .Build(); + + var loggerProvider = (LoggerProvider)host.Services.GetRequiredService(); + var processor = loggerProvider.Processor; + Assert.NotNull(processor); + Assert.IsType>(processor); + } + + [Fact] + public void AddCustomProcessorTypeAndInstance() + { + using var host = FakeHost.CreateBuilder(options => options.FakeLogging = false) + .ConfigureLogging(loggingBuilder => loggingBuilder + .AddOpenTelemetryLogging() + .AddProcessor() + .AddProcessor(new TestProcessor())) + .Build(); + + var loggerProvider = (LoggerProvider)host.Services.GetRequiredService(); + var processor = loggerProvider.Processor; + Assert.NotNull(processor); + Assert.IsType>(processor); + } + + [Fact] + public void LoggerProviderDisposeLogic() + { + var processor = new TestProcessor(); + var loggerProvider = CreateLoggerProvider(processors: new List> { processor }); + + loggerProvider.Dispose(); +#pragma warning disable S3966 // Objects should not be disposed more than once + loggerProvider.Dispose(); +#pragma warning restore S3966 // Objects should not be disposed more than once + + Assert.Equal(1, processor.DisposeCalledTimes); + } + + [Fact] + public void LoggerConfiguredWithConfigurationSection() + { + var configRoot = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + var configSection = configRoot.GetSection("Logging"); + + using var host = FakeHost.CreateBuilder(options => options.FakeLogging = false) + .ConfigureLogging(loggingBuilder => loggingBuilder.AddOpenTelemetryLogging(configSection)) + .Build(); + + var loggerProvider = (LoggerProvider)host.Services.GetRequiredService(); + Assert.True(loggerProvider.UseFormattedMessage); + Assert.False(loggerProvider.IncludeScopes); + } + + [Fact] + public void LogException_IncludeStackTrace_Enabled_StackTraceLengthConfigured_GreaterThanRange_Throw() + { + using var exporter = new TestExporter(); + Assert.Throws(() => + { + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging(options => + { + options.IncludeStackTrace = true; + options.MaxStackTraceLength = 32770; + }) + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + }); + } + + [Fact] + public void LogException_IncludeStackTrace_Enabled_StackTraceLengthConfigured_LessThanRange_Throws() + { + using var exporter = new TestExporter(); + Assert.Throws(() => + { + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging(options => + { + options.IncludeStackTrace = true; + options.MaxStackTraceLength = 2046; + }) + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + }); + } + + [Fact] + public void LogException_IncludeStackTrace_Disabled_StackTraceLengthConfigured_GreaterThanRange_Throws() + { + using var exporter = new TestExporter(); + Assert.Throws(() => + { + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging(options => + { + options.MaxStackTraceLength = 32770; + }) + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + }); + } + + [Fact] + public void LogException_IncludeStackTrace_Disabled_StackTraceLengthConfigured_LessThanRange_Throws() + { + using var exporter = new TestExporter(); + Assert.Throws(() => + { + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging(options => + { + options.MaxStackTraceLength = 2047; + }) + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + }); + } + + [Fact] + public void LogException_IncludeStackTrace_Disabled_StackTraceLengthConfigured_NoStackTrace() + { + using var exporter = new TestExporter(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging(options => + { + options.MaxStackTraceLength = 4000; + }) + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + + ILogger logger = loggerFactory.CreateLogger("R9"); + logger.LogError(new NotImplementedException(), "Method is not implemented"); + + Assert.False(exporter.FirstState!.TryGetStackTrace(out string? stackTrace)); + } + + [Fact] + public void LogException_MultipleLevelInnerStack_BeyondSupportedLimit_MaxStackTraceLength() + { + using var exporter = new TestExporter(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging(options => + { + options.IncludeStackTrace = true; + options.MaxStackTraceLength = 32768; + }) + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + + ILogger logger = loggerFactory.CreateLogger("R9"); + + LoggerTests.ThrowExceptionAndLogError(() => TestExceptionThrower.ThrowExceptionWithMultipleLevelLargeStack(), logger); + + bool stackTraceReturned = exporter.FirstState!.TryGetStackTrace(out string? stackTrace); + Assert.NotEmpty(stackTrace!); + Assert.DoesNotContain("InnerException type:System.AggregateException message:Exception caught in Class C", stackTrace); + } + + [Fact] + public void LogException_VerylargeStack_StackTraceTruncated_MaxStackTraceLength() + { + using var exporter = new TestExporter(); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetryLogging(options => + { + options.IncludeStackTrace = true; + options.MaxStackTraceLength = 32768; + }) + .AddProcessor(new SimpleLogRecordExportProcessor(exporter))); + + ILogger logger = loggerFactory.CreateLogger("R9"); + + LoggerTests.ThrowExceptionAndLogError(() => TestExceptionThrower.ThrowExceptionWithBigExceptionStack(), logger); + + bool stackTraceReturned = exporter.FirstState!.TryGetStackTrace(out string? stackTrace); + Assert.NotEmpty(stackTrace!); + Assert.Equal(32768, stackTrace!.Length); + } + + private static void ThrowExceptionAndLogError(Action action, ILogger logger) + { +#pragma warning disable CA1031 // Do not catch general exception types + try + { + action(); + } + catch (Exception ex) + { + logger.LogError(ex, "test exception"); + } +#pragma warning restore CA1031 // Do not catch general exception types + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/Auxiliary/TestEventSource.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/Auxiliary/TestEventSource.cs new file mode 100644 index 0000000000..70f8e94fc7 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/Auxiliary/TestEventSource.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Tracing; + +namespace Microsoft.Extensions.Telemetry.Metering.Test.Auxiliary; + +internal class TestEventSource : EventSource +{ + public EventCommandEventArgs? EventCommandEventArgs { get; set; } + + public TestEventSource(string eventSourceName) + : base(eventSourceName) + { + } + + protected override void OnEventCommand(EventCommandEventArgs command) + { + EventCommandEventArgs = command; + base.OnEventCommand(command); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/Auxiliary/TestUtils.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/Auxiliary/TestUtils.cs new file mode 100644 index 0000000000..a91d9e9ccf --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/Auxiliary/TestUtils.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Options; + +using static Microsoft.Extensions.Options.Options; + +namespace Microsoft.Extensions.Telemetry.Metering.Test.Auxiliary; + +public static class TestUtils +{ + public const string SystemRuntime = "System.Runtime"; + + public static IOptions CreateOptions(string eventSource, string counterName) + { + var options = Create(new EventCountersCollectorOptions()); + options.Value.Counters.Add(eventSource, new HashSet { counterName }); + options.Value.SamplingInterval = TimeSpan.FromMilliseconds(1); + + return options; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/EventCountersExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/EventCountersExtensionsTest.cs new file mode 100644 index 0000000000..a2e65b7be4 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/EventCountersExtensionsTest.cs @@ -0,0 +1,201 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Metering.Test; + +public class EventCountersExtensionsTest +{ + [Fact] + public void AddEventCounterCollector_Throws_WhenServiceCollectionNull() + { + Assert.Throws(() => EventCountersExtensions.AddEventCounterCollector(null!, Mock.Of())); + Assert.Throws(() => EventCountersExtensions.AddEventCounterCollector(null!, options => { })); + } + + [Fact] + public void AddEventCounterCollector_Throws_WhenActionNull() + { + var services = new ServiceCollection(); + Assert.Throws(() => services.AddEventCounterCollector((Action)null!)); + } + + [Fact] + public void AddEventCounterCollector_Throws_WhenConfigSectionNull() + { + var services = new ServiceCollection(); + Assert.Throws(() => services.AddEventCounterCollector((IConfigurationSection)null!)); + } + + [Fact] + public async Task AddEventCounterCollector_ValidatesOptionsInSection() + { + using var host = FakeHost.CreateBuilder() + .ConfigureAppConfiguration(static x => x.AddJsonFile("appsettings.json")) + .ConfigureServices(static (context, services) => services + .AddEventCounterCollector(context.Configuration.GetSection("InvalidConfig"))) + .Build(); + + await Assert.ThrowsAsync(() => host.RunAsync()); + } + + [Fact] + public async Task AddEventCounterCollector_ValidatesNullCountersInOptions() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(static services => services + .AddEventCounterCollector(static x => x.Counters = null!)) + .Build(); + + await Assert.ThrowsAsync(() => host.RunAsync()); + } + + [Fact] + public async Task AddEventCounterCollector_ValidatesEmptyCountersInOptions() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(static services => services + .AddEventCounterCollector(static _ => { })) + .Build(); + + await Assert.ThrowsAsync(() => host.RunAsync()); + } + + [Fact] + public async Task AddEventCounterCollector_ValidatesCountersNullValueInOptions() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(static services => services + .AddEventCounterCollector(static x => x.Counters.Add("key", null!))) + .Build(); + + await Assert.ThrowsAsync(() => host.RunAsync()); + } + + [Fact] + public async Task AddEventCounterCollector_ValidatesCountersEmptyValueInOptions() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(static services => services + .AddEventCounterCollector(static x => x.Counters.Add("key", new HashSet()))) + .Build(); + + await Assert.ThrowsAsync(() => host.RunAsync()); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(601)] + public async Task AddEventCounterCollector_ValidatesSamplingIntervalInOptions(int interval) + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddEventCounterCollector(x => x.SamplingInterval = TimeSpan.FromSeconds(interval))) + .Build(); + + await Assert.ThrowsAsync(() => host.RunAsync()); + } + + [Fact] + public void AddEventCounterCollector_AddsOptions() + { + var services = new ServiceCollection(); + services.AddEventCounterCollector(static x => x.Counters.Add("key", new SortedSet { "foo" })); + + using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + Assert.NotNull(options); + Assert.NotNull(options.Value); + Assert.NotNull(options.Value.Counters); + Assert.NotEmpty(options.Value.Counters); + Assert.Equal(1D, options.Value.SamplingInterval.TotalSeconds); + } + + [Fact] + public void AddEventCounterCollectorWithAction_AddsOptions() + { + var services = new ServiceCollection(); + services.AddEventCounterCollector(static o => + o.Counters.Add("eventSource", new HashSet { "eventCounter" })); + + using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + Assert.Contains("eventSource", options.Value.Counters); + } + + [Fact] + public void AddEventCounterCollectorWithSection_AddsOptions() + { + EventCountersCollectorOptions? opts = null; + var configRoot = new ConfigurationBuilder() + .AddInMemoryCollection( + new Dictionary + { + { ConfigurationPath.Combine("SectionName", nameof(opts.SamplingInterval)), "00:01:00" }, + { ConfigurationPath.Combine("SectionName", nameof(opts.Counters), "Key1", "0"), "foo" }, + { ConfigurationPath.Combine("SectionName", nameof(opts.Counters), "Key1", "1"), "bar" }, + { ConfigurationPath.Combine("SectionName", nameof(opts.Counters), "Key1", "2"), "bar" }, // Duplicate value + { ConfigurationPath.Combine("SectionName", nameof(opts.Counters), "Key2", "0"), "baz" }, + }) + .Build(); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddEventCounterCollector(configRoot.GetSection("SectionName")); + + using var serviceProvider = serviceCollection.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + Assert.NotNull(options); + Assert.NotNull(options.Value); + + opts = options.Value; + Assert.NotNull(opts.Counters); + Assert.Equal(2, opts.Counters.Count); + Assert.Equal(2, opts.Counters["Key1"].Count); + Assert.Equal(1, opts.Counters["Key2"].Count); + + Assert.Contains("foo", opts.Counters["Key1"]); + Assert.Contains("bar", opts.Counters["Key1"]); + Assert.Contains("baz", opts.Counters["Key2"]); + Assert.Equal(1D, opts.SamplingInterval.TotalMinutes); + } + + [Fact] + public void AddEventCounterCollectorWithSectionFromFile_AddsOptions() + { + var configRoot = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddEventCounterCollector(configRoot.GetSection("ValidConfig")); + + using var serviceProvider = serviceCollection.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + Assert.NotNull(options); + Assert.NotNull(options.Value); + + var opts = options.Value; + Assert.NotNull(opts.Counters); + Assert.Equal(2, opts.Counters.Count); + Assert.Equal(4, opts.Counters["Key1"].Count); + Assert.Equal(1, opts.Counters["Key2"].Count); + + Assert.Contains("one", opts.Counters["Key1"]); + Assert.Contains("two", opts.Counters["Key1"]); + Assert.Contains("three", opts.Counters["Key1"]); + Assert.Contains("four", opts.Counters["Key1"]); + Assert.Contains("ABC", opts.Counters["Key2"]); + Assert.Equal(2D, opts.SamplingInterval.TotalMinutes); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/EventCountersListenerTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/EventCountersListenerTest.cs new file mode 100644 index 0000000000..064ad0fe65 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/EventCountersListenerTest.cs @@ -0,0 +1,634 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +#if !NETFRAMEWORK +using System.Threading; +#endif +using AutoFixture; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Extensions.Telemetry.Metering.Test.Auxiliary; +using Microsoft.Extensions.Telemetry.Testing.Metering; +using Xunit; + +using static Microsoft.Extensions.Options.Options; + +namespace Microsoft.Extensions.Telemetry.Metering.Test; + +public class EventCountersListenerTest +{ + private const string EventName = "EventCounters"; + private readonly Fixture _fixture = new(); + private readonly IOptions _options; + private readonly string _eventSourceName; + private readonly string _counterName; + private readonly ILogger _logger; + + public EventCountersListenerTest() + { + _options = Create(new EventCountersCollectorOptions()); + _eventSourceName = _fixture.Create(); + _counterName = _fixture.Create(); + _options.Value.Counters.Add(_eventSourceName, new HashSet { _counterName }); + _logger = NullLoggerFactory.Instance.CreateLogger(); + } + + [Fact] + public void EventCountersListener_WhenNullOptions_Throws() + { + using var meter = new Meter(); + Assert.Throws(() => new EventCountersListener(Create(null!), meter)); + } + + [Fact] + public void EventCountersListener_Ignores_NonStructuredEvents() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + using var eventSource = new EventSource(_eventSourceName); + using var listener = new EventCountersListener(_options, meter, _logger); + eventSource.Write(EventName, + new EventSourceOptions { Level = EventLevel.LogAlways }, + new + { + CounterType = "Sum", + Name = _counterName, + Increment = 1 + }); + + var counters = metricCollector.GetAllCounters(); + var histograms = metricCollector.GetAllCounters(); + + Assert.Empty(counters!); + Assert.Empty(histograms!); + } + + [Fact] + public void EventCountersListener_Ignores_UnknownCounterTypes() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + using var eventSource = new EventSource(_eventSourceName); + using var listener = new EventCountersListener(_options, meter); + eventSource.Write(EventName, + new EventSourceOptions { Level = EventLevel.LogAlways }, + new + { + payload = new { CounterType = _fixture.Create(), Name = _counterName, Increment = 1 } + }); + + var counters = metricCollector.GetAllCounters(); + var histograms = metricCollector.GetAllCounters(); + + Assert.Empty(counters!); + Assert.Empty(histograms!); + } + + [Fact] + public void EventCountersListener_ValidateEventCounterInterval_SetCorrectly() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + var intervalInSeconds = 2; + + IOptions options = Create(new EventCountersCollectorOptions + { + SamplingInterval = TimeSpan.FromSeconds(intervalInSeconds), + }); + + options.Value.Counters.Add(_eventSourceName, new HashSet { _counterName }); + + using var eventSource = new TestEventSource(_eventSourceName); + using var listener = new EventCountersListener(options, meter); + + eventSource.Write(EventName, + new EventSourceOptions { Level = EventLevel.LogAlways }, + new + { + payload = new { CounterType = "Sum", Name = _counterName, Increment = 1 } + }); + + Assert.True(eventSource.EventCommandEventArgs?.Arguments?.ContainsKey("EventCounterIntervalSec")); + Assert.Equal($"{intervalInSeconds}", eventSource.EventCommandEventArgs?.Arguments?["EventCounterIntervalSec"]); + } + + [Fact] + public void EventCountersListener_OnEventWritten_Ignores_WithNullCounter() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + IDictionary> counters = new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + { _eventSourceName, null! } + }; + + IOptions options = Create(new EventCountersCollectorOptions + { + Counters = counters + }); + + using var eventSource = new TestEventSource(_eventSourceName); + using var listener = new EventCountersListener(options, meter); + + eventSource.Write(EventName, + new EventSourceOptions { Level = EventLevel.LogAlways }, + new + { + payload = new { CounterType = "Sum", Name = _counterName, Increment = 1 } + }); + + Assert.Empty(metricCollector.GetAllHistograms()!); + Assert.Empty(metricCollector.GetAllCounters()!); + } + + [Fact] + public void EventCountersListener_OnEventWritten_Ignores_WithEmptyCounterName() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + using var eventSource = new EventSource(_eventSourceName); + using var listener = new EventCountersListener(_options, meter); + + string emptyCounterName = string.Empty; + eventSource.Write(EventName, + new EventSourceOptions { Level = EventLevel.LogAlways }, + new + { + payload = new { CounterType = "Sum", Name = emptyCounterName, Increment = 1 } + }); + + Assert.Empty(metricCollector.GetAllHistograms()!); + Assert.Empty(metricCollector.GetAllCounters()!); + } + + [Fact] + public void EventCountersListener_OnEventWritten_Ignores_WithoutEventName() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + using var eventSource = new TestEventSource(_eventSourceName); + using var listener = new EventCountersListener(_options, meter); + + eventSource.Write(null, + new EventSourceOptions { Level = EventLevel.LogAlways }, + new + { + payload = new { CounterType = "Sum", Name = _counterName, Increment = 1 } + }); + + Assert.Empty(metricCollector.GetAllHistograms()!); + Assert.Empty(metricCollector.GetAllCounters()!); + } + + [Fact] + public void EventCountersListener_OnEventWritten_Ignores_WithoutPayload() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + using var eventSource = new TestEventSource(_eventSourceName); + using var listener = new EventCountersListener(_options, meter); + + eventSource.Write(_eventSourceName, + new EventSourceOptions { Level = EventLevel.LogAlways }); + + Assert.Empty(metricCollector.GetAllHistograms()!); + Assert.Empty(metricCollector.GetAllCounters()!); + } + + [Fact] + public void EventCountersListener_OnEventWritten_Ignores_WithoutEventData() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + using var eventSource = new TestEventSource(_eventSourceName); + using var listener = new EventCountersListener(_options, meter); + + eventSource.Write(_eventSourceName); + + Assert.Empty(metricCollector.GetAllHistograms()!); + Assert.Empty(metricCollector.GetAllCounters()!); + } + + [Fact] + public void EventCountersListener_Ignores_UnknownEventSources() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + SendMeanEvent(meter, + MeanEventProperties.All, + eventSourceName: _fixture.Create(), + counterName: _counterName, + eventName: EventName); + + Assert.Empty(metricCollector.GetAllHistograms()!); + Assert.Empty(metricCollector.GetAllCounters()!); + } + + [Fact] + public void EventCountersListener_Ignores_UnknownEvents() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + SendMeanEvent(meter, + MeanEventProperties.All, + eventSourceName: _eventSourceName, + counterName: _counterName, + eventName: _fixture.Create()); + + Assert.Empty(metricCollector.GetAllHistograms()!); + Assert.Empty(metricCollector.GetAllCounters()!); + } + + [Fact] + public void EventCountersListener_Ignores_UnknownCounters() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + _options.Value.Counters.Clear(); + + SendMeanEvent(meter, MeanEventProperties.All); + + Assert.Empty(metricCollector.GetAllHistograms()!); + Assert.Empty(metricCollector.GetAllCounters()!); + } + + [Fact] + public void EventCountersListener_Ignores_EventsWithoutCounterType() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + SendMeanEvent(meter, MeanEventProperties.All ^ MeanEventProperties.WithType); + + Assert.Empty(metricCollector.GetAllHistograms()!); + Assert.Empty(metricCollector.GetAllCounters()!); + } + + [Fact] + public void EventCountersListener_Ignores_EventsWithoutName() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + SendMeanEvent(meter, MeanEventProperties.All ^ MeanEventProperties.WithName); + + Assert.Empty(metricCollector.GetAllHistograms()!); + Assert.Empty(metricCollector.GetAllCounters()!); + } + + [Fact] + public void EventCountersListener_Ignores_MeanEventsWithoutMean() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + SendMeanEvent(meter, MeanEventProperties.All ^ MeanEventProperties.WithMean); + + Assert.Empty(metricCollector.GetAllHistograms()!); + Assert.Empty(metricCollector.GetAllCounters()!); + } + + [Fact] + public void EventCountersListener_Ignores_Empty_Counters_Maps() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + var eventSourceName = Guid.NewGuid().ToString(); + var options = new EventCountersCollectorOptions(); + options.Counters.Add(eventSourceName, new HashSet()); + using var eventSource = new EventSource(eventSourceName); + using var listener = new EventCountersListener(_options, meter); + + eventSource.Write(EventName, + new EventSourceOptions { Level = EventLevel.LogAlways }, + new + { + payload = new { CounterType = "Sum", Name = _counterName, Increment = 1 } + }); + + Assert.Empty(metricCollector.GetAllCounters()!); + } + + [Fact] + public void EventCountersListener_Emits_WhenSumCounterMatches() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + using var eventSource = new EventSource(_eventSourceName); + using var listener = new EventCountersListener(_options, meter); + eventSource.Write(EventName, + new EventSourceOptions { Level = EventLevel.LogAlways }, + new + { + payload = new { CounterType = "Sum", Name = _counterName, Increment = 1 } + }); + + var latest = metricCollector.GetCounterValues($"{_eventSourceName}|{_counterName}")!.LatestWritten!; + Assert.NotNull(latest); + Assert.Equal(1, latest.Value); + } + + [Fact] + public void EventCountersListener_Ignores_WhenSumCounterMatches_ValueIsDoublePositiveInfinity() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + using var eventSource = new EventSource(_eventSourceName); + using var listener = new EventCountersListener(_options, meter); + eventSource.Write(EventName, + new EventSourceOptions { Level = EventLevel.LogAlways }, + new + { + payload = new { CounterType = "Sum", Name = _counterName, Increment = double.PositiveInfinity } + }); + + Assert.Empty(metricCollector.GetAllCounters()!); + } + + [Fact] + public void EventCountersListener_Ignores_WhenSumCounterMatches_ValueIsDoubleNegativeInfinity() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + using var eventSource = new EventSource(_eventSourceName); + using var listener = new EventCountersListener(_options, meter); + eventSource.Write(EventName, + new EventSourceOptions { Level = EventLevel.LogAlways }, + new + { + payload = new { CounterType = "Sum", Name = _counterName, Increment = double.NegativeInfinity } + }); + + Assert.Empty(metricCollector.GetAllCounters()!); + } + + [Fact] + public void EventCountersListener_Ignores_WhenSumCounterMatches_ValueIsDoubleNan() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + using var eventSource = new EventSource(_eventSourceName); + using var listener = new EventCountersListener(_options, meter); + eventSource.Write(EventName, + new EventSourceOptions { Level = EventLevel.LogAlways }, + new + { + payload = new { CounterType = "Sum", Name = _counterName, Increment = double.NaN } + }); + + Assert.Empty(metricCollector.GetAllCounters()!); + } + + [Fact] + public void EventCountersListener_Ignores_WhenSumCounterMatches_ValueIncorrectlyFormatted() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + using var eventSource = new EventSource(_eventSourceName); + using var listener = new EventCountersListener(_options, meter); + eventSource.Write(EventName, + new EventSourceOptions { Level = EventLevel.LogAlways }, + new + { + payload = new { CounterType = "Sum", Name = _counterName, Increment = "?/str." } + }); + + Assert.Empty(metricCollector.GetAllCounters()!); + } + + [Fact] + public void EventCountersListener_Emits_WhenSourceIsCreatedAfterListener() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + using var listener = new EventCountersListener(_options, meter); + using var eventSource = new EventSource(_eventSourceName); + eventSource.Write(EventName, + new EventSourceOptions { Level = EventLevel.LogAlways }, + new + { + payload = new { CounterType = "Sum", Name = _counterName, Increment = 1 } + }); + + var latest = metricCollector.GetCounterValues($"{_eventSourceName}|{_counterName}")!.LatestWritten!; + Assert.NotNull(latest); + Assert.Equal(1, latest.Value); + } + + [Fact] + public void EventCountersListener_Ignores_SumCounterWithoutIncrement() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + using var eventSource = new EventSource(_eventSourceName); + using var listener = new EventCountersListener(_options, meter); + eventSource.Write(EventName, + new EventSourceOptions { Level = EventLevel.LogAlways }, + new + { + payload = new { CounterType = "Sum", Name = _counterName } + }); + + Assert.Empty(metricCollector.GetAllCounters()!); + } + + [Fact] + public void EventCountersListener_Emits_WhenMeanCounterMatches() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + SendMeanEvent(meter, MeanEventProperties.All); + + var latest = metricCollector.GetHistogramValues($"{_eventSourceName}|{_counterName}")!.LatestWritten!; + Assert.NotNull(latest); + Assert.Equal(1, latest.Value); + } + + [Fact] + public void EventCountersListener_Ignores_WhenMeanCounterMatches_MeanValueDoubleNan() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + SendMeanEvent(meter, MeanEventProperties.All, _eventSourceName, _counterName, EventName, meanValue: double.NaN); + + Assert.Empty(metricCollector.GetAllHistograms()!); + } + + [Fact] + public void EventCountersListener_Ignores_WhenMeanCounterMatches_MeanValuePositiveInfinity() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + SendMeanEvent(meter, MeanEventProperties.All, _eventSourceName, _counterName, EventName, meanValue: double.PositiveInfinity); + + Assert.Empty(metricCollector.GetAllHistograms()!); + } + + [Fact] + public void EventCountersListener_Ignores_WhenMeanCounterMatches_MeanValueNegativeInfinity() + { + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + SendMeanEvent(meter, MeanEventProperties.All, _eventSourceName, _counterName, EventName, meanValue: double.NegativeInfinity); + + Assert.Empty(metricCollector.GetAllHistograms()!); + } + + [Fact] + public void EventCountersListener_Ignores_WhenCounterNameIsNotRegistered() + { + var options = Create(new EventCountersCollectorOptions()); + options.Value.Counters.Add(_eventSourceName, new HashSet { _counterName }); + + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + + using var eventSource = new EventSource(_eventSourceName); + using var listener = new EventCountersListener(options, meter); + + eventSource.Write(EventName, + new EventSourceOptions { Level = EventLevel.LogAlways }, + new + { + payload = new { CounterType = "Sum", Name = "randomCounterName", Increment = 1 } + }); + + Assert.Empty(metricCollector.GetAllCounters()!); + Assert.Empty(metricCollector.GetAllHistograms()!); + } + + [Flags] + private enum MeanEventProperties + { + None = 0, + WithName = 1, + WithType = 2, + WithMean = 4, + All = WithMean | WithType + } + + private void SendMeanEvent(Meter meter, MeanEventProperties meanEventProperties) + { + SendMeanEvent(meter, meanEventProperties, _eventSourceName, _counterName, EventName); + } + + private void SendMeanEvent(Meter meter, + MeanEventProperties meanEventProperties, + string eventSourceName, + string counterName, + string eventName, + double meanValue = 1) + { + using var eventSource = new EventSource(eventSourceName); + using var listener = new EventCountersListener(_options, meter); + + switch (meanEventProperties) + { + case MeanEventProperties.All: + eventSource.Write(eventName, + new EventSourceOptions { Level = EventLevel.LogAlways }, + new + { + payload = new { CounterType = "Mean", Name = counterName, Mean = meanValue } + }); + break; + + case MeanEventProperties.All ^ MeanEventProperties.WithMean: + eventSource.Write(eventName, + new EventSourceOptions { Level = EventLevel.LogAlways }, + new + { + payload = new { CounterType = "Mean", Name = counterName } + }); + break; + + case MeanEventProperties.All ^ MeanEventProperties.WithName: + eventSource.Write(eventName, + new EventSourceOptions { Level = EventLevel.LogAlways }, + new + { + payload = new { CounterType = "Mean", Mean = meanValue } + }); + break; + + case MeanEventProperties.All ^ MeanEventProperties.WithType: + eventSource.Write(eventName, + new EventSourceOptions { Level = EventLevel.LogAlways }, + new + { + payload = new { Name = counterName, Mean = meanValue } + }); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(meanEventProperties), meanEventProperties, "unsupported combination"); + } + } + +#if !NETFRAMEWORK + [Fact] + public void MeanCounter() + { + string counterName = "active-timer-count"; + string metricName = $"{TestUtils.SystemRuntime}|{counterName}"; + var options = TestUtils.CreateOptions(TestUtils.SystemRuntime, counterName); + + using var eventWaitHandle = new EventWaitHandle(false, EventResetMode.ManualReset); + + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + RunListener(options, meter, eventWaitHandle); + + var latest = metricCollector.GetHistogramValues(metricName)!.LatestWritten!; + Assert.NotNull(latest); + Assert.True(latest.Value >= 0); + } + + [Fact] + public void SumCounter() + { + string counterName = "alloc-rate"; + string metricName = $"{TestUtils.SystemRuntime}|{counterName}"; + var options = TestUtils.CreateOptions(TestUtils.SystemRuntime, counterName); + + using var eventWaitHandle = new EventWaitHandle(false, EventResetMode.ManualReset); + + using var meter = new Meter(); + using var metricCollector = new MetricCollector(meter); + RunListener(options, meter, eventWaitHandle); + + var latest = metricCollector.GetCounterValues(metricName)!.LatestWritten!; + Assert.NotNull(latest); + Assert.True(latest.Value >= 0); + } + + private static void RunListener(IOptions options, Meter meter, WaitHandle eventWaitHandle) + { + using var eventListener = new EventCountersListener(options, meter); + eventWaitHandle.WaitOne(TimeSpan.FromMilliseconds(1000)); + } +#endif +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/EventCountersValidatorTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/EventCountersValidatorTest.cs new file mode 100644 index 0000000000..749fe0ad02 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering.Collectors.EventCounters/EventCountersValidatorTest.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Extensions.Telemetry.Metering.Internal; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Metering.Test; + +public class EventCountersValidatorTest +{ + private const string FieldName = nameof(EventCountersCollectorOptions.Counters); + + private readonly EventCountersValidator _validator = new(); + + [Fact] + public void ShouldValidateCustomNamedOptions_NullSet() + { + const string OptionsName = "MyCustomName"; + + var options = new EventCountersCollectorOptions(); + options.Counters["key"] = null!; + + var result = _validator.Validate(OptionsName, options); + Assert.True(result.Failed); + Assert.Equal($"Counters[\"key\"]: The {OptionsName}.{FieldName}[\"key\"] field is required.", result.FailureMessage); + } + + [Fact] + public void ShouldValidateCustomNamedOptions_EmptySet() + { + const string OptionsName = nameof(EventCountersCollectorOptions); + + var options = new EventCountersCollectorOptions(); + options.Counters["key"] = new HashSet(); + + var result = _validator.Validate(string.Empty, options); + Assert.True(result.Failed); + Assert.Equal($"Counters[\"key\"]: The field {OptionsName}.{FieldName}[\"key\"] length must be greater or equal than 1.", result.FailureMessage); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering/Internal/TestEnricher.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering/Internal/TestEnricher.cs new file mode 100644 index 0000000000..fca409c65e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering/Internal/TestEnricher.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Telemetry.Enrichment; + +namespace Microsoft.Extensions.Telemetry.Metering.Test.Internal; + +internal class TestEnricher : IMetricEnricher +{ + public void Enrich(IEnrichmentPropertyBag enrichmentBag) + { + enrichmentBag.Add("testKey", "testValue"); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering/Internal/TestExporter.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering/Internal/TestExporter.cs new file mode 100644 index 0000000000..a017d7e258 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering/Internal/TestExporter.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using OpenTelemetry; +using OpenTelemetry.Metrics; + +namespace Microsoft.Extensions.Telemetry.Metering.Test.Internal; + +internal class TestExporter : BaseExporter +{ + public Batch Metrics { get; set; } + + public override ExportResult Export(in Batch batch) + { + Metrics = batch; + return ExportResult.Success; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering/Internal/TestExtensions.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering/Internal/TestExtensions.cs new file mode 100644 index 0000000000..8f9dd0e87b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering/Internal/TestExtensions.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using OpenTelemetry; +using OpenTelemetry.Metrics; + +namespace Microsoft.Extensions.Telemetry.Metering.Test.Internal; + +internal static class TestExtensions +{ + public static MeterProviderBuilder AddTestExporter(this MeterProviderBuilder builder, MetricReader reader) + { + return builder.AddReader(reader); + } + +#pragma warning disable S1751 // Loops with at most one iteration should be refactored + public static MetricPoint First(this Metric metric) + { + foreach (var metricPoint in metric.GetMetricPoints()) + { + return metricPoint; + } + + return default; + } + + public static Metric First(this Batch metrics) + { + foreach (var metric in metrics) + { + return metric; + } + + return null!; + } + + public static Metric Get(this Batch metrics, int index) + { + foreach (var metric in metrics) + { + if (--index == 0) + { + return metric; + } + } + + return null!; + } + + public static Metric FirstMetric(this TestExporter testExporter) + { + return testExporter.Metrics.First(); + } + + public static MetricPoint FirstMetricPoint(this TestExporter testExporter) + { + return testExporter.FirstMetric().First(); + } + + public static MetricPoint FirstMetricPoint(this Batch metrics) + { + return metrics.First().First(); + } +#pragma warning restore S1751 // Loops with at most one iteration should be refactored +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering/OTelMeteringExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering/OTelMeteringExtensionsTests.cs new file mode 100644 index 0000000000..8ef3fbdf5d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Metering/OTelMeteringExtensionsTests.cs @@ -0,0 +1,414 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Telemetry.Enrichment; +using Microsoft.Extensions.Telemetry.Metering.Test.Internal; +using Newtonsoft.Json; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Metering.Test; + +public class OTelMeteringExtensionsTests +{ + [Fact(Skip = "Flaky")] + public void AddMetering_NullThrows() + { + MeterProviderBuilder nullBuilder = null!; + IConfigurationRoot jsonConfigRoot = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + var configurationSection = jsonConfigRoot.GetSection("Metering"); + + var meteringOptions = new MeteringOptions(); + jsonConfigRoot.Bind("Metering", meteringOptions); + configurationSection.Value = JsonConvert.SerializeObject(meteringOptions); + + Assert.Throws(() => nullBuilder.AddMetering()); + Assert.Throws(() => nullBuilder.AddMetering(options => { })); + Assert.Throws(() => nullBuilder.AddMetering(configurationSection)); + } + + [Fact(Skip = "Flaky")] + public async Task AddMetering_MetricPointsPerMetricStream_MoreThanConfigured_GetsDropped() + { + using var exporter = new TestExporter(); + using var reader = new BaseExportingMetricReader(exporter); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices((_, services) => + services.AddOpenTelemetry().WithMetrics(builder => + { + builder + .AddMetering(options => + { + options.MaxMetricPointsPerStream = 3; + }) + .AddTestExporter(reader); + })) + .StartAsync(); + + var meterName = $"testMeter{DateTime.Now.Ticks}"; + using var meter = new Meter(meterName); + var counter = meter.CreateCounter($"counter"); + + for (int i = 0; i < 5; i++) + { + var tagList = new TagList + { + new KeyValuePair($"dim", $"value{i}") + }; + + counter.Add(1, tagList); + } + + reader.Collect(); + + Assert.Equal(1, exporter.Metrics.Count); + Assert.Equal(meterName, exporter.Metrics.Get(1).MeterName); + Assert.Equal("counter", exporter.Metrics.Get(1).Name); + var metric = exporter.Metrics.Get(1); + + int metricPointsCount = 0; + foreach (var metricPoint in metric.GetMetricPoints()) + { + metricPointsCount++; + Assert.Equal(1, metricPoint.Tags.Count); + } + + Assert.Equal(2, metricPointsCount); + await host.StopAsync(); + } + + [Fact(Skip = "Flaky")] + public async Task AddMetering_MeterStateOverrides_GetsAppliedCorrectly() + { + MeterProviderBuilder builder = Sdk.CreateMeterProviderBuilder(); + IConfigurationRoot jsonConfigRoot = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + var configurationSection = jsonConfigRoot.GetSection("MeteringWithOverrides"); + + var meteringOptions = new MeteringOptions(); + jsonConfigRoot.Bind("MeteringWithOverrides", meteringOptions); + configurationSection.Value = JsonConvert.SerializeObject(meteringOptions); + + using var exporter = new TestExporter(); + using var reader = new BaseExportingMetricReader(exporter); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices((_, services) => + services.AddOpenTelemetry().WithMetrics(builder => + { + builder.AddMetering(configurationSection) + .AddTestExporter(reader); + })) + .StartAsync(); + + var meterName2 = $"testMeter2{DateTime.Now.Ticks}"; + using var meter2 = new Meter(meterName2); + var counter2 = meter2.CreateCounter("meter2_counter"); + + var meterName3 = $"testMeter3{DateTime.Now.Ticks}"; + using var meter3 = new Meter(meterName3); + var counter3 = meter3.CreateCounter("meter3_counter"); + + var tagList = new TagList + { + new KeyValuePair("dim1", "value1") + }; + + counter2.Add(7, tagList); + counter3.Add(15, tagList); + + reader.Collect(); + Assert.Equal(1, exporter.Metrics.Count); + Assert.Equal(meterName3, exporter.FirstMetric().MeterName); + Assert.Equal("meter3_counter", exporter.FirstMetric().Name); + Assert.Equal(MetricType.LongSum, exporter.FirstMetric().MetricType); + Assert.Equal(15, exporter.FirstMetricPoint().GetSumLong()); + await host.StopAsync(); + } + + [Fact(Skip = "Flaky")] + public async Task AddMetering_MeterStateOverrides_EmptyEntry_ShouldNotMatchAnyCategory() + { + MeterProviderBuilder builder = Sdk.CreateMeterProviderBuilder(); + IConfigurationRoot jsonConfigRoot = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + var configurationSection = jsonConfigRoot.GetSection("MeteringWithOverridesWithEmptyOverride"); + + var meteringOptions = new MeteringOptions(); + jsonConfigRoot.Bind("MeteringWithOverridesWithEmptyOverride", meteringOptions); + configurationSection.Value = JsonConvert.SerializeObject(meteringOptions); + + using var exporter = new TestExporter(); + using var reader = new BaseExportingMetricReader(exporter); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices((_, services) => + services.AddOpenTelemetry().WithMetrics(builder => + { + builder.AddMetering(configurationSection) + .AddTestExporter(reader); + })) + .StartAsync(); + + var meterName = $"testMeter{DateTime.Now.Ticks}"; + using var meter = new Meter(meterName); + var counter = meter.CreateCounter("meter_counter"); + + var tagList = new TagList + { + new KeyValuePair("dim1", "value1") + }; + + counter.Add(7, tagList); + + reader.Collect(); + Assert.Equal(1, exporter.Metrics.Count); + Assert.Equal(meterName, exporter.FirstMetric().MeterName); + Assert.Equal("meter_counter", exporter.FirstMetric().Name); + Assert.Equal(MetricType.LongSum, exporter.FirstMetric().MetricType); + Assert.Equal(7, exporter.FirstMetricPoint().GetSumLong()); + await host.StopAsync(); + } + + [Fact(Skip = "Flaky")] + public async Task AddMetering_MeterStateOverrides_WithOverlappingMatches_AppliesBestMatch() + { + MeterProviderBuilder builder = Sdk.CreateMeterProviderBuilder(); + IConfigurationRoot jsonConfigRoot = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + var configurationSection = jsonConfigRoot.GetSection("MeteringWithOverrides"); + + var meteringOptions = new MeteringOptions(); + jsonConfigRoot.Bind("MeteringWithOverrides", meteringOptions); + configurationSection.Value = JsonConvert.SerializeObject(meteringOptions); + + using var exporter = new TestExporter(); + using var reader = new BaseExportingMetricReader(exporter); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices((_, services) => + services.AddOpenTelemetry().WithMetrics(builder => + { + builder.AddMetering(configurationSection) + .AddTestExporter(reader); + })) + .StartAsync(); + + var meterNameR9Test = $"R9.Test{DateTime.Now.Ticks}"; + using var meter1 = new Meter(meterNameR9Test); + var counter1 = meter1.CreateCounter("counter1"); + counter1.Add(5); + reader.Collect(); + Assert.Equal(0, exporter.Metrics.Count); + Assert.False(IsMetricAvailable(exporter.Metrics, meterNameR9Test, "counter1")); + + var meterNameR9TestInternal = $"R9.Test.Internal{DateTime.Now.Ticks}"; + using var meter2 = new Meter(meterNameR9TestInternal); + var counter2 = meter2.CreateCounter("counter2"); + counter2.Add(16); + reader.Collect(); + Assert.True(IsMetricAvailable(exporter.Metrics, meterNameR9TestInternal, "counter2")); + Assert.Equal(16, exporter.FirstMetricPoint().GetSumLong()); + + var meterNameR9TestExternal = $"R9.Test.External{DateTime.Now.Ticks}"; + using var meter3 = new Meter(meterNameR9TestExternal); + var counter3 = meter3.CreateCounter("counter3"); + counter2.Add(8); + reader.Collect(); + Assert.False(IsMetricAvailable(exporter.Metrics, meterNameR9TestExternal, "counter3")); + + await host.StopAsync(); + } + + [Fact(Skip = "Flaky")] + public async Task MeterState_Disabled_NoMetricsDisabled() + { + using var exporter = new TestExporter(); + using var reader = new BaseExportingMetricReader(exporter); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices((_, services) => + services.AddOpenTelemetry().WithMetrics(builder => + { + builder.AddMetering(options => + { + options.MeterState = MeteringState.Disabled; + }) + .AddTestExporter(reader); + })) + .StartAsync(); + + var meterName = $"testMeter{DateTime.Now.Ticks}"; + using var meter1 = new Meter(meterName); + var counter1 = meter1.CreateCounter("meter1_counter1"); + + var tagList = new TagList + { + new KeyValuePair("dim1", "value1") + }; + + counter1.Add(10, tagList); + reader.Collect(); + Assert.Null(exporter.FirstMetric()); + await host.StopAsync(); + } + + [Fact(Skip = "Flaky")] + public async Task MeterState_Mixed_MetricsForOnlyEnabledMetersAreEmitted() + { + using var exporter = new TestExporter(); + using var reader = new BaseExportingMetricReader(exporter); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices((_, services) => + services.AddOpenTelemetry().WithMetrics(builder => + { + builder.AddMetering(options => + { + options.MeterState = MeteringState.Disabled; + options.MeterStateOverrides.Add(new KeyValuePair("testMeter2", MeteringState.Enabled)); + }) + .AddTestExporter(reader); + })) + .StartAsync(); + + var meterName = $"testMeter{DateTime.Now.Ticks}"; + using var meter1 = new Meter(meterName); + var counter1 = meter1.CreateCounter("meter1_counter1"); + + var meterName2 = $"testMeter2{DateTime.Now.Ticks}"; + using var meter2 = new Meter(meterName2); + var counter2 = meter2.CreateCounter("meter2_counter"); + + var tagList = new TagList + { + new KeyValuePair("dim1", "value1") + }; + + counter1.Add(10, tagList); + + counter2.Add(7, tagList); + reader.Collect(); + Assert.Equal(1, exporter.Metrics.Count); + Assert.Equal(meterName2, exporter.FirstMetric().MeterName); + Assert.Equal("meter2_counter", exporter.FirstMetric().Name); + Assert.Equal(MetricType.LongSum, exporter.FirstMetric().MetricType); + Assert.Equal(7, exporter.FirstMetricPoint().GetSumLong()); + await host.StopAsync(); + } + + [Fact(Skip = "Flaky")] + public async Task LongCounter_SumAsExpected() + { + using var exporter = new TestExporter(); + using var reader = new BaseExportingMetricReader(exporter); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices((_, services) => + services + .AddMetricEnricher() + .AddOpenTelemetry().WithMetrics(builder => + { + builder + .AddMetering() + .AddTestExporter(reader); + })) + .StartAsync(); + + var meterName = $"testMeter{DateTime.Now.Ticks}"; + using var meter1 = new Meter(meterName); + + var counter1 = meter1.CreateCounter("meter1_counter1"); + + var tagList = new TagList + { + new KeyValuePair("dim1", "value1") + }; + + counter1.Add(10, tagList); + reader.Collect(); + Assert.Equal(meterName, exporter.FirstMetric().MeterName); + Assert.Equal("meter1_counter1", exporter.FirstMetric().Name); + Assert.Equal(MetricType.LongSum, exporter.FirstMetric().MetricType); + Assert.Equal(10, exporter.FirstMetricPoint().GetSumLong()); + + counter1.Add(10, tagList); + reader.Collect(); + Assert.Equal(MetricType.LongSum, exporter.FirstMetric().MetricType); + Assert.Equal(20, exporter.FirstMetricPoint().GetSumLong()); + Assert.Equal(tagList.Count, exporter.FirstMetricPoint().Tags.Count); + + var tags = exporter.FirstMetricPoint().Tags; + + foreach (var tag in tags) + { + Assert.Equal(tagList.First().Key, tag.Key); + Assert.Equal(tagList.First().Value, tag.Value); + } + + await host.StopAsync(); + } + + [Fact(Skip = "Flaky")] + public async Task EmitMetric_DoesNotThrowAsync() + { + using var exporter = new TestExporter(); + using var reader = new BaseExportingMetricReader(exporter); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices((_, services) => + services.AddOpenTelemetry().WithMetrics(builder => + { + builder.AddMetering(option => + { + option.MeterState = MeteringState.Disabled; + option.MeterStateOverrides.Add(new KeyValuePair("Microsoft.Extensions", MeteringState.Enabled)); + }) + .AddTestExporter(reader); + })) + .StartAsync(); + + var meterName1 = $"Microsoft.Extensions.Meter1{DateTime.Now.Ticks}"; + var meterName2 = $"Meter2{DateTime.Now.Ticks}"; + using var meter1 = new Meter(meterName1); + using var meter2 = new Meter(meterName2); + + var counter1 = meter1.CreateCounter("meter1_counter1"); + var counter2 = meter2.CreateCounter("meter2_counter1"); + + var tagList = new TagList + { + new KeyValuePair("dim1", "value1") + }; + + counter1.Add(10, tagList); + counter2.Add(50, tagList); + + reader.Collect(); + + Assert.Equal(MetricType.LongSum, exporter.Metrics.First().MetricType); + await host.StopAsync(); + } + + private static bool IsMetricAvailable(Batch metrics, string meterName, string instrumentName) + { + foreach (var metric in metrics) + { + if (metric.MeterName == meterName && metric.Name == instrumentName) + { + return true; + } + } + + return false; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj new file mode 100644 index 0000000000..ce10ec930a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Microsoft.Extensions.Telemetry.Tests.csproj @@ -0,0 +1,37 @@ + + + Microsoft.Extensions.Telemetry + Unit tests for Microsoft.Extensions.Telemetry. + + + + false + false + false + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/HttpHeadersRedactorTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/HttpHeadersRedactorTests.cs new file mode 100644 index 0000000000..089d1cd438 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/HttpHeadersRedactorTests.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Http.Telemetry; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Internal.Test; + +public class HttpHeadersRedactorTests +{ + [Theory] + [ClassData(typeof(HttpHeadersTestData))] + public void Redact_Works_Correctly(IEnumerable input, string expected) + { + var redactorProvider = new FakeRedactorProvider(new FakeRedactorOptions { RedactionFormat = "Redacted:{0}" }); + var headersRedactor = new HttpHeadersRedactor(redactorProvider); + + var actual = headersRedactor.Redact(input, SimpleClassifications.PrivateData); + + actual.Should().Be(expected); + } + + internal class HttpHeadersTestData : TheoryData, string> + { + public HttpHeadersTestData() + { + string longStr = new('z', 312); + + Add(new LinkedList(new List { "aaa", "bbb", "ccc" }), "Redacted:aaa,Redacted:bbb,Redacted:ccc"); + Add(new LinkedList(new List { "aaa", "bbb", null! }), "Redacted:aaa,Redacted:bbb,"); + Add(new LinkedList(new List { "aaa", null!, null! }), "Redacted:aaa,,"); + Add(new LinkedList(new List { null!, null!, null! }), ",,"); + Add(new LinkedList(new List { null! }), string.Empty); + Add(new LinkedList(new List { "aaa" }), "Redacted:aaa"); + Add(new LinkedList(new List()), string.Empty); + Add(new LinkedList(new List { longStr, "bbb", "ccc" }), $"Redacted:{longStr},Redacted:bbb,Redacted:ccc"); + + Add(new[] { "aaa", "bbb", "ccc" }, "Redacted:aaa,Redacted:bbb,Redacted:ccc"); + Add(new[] { "aaa", "bbb", null! }, "Redacted:aaa,Redacted:bbb,"); + Add(new[] { "aaa", null!, null! }, "Redacted:aaa,,"); + Add(new[] { (string)null!, null!, null! }, ",,"); + Add(new[] { (string)null! }, string.Empty); + Add(new[] { "aaa" }, "Redacted:aaa"); + Add(new string[] { }, string.Empty); + Add(null!, TelemetryConstants.Unknown); + Add(new[] { longStr, "bbb", "ccc" }, $"Redacted:{longStr},Redacted:bbb,Redacted:ccc"); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/HttpParserTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/HttpParserTests.cs new file mode 100644 index 0000000000..23f2673081 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/HttpParserTests.cs @@ -0,0 +1,500 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Telemetry; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Internal.Test; + +public class HttpParserTests +{ + [Theory] + [CombinatorialData] + public void TryExtractParameters_NullRouteParametersArray_ReturnsFalse(HttpRouteParameterRedactionMode redactionMode) + { + HttpRouteParser httpParser = CreateHttpRouteParser(); + Dictionary parametersToRedact = new() { { "chatId", SimpleClassifications.PrivateData } }; + + string httpPath = "api/routes/routeId123/chats/chatId123"; + string httpRoute = "/api/routes/{routeId}/chats/{chatId}"; + + var routeSegments = httpParser.ParseRoute(httpRoute); + + HttpRouteParameter[] httpRouteParameters = null!; + var success = httpParser.TryExtractParameters(httpPath, routeSegments, redactionMode, parametersToRedact, ref httpRouteParameters); + Assert.False(success); + } + + [Theory] + [CombinatorialData] + public void TryExtractParameters_RouteParametersArraySmallerThanActualParamCount_ReturnsFalse(HttpRouteParameterRedactionMode redactionMode) + { + HttpRouteParser httpParser = CreateHttpRouteParser(); + Dictionary parametersToRedact = new() { { "chatId", SimpleClassifications.PrivateData } }; + + string httpPath = "api/routes/routeId123/chats/chatId123"; + string httpRoute = "/api/routes/{routeId}/chats/{chatId}"; + + var routeSegments = httpParser.ParseRoute(httpRoute); + + HttpRouteParameter[] httpRouteParameters = new HttpRouteParameter[1]; + var success = httpParser.TryExtractParameters(httpPath, routeSegments, redactionMode, parametersToRedact, ref httpRouteParameters); + Assert.False(success); + } + + [Theory] + [InlineData(-1)] + [InlineData(3)] + [InlineData(4)] + public void TryExtractParameters_InvalidHttpRouteParameterRedactionMode_Throws(int redactionMode) + { + HttpRouteParser httpParser = CreateHttpRouteParser(); + Dictionary parametersToRedact = new() { { "routeId", SimpleClassifications.PrivateData } }; + + string httpPath = "routeId123/chats/chatId123/"; + string httpRoute = "{routeId}/chats/{chatId}/"; + var routeSegments = httpParser.ParseRoute(httpRoute); + + HttpRouteParameter[] httpRouteParameters = new HttpRouteParameter[2]; + var ex = Assert.Throws( + () => httpParser.TryExtractParameters(httpPath, routeSegments, (HttpRouteParameterRedactionMode)redactionMode, parametersToRedact, ref httpRouteParameters)); + Assert.Equal(TelemetryCommonExtensions.UnsupportedEnumValueExceptionMessage, ex.Message); + } + + [Theory] + [CombinatorialData] + public void TryExtractParameters_RouteHasFirstParameterToBeRedacted_ReturnsCorrectParameters(HttpRouteParameterRedactionMode redactionMode) + { + bool isRedacted = redactionMode != HttpRouteParameterRedactionMode.None; + string redactedPrefix = isRedacted ? "Redacted:" : string.Empty; + + HttpRouteParser httpParser = CreateHttpRouteParser(); + Dictionary parametersToRedact = new() { { "routeId", SimpleClassifications.PrivateData } }; + + string httpPath = "routeId123/chats/chatId123/"; + string httpRoute = "{routeId}/chats/{chatId}/"; + var routeSegments = httpParser.ParseRoute(httpRoute); + + HttpRouteParameter[] httpRouteParameters = new HttpRouteParameter[2]; + var success = httpParser.TryExtractParameters(httpPath, routeSegments, redactionMode, parametersToRedact, ref httpRouteParameters); + + ValidateRouteParameter(httpRouteParameters[0], "routeId", $"{redactedPrefix}routeId123", isRedacted); + + if (redactionMode == HttpRouteParameterRedactionMode.Strict) + { + ValidateRouteParameter(httpRouteParameters[1], "chatId", TelemetryConstants.Redacted, true); + } + + if (redactionMode == HttpRouteParameterRedactionMode.Loose) + { + ValidateRouteParameter(httpRouteParameters[1], "chatId", "chatId123", false); + } + + // route begins with forward slash + httpPath = "routeId123/chats/chatId123/"; + httpRoute = "/{routeId}/chats/{chatId}/"; + routeSegments = httpParser.ParseRoute(httpRoute); + + httpParser.TryExtractParameters(httpPath, routeSegments, redactionMode, parametersToRedact, ref httpRouteParameters); + + ValidateRouteParameter(httpRouteParameters[0], "routeId", $"{redactedPrefix}routeId123", isRedacted); + + if (redactionMode == HttpRouteParameterRedactionMode.Strict) + { + ValidateRouteParameter(httpRouteParameters[1], "chatId", TelemetryConstants.Redacted, true); + } + + if (redactionMode == HttpRouteParameterRedactionMode.Loose) + { + ValidateRouteParameter(httpRouteParameters[1], "chatId", "chatId123", false); + } + } + + [Theory] + [CombinatorialData] + public void TryExtractParameters_NoParameters_ReturnEmptyArray(HttpRouteParameterRedactionMode redactionMode) + { + HttpRouteParser httpParser = CreateHttpRouteParser(); + Dictionary parametersToRedact = new() { { "routeId", SimpleClassifications.PrivateData } }; + + string httpPath = "users/chats/messages"; + string httpRoute = "users/chats/messages"; + var routeSegments = httpParser.ParseRoute(httpRoute); + HttpRouteParameter[] httpRouteParameters = new HttpRouteParameter[0]; + var success = httpParser.TryExtractParameters(httpPath, routeSegments, redactionMode, parametersToRedact, ref httpRouteParameters); + Assert.Empty(httpRouteParameters); + + httpPath = "/"; + httpRoute = "/"; + routeSegments = httpParser.ParseRoute(httpRoute); + httpRouteParameters = new HttpRouteParameter[0]; + success = httpParser.TryExtractParameters(httpPath, routeSegments, redactionMode, parametersToRedact, ref httpRouteParameters); + Assert.Empty(httpRouteParameters); + + httpPath = ""; + httpRoute = "/"; + routeSegments = httpParser.ParseRoute(httpRoute); + httpRouteParameters = new HttpRouteParameter[0]; + success = httpParser.TryExtractParameters(httpPath, routeSegments, redactionMode, parametersToRedact, ref httpRouteParameters); + Assert.Empty(httpRouteParameters); + } + + [Theory] + [CombinatorialData] + public void TryExtractParameters_ReturnsExpectedParameters(HttpRouteParameterRedactionMode redactionMode) + { + bool isRedacted = redactionMode != HttpRouteParameterRedactionMode.None; + string redactedPrefix = isRedacted ? "Redacted:" : string.Empty; + + HttpRouteParser httpParser = CreateHttpRouteParser(); + Dictionary parametersToRedact = new() { { "chatId", SimpleClassifications.PrivateData } }; + + // Route ends with text + string httpPath = "api/routes/routeId123/chats/chatId123"; + string httpRoute = "/api/routes/{routeId}/chats/{chatId}"; + + var routeSegments = httpParser.ParseRoute(httpRoute); + + HttpRouteParameter[] httpRouteParameters = new HttpRouteParameter[2]; + var success = httpParser.TryExtractParameters(httpPath, routeSegments, redactionMode, parametersToRedact, ref httpRouteParameters); + Assert.True(success); + + if (redactionMode == HttpRouteParameterRedactionMode.Strict) + { + ValidateRouteParameter(httpRouteParameters[0], "routeId", TelemetryConstants.Redacted, true); + } + + if (redactionMode == HttpRouteParameterRedactionMode.Loose) + { + ValidateRouteParameter(httpRouteParameters[0], "routeId", "routeId123", false); + } + + ValidateRouteParameter(httpRouteParameters[1], "chatId", $"{redactedPrefix}chatId123", isRedacted); + + // Route ends with parameter that needs to be redacted + httpPath = "api/routes/routeId123/chats/chatId123/"; + httpRoute = "/api/routes/{routeId}/chats/{chatId}/"; + success = httpParser.TryExtractParameters(httpPath, routeSegments, redactionMode, parametersToRedact, ref httpRouteParameters); + Assert.True(success); + + if (redactionMode == HttpRouteParameterRedactionMode.Strict) + { + ValidateRouteParameter(httpRouteParameters[0], "routeId", TelemetryConstants.Redacted, true); + } + + if (redactionMode == HttpRouteParameterRedactionMode.Loose) + { + ValidateRouteParameter(httpRouteParameters[0], "routeId", "routeId123", false); + } + + ValidateRouteParameter(httpRouteParameters[1], "chatId", $"{redactedPrefix}chatId123", isRedacted); + + // Route ends with parameter that doesn't need to be redacted + parametersToRedact.Add("routeId", SimpleClassifications.PrivateData); + parametersToRedact.Remove("chatId"); + httpPath = "api/routes/routeId123/chats/chatId123"; + httpRoute = "/api/routes/{routeId}/chats/{chatId}"; + success = httpParser.TryExtractParameters(httpPath, routeSegments, redactionMode, parametersToRedact, ref httpRouteParameters); + Assert.True(success); + + ValidateRouteParameter(httpRouteParameters[0], "routeId", $"{redactedPrefix}routeId123", isRedacted); + + if (redactionMode == HttpRouteParameterRedactionMode.Strict) + { + ValidateRouteParameter(httpRouteParameters[1], "chatId", TelemetryConstants.Redacted, true); + } + + if (redactionMode == HttpRouteParameterRedactionMode.Loose) + { + ValidateRouteParameter(httpRouteParameters[1], "chatId", "chatId123", false); + } + } + + [Theory] + [CombinatorialData] + public void TryExtractParameters_WithSomeParametersWithPublicNonPersonalData_ReturnsExpectedParameters(HttpRouteParameterRedactionMode redactionMode) + { + bool isRedacted = redactionMode != HttpRouteParameterRedactionMode.None; + string redactedPrefix = isRedacted ? "Redacted:" : string.Empty; + + HttpRouteParser httpParser = CreateHttpRouteParser(); + Dictionary parametersToRedact = new() { { "chatId", SimpleClassifications.PrivateData } }; + + // Route ends with text + string httpPath = "api/routes/routeId123/chats/chatId123"; + string httpRoute = "/api/routes/{routeId}/chats/{chatId}"; + + var routeSegments = httpParser.ParseRoute(httpRoute); + + HttpRouteParameter[] httpRouteParameters = new HttpRouteParameter[2]; + var success = httpParser.TryExtractParameters(httpPath, routeSegments, redactionMode, parametersToRedact, ref httpRouteParameters); + Assert.True(success); + + if (redactionMode == HttpRouteParameterRedactionMode.Strict) + { + ValidateRouteParameter(httpRouteParameters[0], "routeId", TelemetryConstants.Redacted, true); + } + + if (redactionMode == HttpRouteParameterRedactionMode.Loose) + { + ValidateRouteParameter(httpRouteParameters[0], "routeId", "routeId123", false); + } + + ValidateRouteParameter(httpRouteParameters[1], "chatId", $"{redactedPrefix}chatId123", isRedacted); + + // Route ends with parameter that needs to be redacted + parametersToRedact.Add("routeId", DataClassification.None); + httpPath = "api/routes/routeId123/chats/chatId123/"; + httpRoute = "/api/routes/{routeId}/chats/{chatId}/"; + success = httpParser.TryExtractParameters(httpPath, routeSegments, redactionMode, parametersToRedact, ref httpRouteParameters); + Assert.True(success); + + ValidateRouteParameter(httpRouteParameters[0], "routeId", "routeId123", false); + ValidateRouteParameter(httpRouteParameters[1], "chatId", $"{redactedPrefix}chatId123", isRedacted); + + // Route ends with parameter that doesn't need to be redacted + parametersToRedact.Remove("chatId"); + parametersToRedact.Add("chatId", DataClassification.None); + httpPath = "api/routes/routeId123/chats/chatId123"; + httpRoute = "/api/routes/{routeId}/chats/{chatId}"; + success = httpParser.TryExtractParameters(httpPath, routeSegments, redactionMode, parametersToRedact, ref httpRouteParameters); + Assert.True(success); + + ValidateRouteParameter(httpRouteParameters[0], "routeId", "routeId123", false); + ValidateRouteParameter(httpRouteParameters[1], "chatId", "chatId123", false); + } + + [Theory] + [CombinatorialData] + public void TryExtractParameters_WhenRouteHasDefaultParameters_ReturnsExpectedParameters(HttpRouteParameterRedactionMode redactionMode) + { + bool isRedacted = redactionMode != HttpRouteParameterRedactionMode.None; + string redactedPrefix = isRedacted ? "Redacted:" : string.Empty; + + HttpRouteParser httpParser = CreateHttpRouteParser(); + Dictionary parametersToRedact = new() { { "filter", SimpleClassifications.PrivateData } }; + HttpRouteParameter[] httpRouteParameters = new HttpRouteParameter[3]; + + const string HttpRoute = "{controller=home}/{action=index}/{filter=all}"; + ParsedRouteSegments routeSegments = httpParser.ParseRoute(HttpRoute); + + // An http path includes well known "controller" and "action" parameters, and a parameter "filter". + // Well known parameters are not redacted, a parameter with default value is redacted. + string httpPath = "users/list/top10"; + + bool success = httpParser.TryExtractParameters( + httpPath, routeSegments, redactionMode, parametersToRedact, ref httpRouteParameters); + Assert.True(success); + + ValidateRouteParameter(httpRouteParameters[0], "controller", "users", false); + ValidateRouteParameter(httpRouteParameters[1], "action", "list", false); + ValidateRouteParameter(httpRouteParameters[2], "filter", $"{redactedPrefix}top10", isRedacted); + + // An http path doesn't include some of the optional parameters. + // Well known parameters are not redacted, a missing parameter with default value is not redacted. + httpPath = "users"; + + success = httpParser.TryExtractParameters( + httpPath, routeSegments, redactionMode, parametersToRedact, ref httpRouteParameters); + Assert.True(success); + + ValidateRouteParameter(httpRouteParameters[0], "controller", "users", false); + ValidateRouteParameter(httpRouteParameters[1], "action", "index", false); + ValidateRouteParameter(httpRouteParameters[2], "filter", "all", false); + + // An http path doesn't include all optional parameters. + // Well known parameters are not redacted, + // a missing parameter with default value is not redacted. + httpPath = ""; + + success = httpParser.TryExtractParameters( + httpPath, routeSegments, redactionMode, parametersToRedact, ref httpRouteParameters); + Assert.True(success); + + ValidateRouteParameter(httpRouteParameters[0], "controller", "home", false); + ValidateRouteParameter(httpRouteParameters[1], "action", "index", false); + ValidateRouteParameter(httpRouteParameters[2], "filter", "all", false); + + // A well known parameter is redacted when it is explicitly specified in an http path, + // and is not redacted when it is omitted. + parametersToRedact.Add("controller", SimpleClassifications.PrivateData); + parametersToRedact.Add("action", SimpleClassifications.PrivateData); + + httpPath = "users"; + + success = httpParser.TryExtractParameters( + httpPath, routeSegments, redactionMode, parametersToRedact, ref httpRouteParameters); + Assert.True(success); + + ValidateRouteParameter(httpRouteParameters[0], "controller", $"{redactedPrefix}users", isRedacted); + ValidateRouteParameter(httpRouteParameters[1], "action", "index", false); + ValidateRouteParameter(httpRouteParameters[2], "filter", "all", false); + } + + [Theory] + [CombinatorialData] + public void TryExtractParameters_WhenRouteHasOptionalsAndConstraints_ReturnsExpectedParameters(HttpRouteParameterRedactionMode redactionMode) + { + bool isRedacted = redactionMode != HttpRouteParameterRedactionMode.None; + string redactedPrefix = isRedacted ? "Redacted:" : string.Empty; + + HttpRouteParser httpParser = CreateHttpRouteParser(); + Dictionary parametersToRedact = new() + { + { "routeId", SimpleClassifications.PrivateData }, + { "chatId", SimpleClassifications.PrivateData }, + }; + HttpRouteParameter[] httpRouteParameters = new HttpRouteParameter[2]; + + const string HttpRoute = "api/routes/{routeId:int:min(1)}/chats/{chatId?}"; + ParsedRouteSegments routeSegments = httpParser.ParseRoute(HttpRoute); + + // An http path includes a parameter with a constraint and an optional parameter. + // Both parameters are redacted. + string httpPath = "api/routes/routeId123/chats/chatId123"; + + bool success = httpParser.TryExtractParameters( + httpPath, routeSegments, redactionMode, parametersToRedact, ref httpRouteParameters); + Assert.True(success); + + ValidateRouteParameter(httpRouteParameters[0], "routeId", $"{redactedPrefix}routeId123", isRedacted); + ValidateRouteParameter(httpRouteParameters[1], "chatId", $"{redactedPrefix}chatId123", isRedacted); + + // An http path includes a parameter with a constraint, an optional parameter is not provided. + // The parameter with constraint is redacted, the optional parameter isn't. + httpPath = "api/routes/routeId123/chats"; + + success = httpParser.TryExtractParameters( + httpPath, routeSegments, redactionMode, parametersToRedact, ref httpRouteParameters); + Assert.True(success); + + ValidateRouteParameter(httpRouteParameters[0], "routeId", $"{redactedPrefix}routeId123", isRedacted); + ValidateRouteParameter(httpRouteParameters[1], "chatId", "", false); + } + + [Fact] + public void ParseRoute_WithRouteParameter_ReturnsRouteSegments() + { + HttpRouteParser httpParser = CreateHttpRouteParser(); + + // An http route has parameters and ends with a parameter. + string httpRoute = "/api/routes/{routeId}/chats/{chatId}"; + var routeSegments = httpParser.ParseRoute(httpRoute); + + Assert.Equal(4, routeSegments.Segments.Length); + Assert.Equal("api/routes/{routeId}/chats/{chatId}", routeSegments.RouteTemplate); + + ValidateRouteSegment(routeSegments.Segments[0], "api/routes/", false, "", "", 0, 11); + ValidateRouteSegment(routeSegments.Segments[1], "routeId", true, "routeId", "", 11, 20); + ValidateRouteSegment(routeSegments.Segments[2], "/chats/", false, "", "", 20, 27); + ValidateRouteSegment(routeSegments.Segments[3], "chatId", true, "chatId", "", 27, 35); + + // An http route has parameters and ends with text. + httpRoute = "/api/routes/{routeId}/chats/{chatId}/messages"; + routeSegments = httpParser.ParseRoute(httpRoute); + + Assert.Equal(5, routeSegments.Segments.Length); + Assert.Equal("api/routes/{routeId}/chats/{chatId}/messages", routeSegments.RouteTemplate); + + ValidateRouteSegment(routeSegments.Segments[0], "api/routes/", false, "", "", 0, 11); + ValidateRouteSegment(routeSegments.Segments[1], "routeId", true, "routeId", "", 11, 20); + ValidateRouteSegment(routeSegments.Segments[2], "/chats/", false, "", "", 20, 27); + ValidateRouteSegment(routeSegments.Segments[3], "chatId", true, "chatId", "", 27, 35); + ValidateRouteSegment(routeSegments.Segments[4], "/messages", false, "", "", 35, 44); + } + + [Fact] + public void ParseRoute_WithQueryParameter_ReturnRouteSegmentExcludingQueryParams() + { + HttpRouteParser httpParser = CreateHttpRouteParser(); + + string httpRoute = "/api/routes/{routeId}/chats/{chatId}/messages?from=7"; + var routeSegments = httpParser.ParseRoute(httpRoute); + + Assert.Equal(5, routeSegments.Segments.Length); + Assert.Equal("api/routes/{routeId}/chats/{chatId}/messages", routeSegments.RouteTemplate); + + ValidateRouteSegment(routeSegments.Segments[0], "api/routes/", false, "", "", 0, 11); + ValidateRouteSegment(routeSegments.Segments[1], "routeId", true, "routeId", "", 11, 20); + ValidateRouteSegment(routeSegments.Segments[2], "/chats/", false, "", "", 20, 27); + ValidateRouteSegment(routeSegments.Segments[3], "chatId", true, "chatId", "", 27, 35); + ValidateRouteSegment(routeSegments.Segments[4], "/messages", false, "", "", 35, 44); + + // Route doesn't start with forward slash, the final result should begin with forward slash. + httpRoute = "api/routes/{routeId}/chats/{chatId}/messages?from=7"; + routeSegments = httpParser.ParseRoute(httpRoute); + + Assert.Equal(5, routeSegments.Segments.Length); + Assert.Equal("api/routes/{routeId}/chats/{chatId}/messages", routeSegments.RouteTemplate); + + ValidateRouteSegment(routeSegments.Segments[0], "api/routes/", false, "", "", 0, 11); + ValidateRouteSegment(routeSegments.Segments[1], "routeId", true, "routeId", "", 11, 20); + ValidateRouteSegment(routeSegments.Segments[2], "/chats/", false, "", "", 20, 27); + ValidateRouteSegment(routeSegments.Segments[3], "chatId", true, "chatId", "", 27, 35); + ValidateRouteSegment(routeSegments.Segments[4], "/messages", false, "", "", 35, 44); + } + + [Fact] + public void ParseRoute_WhenRouteHasDefaultsOptionalsConstraints_ReturnsRouteSegments() + { + HttpRouteParser httpParser = CreateHttpRouteParser(); + + string httpRoute = "api/{controller=home}/{action=index}/{routeId:int:min(1)}/{chatId?}"; + ParsedRouteSegments routeSegments = httpParser.ParseRoute(httpRoute); + + Assert.Equal(8, routeSegments.Segments.Length); + Assert.Equal(httpRoute, routeSegments.RouteTemplate); + + ValidateRouteSegment(routeSegments.Segments[0], "api/", false, "", "", 0, 4); + ValidateRouteSegment(routeSegments.Segments[1], "controller=home", true, "controller", "home", 4, 21); + ValidateRouteSegment(routeSegments.Segments[2], "/", false, "", "", 21, 22); + ValidateRouteSegment(routeSegments.Segments[3], "action=index", true, "action", "index", 22, 36); + ValidateRouteSegment(routeSegments.Segments[4], "/", false, "", "", 36, 37); + ValidateRouteSegment(routeSegments.Segments[5], "routeId:int:min(1)", true, "routeId", "", 37, 57); + ValidateRouteSegment(routeSegments.Segments[6], "/", false, "", "", 57, 58); + ValidateRouteSegment(routeSegments.Segments[7], "chatId?", true, "chatId", "", 58, 67); + } + + [Fact] + public void AddHttpRouteProcessor_ParserAndFormatterInstanceAdded() + { + var sp = new ServiceCollection().AddHttpRouteProcessor().AddFakeRedaction().BuildServiceProvider(); + + var httpRouteParser = sp.GetRequiredService(); + var httpRouteFormatter = sp.GetRequiredService(); + + Assert.NotNull(httpRouteParser); + Assert.NotNull(httpRouteFormatter); + } + + private static HttpRouteParser CreateHttpRouteParser() + { + var redactorProvider = new FakeRedactorProvider( + new FakeRedactorOptions { RedactionFormat = "Redacted:{0}" }); + return new HttpRouteParser(redactorProvider); + } + + private static void ValidateRouteParameter( + HttpRouteParameter parameter, string name, string value, bool isRedacted) + { + Assert.Equal(name, parameter.Name); + Assert.Equal(value, parameter.Value); + Assert.Equal(isRedacted, parameter.IsRedacted); + } + + private static void ValidateRouteSegment( + Segment segment, string content, bool isParam, string paramName, string defaultValue, int start, int end) + { + Assert.Equal(content, segment.Content); + Assert.Equal(isParam, segment.IsParam); + Assert.Equal(paramName, segment.ParamName); + Assert.Equal(defaultValue, segment.DefaultValue); + Assert.Equal(start, segment.Start); + Assert.Equal(end, segment.End); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/HttpRouteFormatterTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/HttpRouteFormatterTests.cs new file mode 100644 index 0000000000..49efba74dc --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/HttpRouteFormatterTests.cs @@ -0,0 +1,502 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.Http.Telemetry; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Internal.Test; + +public class HttpRouteFormatterTests +{ + [Theory] + [CombinatorialData] + public void Format_WithEmptyParametersToRedact_ReturnsOriginalPath(HttpRouteParameterRedactionMode redactionMode) + { + HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter(); + var parametersToRedact = new Dictionary(); + + string httpPath = "/api/routes/routeId123/chats/chatId123"; + string httpRoute = "/api/routes/{routeId}/chats/{chatId}"; + string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + + if (redactionMode == HttpRouteParameterRedactionMode.Strict) + { + Assert.Equal($"api/routes/{TelemetryConstants.Redacted}/chats/{TelemetryConstants.Redacted}", formattedPath); + } + else + { + Assert.Equal($"api/routes/routeId123/chats/chatId123", formattedPath); + } + + httpPath = "api/routes/routeId123/chats/chatId123"; + httpRoute = "api/routes/{routeId}/chats/{chatId}"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + + if (redactionMode == HttpRouteParameterRedactionMode.Strict) + { + Assert.Equal($"api/routes/{TelemetryConstants.Redacted}/chats/{TelemetryConstants.Redacted}", formattedPath); + } + else + { + Assert.Equal($"api/routes/routeId123/chats/chatId123", formattedPath); + } + + httpPath = "api/routes/routeId123/chats/chatId123"; + httpRoute = "api/routes/{routeId}/chats/{chatId}"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + + if (redactionMode == HttpRouteParameterRedactionMode.Strict) + { + Assert.Equal($"api/routes/{TelemetryConstants.Redacted}/chats/{TelemetryConstants.Redacted}", formattedPath); + } + else + { + Assert.Equal($"api/routes/routeId123/chats/chatId123", formattedPath); + } + + httpPath = "/api/chats:chatId123@routeId123"; + httpRoute = "/api/chats:{chatId}@{routeId}"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + + if (redactionMode == HttpRouteParameterRedactionMode.Strict) + { + Assert.Equal($"api/chats:{TelemetryConstants.Redacted}@{TelemetryConstants.Redacted}", formattedPath); + } + else + { + Assert.Equal($"api/chats:chatId123@routeId123", formattedPath); + } + + httpPath = "/api/chats:chatId123@routeId123/messages"; + httpRoute = "/api/chats:{chatId}@{routeId}/messages"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + + if (redactionMode == HttpRouteParameterRedactionMode.Strict) + { + Assert.Equal($"api/chats:{TelemetryConstants.Redacted}@{TelemetryConstants.Redacted}/messages", formattedPath); + } + else + { + Assert.Equal($"api/chats:chatId123@routeId123/messages", formattedPath); + } + } + + [Theory] + [CombinatorialData] + public void Format_NoParameterRoute_WithParametersToRedact_ReturnsOriginalpath(HttpRouteParameterRedactionMode redactionMode) + { + HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter(); + Dictionary parametersToRedact = new() + { + { "userId", SimpleClassifications.PrivateData }, + { "v1", SimpleClassifications.PrivateData } + }; + + string httpPath = "/api/v1/chats"; + string httpRoute = "/api/v1/chats"; + string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal(httpPath.TrimStart('/'), formattedPath); + + // http path doesn't begin with / while route begins with / + httpPath = "api/v1/chats"; + httpRoute = "/api/v1/chats"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal(httpPath.TrimStart('/'), formattedPath); + + // route doesn't begin with / while http path begins with / + httpPath = "/api/v1/chats"; + httpRoute = "api/v1/chats"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal(httpPath.TrimStart('/'), formattedPath); + + // empty route + httpPath = "/"; + httpRoute = "/"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal(httpPath.TrimStart('/'), formattedPath); + + // empty route + httpPath = ""; + httpRoute = "/"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal(httpPath.TrimStart('/'), formattedPath); + } + + [Theory] + [InlineData(-1)] + [InlineData(3)] + [InlineData(4)] + public void Format_WithParametersToRedact_GivenInvalidHttpRouteParameterRedactionMode_Throws(int redactionMode) + { + HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter(); + Dictionary parametersToRedact = new() + { + { "userId", SimpleClassifications.PrivateData }, + { "routeId", SimpleClassifications.PrivateData } + }; + + string httpPath = "/api/routes/routeId123/chats/chatId123"; + string httpRoute = "/api/routes/{routeId}/chats/{chatId}"; + var ex = Assert.Throws( + () => httpFormatter.Format(httpRoute, httpPath, (HttpRouteParameterRedactionMode)redactionMode, parametersToRedact)); + Assert.Equal(TelemetryCommonExtensions.UnsupportedEnumValueExceptionMessage, ex.Message); + } + + [Theory] + [CombinatorialData] + public void Format_WithParametersToRedact_ReturnsPathWithSensitiveParamsRedacted(HttpRouteParameterRedactionMode redactionMode) + { + bool isRedacted = redactionMode != HttpRouteParameterRedactionMode.None; + string redactedPrefix = isRedacted ? "Redacted:" : string.Empty; + + HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter(); + Dictionary parametersToRedact = new() + { + { "userId", SimpleClassifications.PrivateData }, + { "routeId", SimpleClassifications.PrivateData } + }; + + string httpPath = "/api/routes/routeId123/chats/chatId123"; + string httpRoute = "/api/routes/{routeId}/chats/{chatId}"; + string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + + if (redactionMode == HttpRouteParameterRedactionMode.Strict) + { + Assert.Equal($"api/routes/Redacted:routeId123/chats/{TelemetryConstants.Redacted}", formattedPath); + } + + if (redactionMode == HttpRouteParameterRedactionMode.Loose) + { + Assert.Equal($"api/routes/Redacted:routeId123/chats/chatId123", formattedPath); + } + + if (redactionMode == HttpRouteParameterRedactionMode.None) + { + Assert.Equal($"api/routes/routeId123/chats/chatId123", formattedPath); + } + + parametersToRedact.Add("chatId", SimpleClassifications.PrivateData); + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal($"api/routes/{redactedPrefix}routeId123/chats/{redactedPrefix}chatId123", formattedPath); + + // path doesn't begin with forward slash, route does + httpPath = "api/routes/routeId123/chats/chatId123"; + httpRoute = "/api/routes/{routeId}/chats/{chatId}"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal($"api/routes/{redactedPrefix}routeId123/chats/{redactedPrefix}chatId123", formattedPath); + + // route doesn't begin with forward slash, path does + httpPath = "/api/routes/routeId123/chats/chatId123"; + httpRoute = "api/routes/{routeId}/chats/{chatId}"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal($"api/routes/{redactedPrefix}routeId123/chats/{redactedPrefix}chatId123", formattedPath); + + // route has no parameters that needs redaction + parametersToRedact.Remove("routeId"); + parametersToRedact.Remove("chatId"); + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + + if (redactionMode == HttpRouteParameterRedactionMode.Strict) + { + Assert.Equal($"api/routes/{TelemetryConstants.Redacted}/chats/{TelemetryConstants.Redacted}", formattedPath); + } + else + { + Assert.Equal($"api/routes/routeId123/chats/chatId123", formattedPath); + } + } + + [Theory] + [CombinatorialData] + public void Format_WithParametersToRedact_DataClassPublicNonPersonalData_DoesNotRedactParameters(HttpRouteParameterRedactionMode redactionMode) + { + HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter(); + Dictionary parametersToRedact = new() + { + { "userId", DataClassification.None }, + { "routeId", DataClassification.None }, + { "chatId", DataClassification.None }, + }; + + string httpPath = "/api/routes/routeId123/chats/chatId123"; + string httpRoute = "/api/routes/{routeId}/chats/{chatId}"; + string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal($"api/routes/routeId123/chats/chatId123", formattedPath); + } + + [Theory] + [CombinatorialData] + public void Format_WithParametersToRedact_PublicNonPersonalData_NotRedacted(HttpRouteParameterRedactionMode redactionMode) + { + bool isRedacted = redactionMode != HttpRouteParameterRedactionMode.None; + string redactedPrefix = isRedacted ? "Redacted:" : string.Empty; + + HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter(); + Dictionary parametersToRedact = new() + { + { "userId", SimpleClassifications.PrivateData }, + { "routeId", DataClassification.None }, + { "chatId", SimpleClassifications.PrivateData }, + }; + + string httpPath = "/api/routes/routeId123/chats/chatId123"; + string httpRoute = "/api/routes/{routeId}/chats/{chatId}"; + string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal($"api/routes/routeId123/chats/{redactedPrefix}chatId123", formattedPath); + } + + [Theory] + [CombinatorialData] + public void Format_RouteHasFirstParameterToBeRedacted_ReturnsCorrectlyRedactedPath(HttpRouteParameterRedactionMode redactionMode) + { + HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter(); + Dictionary parametersToRedact = new() { { "routeId", SimpleClassifications.PrivateData } }; + + string httpPath = "routeId123/chats/chatId123"; + string httpRoute = "{routeId}/chats/{chatId}"; + string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + + if (redactionMode == HttpRouteParameterRedactionMode.Strict) + { + Assert.Equal($"Redacted:routeId123/chats/{TelemetryConstants.Redacted}", formattedPath); + } + + if (redactionMode == HttpRouteParameterRedactionMode.Loose) + { + Assert.Equal($"Redacted:routeId123/chats/chatId123", formattedPath); + } + + if (redactionMode == HttpRouteParameterRedactionMode.None) + { + Assert.Equal($"routeId123/chats/chatId123", formattedPath); + } + + // path begins with forward slash, route doesn't + httpPath = "/routeId123/chats/chatId123"; + httpRoute = "{routeId}/chats/{chatId}"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + + if (redactionMode == HttpRouteParameterRedactionMode.Strict) + { + Assert.Equal($"Redacted:routeId123/chats/{TelemetryConstants.Redacted}", formattedPath); + } + + if (redactionMode == HttpRouteParameterRedactionMode.Loose) + { + Assert.Equal($"Redacted:routeId123/chats/chatId123", formattedPath); + } + + if (redactionMode == HttpRouteParameterRedactionMode.None) + { + Assert.Equal($"routeId123/chats/chatId123", formattedPath); + } + + // route begins with forward slash, path doesn't + httpPath = "routeId123/chats/chatId123"; + httpRoute = "/{routeId}/chats/{chatId}"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + + if (redactionMode == HttpRouteParameterRedactionMode.Strict) + { + Assert.Equal($"Redacted:routeId123/chats/{TelemetryConstants.Redacted}", formattedPath); + } + + if (redactionMode == HttpRouteParameterRedactionMode.Loose) + { + Assert.Equal($"Redacted:routeId123/chats/chatId123", formattedPath); + } + + if (redactionMode == HttpRouteParameterRedactionMode.None) + { + Assert.Equal($"routeId123/chats/chatId123", formattedPath); + } + } + + [Theory] + [CombinatorialData] + public void Format_RouteHasLastParameterToBeRedacted_ReturnsCorrectlyRedactedPath(HttpRouteParameterRedactionMode redactionMode) + { + HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter(); + Dictionary parametersToRedact = new() { { "chatId", SimpleClassifications.PrivateData } }; + + string httpPath = "/api/routes/routeId123/chats/chatId123"; + string httpRoute = "/api/routes/{routeId}/chats/{chatId}"; + string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + + if (redactionMode == HttpRouteParameterRedactionMode.Strict) + { + Assert.Equal($"api/routes/{TelemetryConstants.Redacted}/chats/Redacted:chatId123", formattedPath); + } + + if (redactionMode == HttpRouteParameterRedactionMode.Loose) + { + Assert.Equal($"api/routes/routeId123/chats/Redacted:chatId123", formattedPath); + } + + if (redactionMode == HttpRouteParameterRedactionMode.None) + { + Assert.Equal($"api/routes/routeId123/chats/chatId123", formattedPath); + } + } + + [Theory] + [CombinatorialData] + public void Format_RouteHasDefaultParametersToBeRedacted_ReturnsCorrectlyRedactedPath(HttpRouteParameterRedactionMode redactionMode) + { + string redactedPrefix = redactionMode == HttpRouteParameterRedactionMode.None ? string.Empty : "Redacted:"; + + HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter(); + Dictionary parametersToRedact = new() { { "routeId", SimpleClassifications.PrivateData } }; + string httpRoute = "/api/routes/{routeId=defaultRoute}"; + + // A default parameter is redacted when it is explicitly specified. + string httpPath = "/api/routes/routeId123"; + string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal($"api/routes/{redactedPrefix}routeId123", formattedPath); + + // An http path is correctly formatted if a default parameter is omitted. + httpPath = "/api/routes"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal("api/routes", formattedPath); + } + + [Theory] + [CombinatorialData] + public void Format_RouteHasWellKnownParameters_ReturnsCorrectlyFormattedPath(HttpRouteParameterRedactionMode redactionMode) + { + string redactedPrefix = redactionMode == HttpRouteParameterRedactionMode.None ? string.Empty : "Redacted:"; + HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter(); + Dictionary parametersToRedact = new() { { "filter", SimpleClassifications.PrivateData } }; + string httpRoute = "{controller=home}/{action=index}/{filter=all}"; + + // An http path includes well known "controller" and "action" parameters, and a parameter "filter". + // Well known parameters are not redacted, a parameter with default value is redacted. + string httpPath = "users/list/top10"; + string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal($"users/list/{redactedPrefix}top10", formattedPath); + + // An http path doesn't include an optional parameter with a default value. + // Well known parameters are not redacted. + httpPath = "users/list"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal("users/list", formattedPath); + + // An http path includes only one optional well known parameter. + // Well known parameters are not redacted. + httpPath = "users"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal("users", formattedPath); + + // An http path doesn't include all optional parameters. + httpPath = ""; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal("", formattedPath); + + // A well known parameter is redacted when it is explicitly specified in an http path, + // and is not redacted when it is omitted. + parametersToRedact.Add("controller", SimpleClassifications.PrivateData); + parametersToRedact.Add("action", SimpleClassifications.PrivateData); + + httpPath = "users"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal($"{redactedPrefix}users", formattedPath); + } + + [Theory] + [CombinatorialData] + public void Format_RouteHasOptionalParametersToBeRedacted_ReturnsCorrectlyRedactedPath(HttpRouteParameterRedactionMode redactionMode) + { + string redactedPrefix = redactionMode == HttpRouteParameterRedactionMode.None ? string.Empty : "Redacted:"; + HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter(); + Dictionary parametersToRedact = new() { { "routeId", SimpleClassifications.PrivateData } }; + string httpRoute = "/api/routes/{routeId?}"; + + // An optional parameter is redacted when it is explicitly specified. + string httpPath = "/api/routes/routeId123"; + string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal($"api/routes/{redactedPrefix}routeId123", formattedPath); + + // An http path is correctly formatted if an optional parameter is omitted. + httpPath = "/api/routes"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal("api/routes", formattedPath); + } + + [Theory] + [CombinatorialData] + public void Format_RouteHasParametersWithConstraintsToBeRedacted_ReturnsCorrectlyRedactedPath(HttpRouteParameterRedactionMode redactionMode) + { + string redactedPrefix = redactionMode == HttpRouteParameterRedactionMode.None ? string.Empty : "Redacted:"; + HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter(); + Dictionary parametersToRedact = new() { { "routeId", SimpleClassifications.PrivateData } }; + + string httpRoute = "/api/routes/{routeId:int:min(1)}"; + string httpPath = "/api/routes/routeId123"; + + string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal($"api/routes/{redactedPrefix}routeId123", formattedPath); + } + + [Theory] + [CombinatorialData] + public void Format_HttpPathMayHaveTrailingSlash_FormattedHttpPathDoNotHaveTrailingSlash(HttpRouteParameterRedactionMode redactionMode) + { + string redactedPrefix = redactionMode == HttpRouteParameterRedactionMode.None ? string.Empty : "Redacted:"; + + HttpRouteFormatter httpFormatter = CreateHttpRouteFormatter(); + Dictionary parametersToRedact = new() { { "routeId", SimpleClassifications.PrivateData } }; + string httpRoute = "/api/routes/static_route"; + + // An http route is static. + string httpPath = "/api/routes/static_route"; + string formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal($"api/routes/static_route", formattedPath); + + httpPath = "/api/routes/static_route/"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal($"api/routes/static_route", formattedPath); + + // An http route ends with a slash and has an optional parameter which may be omitted. + httpRoute = "/api/routes/{routeId?}/"; + + httpPath = "/api/routes/routeId123"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal($"api/routes/{redactedPrefix}routeId123", formattedPath); + + httpPath = "/api/routes/routeId123/"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal($"api/routes/{redactedPrefix}routeId123", formattedPath); + + httpPath = "/api/routes"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal("api/routes", formattedPath); + + httpPath = "/api/routes/"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal("api/routes", formattedPath); + + // All segments of an http route are omitted. + // The formatted http path is always an empty string. + httpRoute = "{controller=home}/{action=index}"; + + httpPath = "/"; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal("", formattedPath); + + httpPath = ""; + formattedPath = httpFormatter.Format(httpRoute, httpPath, redactionMode, parametersToRedact); + Assert.Equal("", formattedPath); + } + + private static HttpRouteFormatter CreateHttpRouteFormatter() + { + var redactorProvider = new FakeRedactorProvider( + new FakeRedactorOptions { RedactionFormat = "Redacted:{0}" }); + var httpParser = new HttpRouteParser(redactorProvider); + return new HttpRouteFormatter(httpParser, redactorProvider); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/MetricEnrichmentPropertyBagTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/MetricEnrichmentPropertyBagTest.cs new file mode 100644 index 0000000000..eff7a630ee --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/MetricEnrichmentPropertyBagTest.cs @@ -0,0 +1,145 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Internal.Test; + +public class MetricEnrichmentPropertyBagTest +{ + [Fact] + public void AddProperty_AllVersions_WithNullKey_Throws() + { + var enrichmentBag = new MetricEnrichmentPropertyBag(); + Assert.Throws(() => enrichmentBag.Add(null!, 1)); + Assert.Throws(() => enrichmentBag.Add(null!, "testString")); + } + + [Fact] + public void AddProperty_AllVersions_WithEmptyKey_Throws() + { + var enrichmentBag = new MetricEnrichmentPropertyBag(); + Assert.Throws(() => enrichmentBag.Add("", 1)); + Assert.Throws(() => enrichmentBag.Add(string.Empty, "testString")); + } + + [Fact] + public void AddProperty_AllVersions_WithNullValue_Throws() + { + var enrichmentBag = new MetricEnrichmentPropertyBag(); + Assert.Throws(() => enrichmentBag.Add("key1", null!)); + } + + [Fact] + public void AddProperty_EmptyString_DoesNotReplaceWithNull() + { + var enrichmentBag = new MetricEnrichmentPropertyBag + { + { "key1", "" } + }; + + Assert.Equal("key1", enrichmentBag[0].Key); + Assert.Equal("", enrichmentBag[0].Value); + } + + [Fact] + public void AddProperty_ValidKeyAndValues_AddedCorrectly() + { + var enrichmentBag = new MetricEnrichmentPropertyBag + { + { "key1", 10 }, + { "key2", "value2" }, + { "key3", new NoString() } + }; + + Assert.Equal("key1", enrichmentBag[0].Key); + Assert.Equal("10", enrichmentBag[0].Value); + + Assert.Equal("key2", enrichmentBag[1].Key); + Assert.Equal("value2", enrichmentBag[1].Value); + + Assert.Equal("key3", enrichmentBag[2].Key); + Assert.Equal(string.Empty, enrichmentBag[2].Value); + } + + [Fact] + public void AddProperty_ObjectSpan_AddedCorrectly() + { + var enrichmentBag = new MetricEnrichmentPropertyBag(); + +#pragma warning disable S3257 // Declarations and initializations should be as concise as possible + var props = new KeyValuePair[] + { + new("key1", 10), + new("key2", "value2"), + new("key3", new NoString()), + }; +#pragma warning restore S3257 // Declarations and initializations should be as concise as possible + + enrichmentBag.Add(props); + + Assert.Equal("key1", enrichmentBag[0].Key); + Assert.Equal("10", enrichmentBag[0].Value); + + Assert.Equal("key2", enrichmentBag[1].Key); + Assert.Equal("value2", enrichmentBag[1].Value); + + Assert.Equal("key3", enrichmentBag[2].Key); + Assert.Equal(string.Empty, enrichmentBag[2].Value); + } + + [Fact] + public void AddProperty_StringSpan_AddedCorrectly() + { + var enrichmentBag = new MetricEnrichmentPropertyBag(); + +#pragma warning disable S3257 // Declarations and initializations should be as concise as possible + var props = new KeyValuePair[] + { + new("key1", "10"), + new("key2", "value2"), + }; +#pragma warning restore S3257 // Declarations and initializations should be as concise as possible + + enrichmentBag.Add(props); + + Assert.Equal("key1", enrichmentBag[0].Key); + Assert.Equal("10", enrichmentBag[0].Value); + + Assert.Equal("key2", enrichmentBag[1].Key); + Assert.Equal("value2", enrichmentBag[1].Value); + } + + [Fact] + public void MetricEnrichmentPropertyBag_Reset_Clears() + { + var enrichmentBag = new MetricEnrichmentPropertyBag(); + +#pragma warning disable S3257 // Declarations and initializations should be as concise as possible + var props = new KeyValuePair[] + { + new("key1", "10"), + new("key2", "value2"), + }; +#pragma warning restore S3257 // Declarations and initializations should be as concise as possible + + enrichmentBag.Add(props); + + Assert.Equal("key1", enrichmentBag[0].Key); + Assert.Equal("10", enrichmentBag[0].Value); + + Assert.Equal("key2", enrichmentBag[1].Key); + Assert.Equal("value2", enrichmentBag[1].Value); + + _ = enrichmentBag.TryReset(); + + Assert.Empty(enrichmentBag); + } + + private class NoString + { + public override string? ToString() => null; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsConfigParserTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsConfigParserTest.cs new file mode 100644 index 0000000000..3ddfc32cc6 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsConfigParserTest.cs @@ -0,0 +1,182 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Internal.Test; + +/// +/// Copied from https://github.com/open-telemetry/opentelemetry-dotnet/blob/952c3b17fc2eaa0622f5f3efd336d4cf103c2813/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigParserTest.cs. +/// +public class SelfDiagnosticsConfigParserTest +{ + [Fact] + public void SelfDiagnosticsConfigParser_TryParseFilePath_Success() + { + string configJson = "{ \t \n " + + "\t \"LogDirectory\" \t : \"Diagnostics\", \n" + + "FileSize \t : \t \n" + + " 1024 \n}\n"; + Assert.True(SelfDiagnosticsConfigParser.TryParseLogDirectory(configJson, out string logDirectory)); + Assert.Equal("Diagnostics", logDirectory); + } + + [Fact] + public void SelfDiagnosticsConfigParser_TryParseFilePath_MissingField() + { + string configJson = @"{ + ""path"": ""Diagnostics"", + ""FileSize"": 1024 + }"; + Assert.False(SelfDiagnosticsConfigParser.TryParseLogDirectory(configJson, out _)); + } + + [Fact] + public void SelfDiagnosticsConfigParser_TryParseFileSize() + { + string configJson = @"{ + ""LogDirectory"": ""Diagnostics"", + ""FileSize"": 1024 + }"; + Assert.True(SelfDiagnosticsConfigParser.TryParseFileSize(configJson, out int fileSize)); + Assert.Equal(1024, fileSize); + } + + [Fact] + public void SelfDiagnosticsConfigParser_TryParseFileSize_CaseInsensitive() + { + string configJson = @"{ + ""LogDirectory"": ""Diagnostics"", + ""fileSize"" : + 2048 + }"; + Assert.True(SelfDiagnosticsConfigParser.TryParseFileSize(configJson, out int fileSize)); + Assert.Equal(2048, fileSize); + } + + [Fact] + public void SelfDiagnosticsConfigParser_TryParseFileSize_MissingField() + { + string configJson = @"{ + ""LogDirectory"": ""Diagnostics"", + ""size"": 1024 + }"; + Assert.False(SelfDiagnosticsConfigParser.TryParseFileSize(configJson, out _)); + } + + [Fact] + public void SelfDiagnosticsConfigParser_TryParseLogLevel() + { + string configJson = @"{ + ""LogDirectory"": ""Diagnostics"", + ""FileSize"": 1024, + ""LogLevel"": ""Error"" + }"; + Assert.True(SelfDiagnosticsConfigParser.TryParseLogLevel(configJson, out string logLevelString)); + Assert.Equal("Error", logLevelString); + } + + [Theory] + [InlineData(@"{""FileSize"": 1024, ""LogLevel"": ""Error""}")] + [InlineData(@"{""LogDirectory"": ""Diagnostics"", ""LogLevel"": ""Error""}")] + [InlineData(@"{""LogDirectory"": ""Diagnostics"", ""FileSize"": 1024}")] +#pragma warning disable xUnit1026 // Theory methods should use all of their parameters - yes, it does use. + public void SelfDiagnosticsConfigParser_TryGetConfiguration_AnyFieldMissing_Fails(string configFileContent) +#pragma warning restore xUnit1026 // Theory methods should use all of their parameters + { + var sut = new Mock(); + sut.Setup(c => c.TryReadConfigFile(It.IsAny(), out configFileContent)).Returns(true); + + Assert.False(sut.Object.TryGetConfiguration(out _, out _, out _)); + } + + [Fact] + public void SelfDiagnosticsConfigParser_TryGetConfiguration_FileSizeLowerThanLimit_AdjustsToLimit() + { + var configFileContent = @"{""LogDirectory"": ""Diagnostics"", ""FileSize"": 1023, ""LogLevel"": ""Error""}"; + var sut = new Mock(); + sut.Setup(c => c.TryReadConfigFile(It.IsAny(), out configFileContent)).Returns(true); + sut.Setup(c => c.SetFileSizeWithinLimit(It.IsAny())).CallBase(); + + var result = sut.Object.TryGetConfiguration(out _, out var fileSize, out _); + + Assert.True(result); + Assert.Equal(SelfDiagnosticsConfigParser.FileSizeLowerLimit, fileSize); + } + + [Fact] + public void SelfDiagnosticsConfigParser_TryGetConfiguration_FileSizeHigherThanLimit_AdjustsToLimit() + { + var configFileContent = @"{""LogDirectory"": ""Diagnostics"", ""FileSize"": 133000, ""LogLevel"": ""Error""}"; + var sut = new Mock(); + sut.Setup(c => c.TryReadConfigFile(It.IsAny(), out configFileContent)).Returns(true); + sut.Setup(c => c.SetFileSizeWithinLimit(It.IsAny())).CallBase(); + + var result = sut.Object.TryGetConfiguration(out _, out var fileSize, out _); + + Assert.True(result); + Assert.Equal(SelfDiagnosticsConfigParser.FileSizeUpperLimit, fileSize); + } + + [Fact] + public void SelfDiagnosticsConfigParser_TryReadConfigFile_ExceptionThrown_ReturnsFalse() + { + CreateConfigFile(SelfDiagnosticsConfigParser.ConfigFileName); + using var file = File.Open(SelfDiagnosticsConfigParser.ConfigFileName, FileMode.Open, FileAccess.Read, + FileShare.None); // file is open, so opening it again in SelfDiagnosticsConfigParser will throw. + + var parser = new SelfDiagnosticsConfigParser(); + + Assert.False(parser.TryReadConfigFile(SelfDiagnosticsConfigParser.ConfigFileName, out _)); + + file.Close(); + CleanupConfigFile(SelfDiagnosticsConfigParser.ConfigFileName); + } + + [Fact] + public void SelfDiagnosticsConfigParser_TryReadConfigFileFromCurrentFolder_SuccessfullyReadsFile() + { + const string ConfigFileName = "testConfig.json"; + CreateConfigFile(ConfigFileName); + + var parser = new SelfDiagnosticsConfigParser(); + + Assert.True(parser.TryReadConfigFile(ConfigFileName, out _)); + + CleanupConfigFile(ConfigFileName); + } + + [Fact] + public void SelfDiagnosticsConfigParser_TryReadConfigFileFromAppDirectory_SuccessfullyReadsFile() + { + var currentDir = Directory.GetCurrentDirectory(); + var directoryInfo = Directory.CreateDirectory("test"); + var configFilePath = Path.Combine(AppContext.BaseDirectory, SelfDiagnosticsConfigParser.ConfigFileName); + CreateConfigFile(configFilePath); + + Directory.SetCurrentDirectory(directoryInfo.FullName); + var parser = new SelfDiagnosticsConfigParser(); + Assert.True(parser.TryReadConfigFile(SelfDiagnosticsConfigParser.ConfigFileName, out _)); + Directory.SetCurrentDirectory(currentDir); + + CleanupConfigFile(configFilePath); + Directory.Delete(directoryInfo.FullName); + } + + private static void CreateConfigFile(string configFilePath) + { + using FileStream file = File.Open(configFilePath, FileMode.Create, FileAccess.Write); + file.Write(new byte[] { 0x5C, 0x75, 0x46, 0x46, 0x30, 0x30 }, 0, 6); + } + + private static void CleanupConfigFile(string configFilePath) + { + if (File.Exists(configFilePath)) + { + File.Delete(configFilePath); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsConfigRefresherTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsConfigRefresherTest.cs new file mode 100644 index 0000000000..5bb71191ed --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsConfigRefresherTest.cs @@ -0,0 +1,418 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.Tracing; +using System.IO; +using System.IO.MemoryMappedFiles; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AutoFixture; +using Microsoft.Extensions.Time.Testing; +using Microsoft.TestUtilities; +using Moq; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Extensions.Telemetry.Internal.Test; + +/// +/// Copied from https://github.com/open-telemetry/opentelemetry-dotnet/blob/952c3b17fc2eaa0622f5f3efd336d4cf103c2813/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigRefresherTest.cs. +/// +public class SelfDiagnosticsConfigRefresherTest +{ + private const int BufferSize = 512; + + private static readonly byte[] _messageOnNewFile = SelfDiagnosticsConfigRefresher.MessageOnNewFile; + private static readonly string _messageOnNewFileString = Encoding.UTF8.GetString(SelfDiagnosticsConfigRefresher.MessageOnNewFile); + private static readonly FakeTimeProvider _timeProvider = new(); + + private readonly Mock _configParserMock = new(); + private readonly ITestOutputHelper _output; + +#pragma warning disable IDE0052 // Remove unread private members - read into the mock's out param. + private string _configFileContent = @"{""LogDirectory"": ""."", ""FileSize"": 1024, ""LogLevel"": ""Error""}"; +#pragma warning restore IDE0052 // Remove unread private members + + public SelfDiagnosticsConfigRefresherTest(ITestOutputHelper output) + { + _configParserMock.Setup(c => c.TryReadConfigFile(It.IsAny(), out _configFileContent)).Returns(true); + _configParserMock.Setup(c => c.SetFileSizeWithinLimit(1024)).Returns(1024); + + _output = output; + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsConfigRefresher_OmitAsConfigured() + { + const string LogFileName = "omit.log"; + + try + { + using SelfDiagnosticsConfigRefresher configRefresher = new SelfDiagnosticsConfigRefresher(_timeProvider, _configParserMock.Object, LogFileName); + + // Emitting event of EventLevel.Warning + TestEventSource.Log.WarningEvent(); + + var actualBytes = ReadFile(BufferSize, LogFileName); + string logText = Encoding.UTF8.GetString(actualBytes); + _output.WriteLine(logText); // for debugging in case the test fails + Assert.StartsWith(_messageOnNewFileString, logText); + + // The event was omitted + Assert.Equal('\0', (char)actualBytes[_messageOnNewFile.Length]); + } + finally + { + RemoveLogFile(LogFileName); + } + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsConfigRefresher_WhenConfigIsAvailable_CaptureAsConfigured() + { + const string LogFileName = "capture.log"; + const int MessageLength = 941; + var fixture = new Fixture(); + var configFileContent = @"{""LogDirectory"": ""."", ""FileSize"": 1, ""LogLevel"": ""Error""}"; + var configParserMock = new Mock(); + configParserMock.Setup(c => c.TryReadConfigFile(It.IsAny(), out configFileContent)).Returns(true); + configParserMock.Setup(c => c.SetFileSizeWithinLimit(It.IsAny())).Returns(x => x); + + // or any string longer than configured default 1024kb to test file overflow. + var longString = string.Join(string.Empty, fixture.CreateMany(MessageLength)); + try + { + using SelfDiagnosticsConfigRefresher configRefresher = new(_timeProvider, configParserMock.Object, LogFileName); + + // Emitting event of EventLevel.Critical or any level equal or higher than the configured EventLevel.Error + TestEventSource.Log.CriticalEvent(longString); + + byte[] actualBytes = ReadFile(MessageLength + BufferSize, LogFileName); + string logText = Encoding.UTF8.GetString(actualBytes); + Assert.StartsWith(_messageOnNewFileString, logText); + + // The event was captured + string logLine = logText.Substring(_messageOnNewFileString.Length); + string logMessage = ParseLogMessage(logLine); + + Assert.StartsWith(TestEventSource.CriticalMessageText, logMessage); + } + finally + { + RemoveLogFile(LogFileName); + } + } + + [Fact(Skip = "Flaky")] + public void TryGetLogStream_WhenViewStreamDisposed_ReturnsFalse() + { + const string LogFileName = "noViewStream.log"; + Stream? stream = null; + var timeProvider = new FakeTimeProvider(); + + try + { + using SelfDiagnosticsConfigRefresher configRefresher = new(timeProvider, _configParserMock.Object, LogFileName); + configRefresher.ViewStream.Dispose(); + var result = configRefresher.TryGetLogStream(100, out stream, out var availableByteCount); + + Assert.False(result); + Assert.Equal(Stream.Null, stream); + Assert.Equal(0, availableByteCount); + } + catch (ObjectDisposedException) + { + // After disposing the ThreadLocal ViewStream, configRefresher is being disposed too and throws, here we are catching it. + } + finally + { + stream?.Dispose(); + File.Delete(LogFileName); + } + } + + [Fact(Skip = "Flaky")] + public async Task SelfDiagnosticsConfigRefresher_WhenConfigDisappearsAndAppearsBack_CaptureAsConfigured() + { + const string LogFileName = "withUnreliableConfig.log"; + var timeProvider = new FakeTimeProvider(startTime: DateTime.UtcNow); + var parserMock = new Mock(); + var configFileContentInitial = @"{""LogDirectory"": ""."", ""FileSize"": 1024, ""LogLevel"": ""Verbose""}"; + var configFileContentNew = @"{""LogDirectory"": ""."", ""FileSize"": 1025, ""LogLevel"": ""Verbose""}"; + parserMock.Setup(parser => parser.TryReadConfigFile(It.IsAny(), out configFileContentInitial)).Returns(true); + parserMock.Setup(parser => parser.SetFileSizeWithinLimit(It.IsAny())).CallBase(); + + try + { + using SelfDiagnosticsConfigRefresher configRefresher = new(timeProvider, parserMock.Object, LogFileName); + timeProvider.Advance(TimeSpan.FromSeconds(10)); // give the SelfDiagnosticsConfigRefresher's ctor to pick up the initial config. + parserMock.Setup(parser => parser.TryReadConfigFile(It.IsAny(), out configFileContentInitial)).Returns(false); // pretending that config file was removed. + TestEventSource.Log.VerboseEvent(); // that event will be dropped + parserMock.Setup(parser => parser.TryReadConfigFile(It.IsAny(), out configFileContentNew)).Returns(true); // restoring config back. + timeProvider.Advance(TimeSpan.FromSeconds(10)); // give the SelfDiagnosticsConfigRefresher's worker thread time to pick up the new config. + await Task.Delay(TimeSpan.FromSeconds(1)); + TestEventSource.Log.VerboseEvent(); + byte[] actualBytes = ReadFile(BufferSize, LogFileName); + string logText = Encoding.UTF8.GetString(actualBytes); + Assert.StartsWith(_messageOnNewFileString, logText); + + // The event was captured + string logLine = logText.Substring(_messageOnNewFileString.Length); + string logMessage = ParseLogMessage(logLine); + + Assert.StartsWith(TestEventSource.VerboseMessageText, logMessage); + } + finally + { + RemoveLogFile(LogFileName); + } + } + + [Fact(Skip = "Flaky")] + public async Task SelfDiagnosticsConfigRefresher_WhenLogLevelUpdated_CaptureAsConfigured() + { + const string LogFileName = "withNewLogLevel.log"; + var timeProvider = new FakeTimeProvider(startTime: DateTime.UtcNow); + var parserMock = new Mock(); + var configFileContentInitial = @"{""LogDirectory"": ""."", ""FileSize"": 1024, ""LogLevel"": ""Error""}"; + var configFileContentNew = @"{""LogDirectory"": ""."", ""FileSize"": 1024, ""LogLevel"": ""Verbose""}"; + parserMock.Setup(parser => parser.TryReadConfigFile(It.IsAny(), out configFileContentInitial)).Returns(true); + parserMock.Setup(parser => parser.SetFileSizeWithinLimit(It.IsAny())).CallBase(); + + try + { + using SelfDiagnosticsConfigRefresher configRefresher = new(timeProvider, parserMock.Object, LogFileName); + timeProvider.Advance(TimeSpan.FromSeconds(10)); // give the SelfDiagnosticsConfigRefresher's ctor to pick up the initial config. + TestEventSource.Log.ErrorEvent(); + await Task.Delay(TimeSpan.FromSeconds(10)); + parserMock.Setup(parser => parser.TryReadConfigFile(It.IsAny(), out configFileContentNew)).Returns(true); // updating log level. + timeProvider.Advance(TimeSpan.FromSeconds(10)); // give the SelfDiagnosticsConfigRefresher's worker thread time to pick up the new config. + await Task.Delay(TimeSpan.FromSeconds(5)); + TestEventSource.Log.VerboseEvent(); + + var outputFilePath = Path.Combine(".", LogFileName); + var times = 3; + string logText = string.Empty; + string logMessage = string.Empty; + + // checking until the file has the right content, + // because the file is updated in a different thread, + // which we have no access to. + while (times > 0) + { + using var file = File.Open(outputFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + byte[] actualBytes = new byte[BufferSize]; + _ = file.Read(actualBytes, 0, BufferSize); + logText = Encoding.UTF8.GetString(actualBytes); + var logLine = logText.Substring(_messageOnNewFileString.Length); + logMessage = ParseLogMessage(logLine); + + if (logMessage.Contains(TestEventSource.VerboseMessageText)) + { + break; + } + + await timeProvider.Delay( + TimeSpan.FromSeconds(10), + CancellationToken.None) + .ConfigureAwait(false); + times--; + } + + Assert.StartsWith(_messageOnNewFileString, logText); + Assert.Contains(TestEventSource.VerboseMessageText, logMessage); + Assert.StartsWith(TestEventSource.ErrorMessageText, logMessage); + } + finally + { + RemoveLogFile(LogFileName); + } + } + + [Fact(Skip = "Flaky")] + public async Task SelfDiagnosticsConfigRefresher_WhenOneStreamDisposed_WorksCorrectly() + { + // Arrange + const string LogFileName = "withDisposedStream.log"; + var timeProvider = TimeProvider.System; + var parserMock = new Mock(); + var configFileContentInitial = @"{""LogDirectory"": ""."", ""FileSize"": 1024, ""LogLevel"": ""Verbose""}"; + parserMock.Setup(parser => parser.TryReadConfigFile(It.IsAny(), out configFileContentInitial)).Returns(true); + parserMock.Setup(parser => parser.SetFileSizeWithinLimit(It.IsAny())).CallBase(); + + using var configRefresher = new SelfDiagnosticsConfigRefresher(timeProvider, parserMock.Object, LogFileName); + using var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresher, timeProvider); + + await timeProvider.Delay(TimeSpan.FromSeconds(10), CancellationToken.None); // give the SelfDiagnosticsConfigRefresher's ctor time to pick up the initial config. + _ = configRefresher.TryGetLogStream(100, out _, out _); // opening the file, i.e. creating a stream + + Stream? stream = null; + try + { + // Act + configRefresher.ViewStream.Value = null; + var exception = Record.Exception(() => configRefresher.Dispose()); + var result = configRefresher.TryGetLogStream(100, out stream, out var availableByteCount); + + // Assert + Assert.Null(exception); + Assert.False(result); + Assert.Equal(Stream.Null, stream); + Assert.Equal(0, availableByteCount); + } + finally + { + stream?.Dispose(); + RemoveLogFile(LogFileName); + } + } + + [Fact(Skip = "Flaky")] + public async Task WorkerAsync_WhenTaskCancelled_CorrectlyStops() + { + // Arrange + var timeProvider = System.TimeProvider.System; + var parserMock = new Mock(); + parserMock.Setup(parser => parser.TryReadConfigFile(It.IsAny(), out It.Ref.IsAny)).Returns(false); + parserMock.Setup(parser => parser.SetFileSizeWithinLimit(It.IsAny())).CallBase(); + + using var configRefresher = new SelfDiagnosticsConfigRefresher(timeProvider, parserMock.Object); + + using var cts = new CancellationTokenSource(); + cts.CancelAfter(10000); + try + { + await configRefresher.WorkerAsync(cts.Token); + } + catch (OperationCanceledException) + { + // no. + } + + parserMock.Verify(p => p.TryReadConfigFile(It.IsAny(), out It.Ref.IsAny), Times.AtMost(3)); + } + + [Fact(Skip = "Flaky")] + public void Worker_WhenClockDoesNotThrow_CorrectlyStops() + { + // Arrange + var timeProvider = new FakeTimeProvider(); + var parserMock = Mock.Of(); + Assert.Null(Record.Exception(() => + { + var configRefresher = new SelfDiagnosticsConfigRefresher(timeProvider, parserMock, workerTaskToken: CancellationToken.None); + configRefresher.Dispose(); + })); + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsConfigRefresher_WhenNoConfigFile_CannotGetLogStream() + { + const string LogFileName = "noFile.log"; + Stream? stream = null; + + try + { + var configRefresher = new SelfDiagnosticsConfigRefresher(_timeProvider, _configParserMock.Object, LogFileName); + configRefresher.Dispose(); + + var result = configRefresher.TryGetLogStream(1, out stream, out var availableByteCount); + + Assert.False(result); + Assert.Equal(Stream.Null, stream); + Assert.Equal(0, availableByteCount); + } + finally + { + stream?.Dispose(); + RemoveLogFile(LogFileName); + } + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsConfigRefresher_InvalidDirectory_WritesEventSourceEvent() + { + using var listener = new TestEventListener(SelfDiagnosticsEventSource.Log, EventLevel.Warning); + + var invalidDirectory = AppContext.BaseDirectory + "\\nul"; + var configParserMock = new Mock(); + var configFileContent = $"{{\"LogDirectory\": \"{invalidDirectory}\", \"FileSize\": 1024, \"LogLevel\": \"Error\"}}"; // nul is a Windows reserved name. + configParserMock.Setup(c => c.TryReadConfigFile(It.IsAny(), out configFileContent)).Returns(true); + using var configRefresher = new SelfDiagnosticsConfigRefresher(_timeProvider, configParserMock.Object); + + var lastEvent = listener.LastEvent; + + Assert.NotNull(lastEvent); + Assert.Equal(SelfDiagnosticsEventSource.FileCreateExceptionEventId, lastEvent!.EventId); + Assert.Equal(EventLevel.Warning, lastEvent!.Level); + Assert.Contains(invalidDirectory, lastEvent!.Payload!); + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux, SkipReason = "See https://github.com/dotnet/r9/issues/97")] + public void TryGetLogStream_MemoryMappedFileCache_NotEqual_MemoryMappedFile_ReturnsTrue() + { + const string LogFileName = "noMemoryMappedFileCache.log"; + Stream? stream = null; + var timeProvider = new FakeTimeProvider(); + + try + { + using SelfDiagnosticsConfigRefresher configRefresher = new(timeProvider, _configParserMock.Object, LogFileName); + using MemoryMappedFile testFile = MemoryMappedFile.CreateNew("TestName", 100); + using MemoryMappedViewStream testViewStream = testFile.CreateViewStream(); + configRefresher.ViewStream.Value = testViewStream; + Assert.NotNull(testViewStream); + Assert.NotNull(configRefresher.ViewStream.Value); + configRefresher.MemoryMappedFileCache.Value = null!; + var result = configRefresher.TryGetLogStream(100, out stream, out var availableByteCount); + + Assert.True(result); + } + catch (ObjectDisposedException) + { + // After disposing the ThreadLocal ViewStream, configRefresher is being disposed too and throws, here we are catching it. + } + finally + { + stream?.Dispose(); + File.Delete(LogFileName); + } + } + + private static string ParseLogMessage(string logLine) + { + int timestampPrefixLength = "2020-08-14T20:33:24.4788109Z:".Length; + Assert.Matches(@"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{7}Z:", logLine.Substring(0, timestampPrefixLength)); + return logLine.Substring(timestampPrefixLength); + } + + private static byte[] ReadFile(int byteCount, string logFileName) + { + var outputFilePath = Path.Combine(".", logFileName); + using var file = File.Open(outputFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + byte[] actualBytes = new byte[byteCount]; + _ = file.Read(actualBytes, 0, byteCount); + return actualBytes; + } + + private static void RemoveLogFile(string filePath) + { + try + { + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception) + { + // no handling. + } +#pragma warning restore CA1031 // Do not catch general exception types + + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsEventListenerTest.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsEventListenerTest.cs new file mode 100644 index 0000000000..c34eaa68fa --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsEventListenerTest.cs @@ -0,0 +1,340 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.Tracing; +using System.Globalization; +using System.IO; +using System.IO.MemoryMappedFiles; +using System.Text; +using Microsoft.Extensions.Time.Testing; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Internal.Test; + +/// +/// Copied from https://github.com/open-telemetry/opentelemetry-dotnet/blob/952c3b17fc2eaa0622f5f3efd336d4cf103c2813/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsEventListenerTest.cs. +/// +public class SelfDiagnosticsEventListenerTest +{ + private const string LogFilePath = "Diagnostics.log"; + private const string Ellipses = "...\n"; + private const string EllipsesWithBrackets = "{...}\n"; + + private static readonly FakeTimeProvider _timeProvider = new(); + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsEventListener_constructor_Invalid_Input() + { + // no configRefresher object + Assert.Throws(() => + { + _ = new SelfDiagnosticsEventListener(EventLevel.Error, null!, _timeProvider); + }); + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsEventListener_CanDispose() + { + var configRefresherMock = new Mock(_timeProvider, null, string.Empty, null); + var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresherMock.Object, _timeProvider); + + var exc1 = Record.Exception(() => listener.Dispose()); + var exc2 = Record.Exception(() => listener.Dispose()); + + Assert.Null(exc1); + Assert.Null(exc2); + + listener.Dispose(); + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsEventListener_EventSourceSetup_LowerSeverity() + { + var configRefresherMock = new Mock(_timeProvider, null, string.Empty, null); + using var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresherMock.Object, _timeProvider); + + // Emitting a Warning event. Or any EventSource event with lower severity than Error. + TestEventSource.Log.WarningEvent(); + configRefresherMock.Verify(refresher => refresher.TryGetLogStream(It.IsAny(), out It.Ref.IsAny, out It.Ref.IsAny), Times.Never()); + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsEventListener_EventSourceSetup_HigherSeverity() + { + var configRefresherMock = new Mock(_timeProvider, null, string.Empty, null); + configRefresherMock + .Setup(configRefresher => configRefresher.TryGetLogStream(It.IsAny(), out It.Ref.IsAny, out It.Ref.IsAny)) + .Returns(true); + using var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresherMock.Object, _timeProvider); + + // Emitting an Error event. Or any EventSource event with higher than or equal to to Error severity. + TestEventSource.Log.ErrorEvent(); + configRefresherMock.Verify(refresher => refresher.TryGetLogStream(It.IsAny(), out It.Ref.IsAny, out It.Ref.IsAny)); + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsEventListener_WriteEvent() + { + // Arrange + var payload = new ReadOnlyCollection( + new List + { + new(), + "test payload item", + null + }); + const string PayloadToString = "{System.Object}{test payload item}{null}"; + + var configRefresherMock = new Mock(_timeProvider, null, string.Empty, null); + var memoryMappedFile = MemoryMappedFile.CreateFromFile(LogFilePath, FileMode.Create, null, 1024); + Stream stream = memoryMappedFile.CreateViewStream(); + string eventMessage = "Event Message"; + int timestampPrefixLength = "2020-08-14T20:33:24.4788109Z:".Length; + byte[] bytes = Encoding.UTF8.GetBytes(eventMessage + PayloadToString); + int availableByteCount = 140; + configRefresherMock + .Setup(configRefresher => configRefresher.TryGetLogStream(timestampPrefixLength + bytes.Length + 1, out stream, out availableByteCount)) + .Returns(true); + using var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresherMock.Object, _timeProvider); + + // Act: call WriteEvent method directly + listener.WriteEvent(eventMessage, payload); + + // Assert + configRefresherMock.Verify(refresher => refresher.TryGetLogStream(timestampPrefixLength + bytes.Length + 1, out stream, out availableByteCount)); + stream.Dispose(); + memoryMappedFile.Dispose(); + AssertFileOutput(LogFilePath, eventMessage + PayloadToString); + + File.Delete(LogFilePath); + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsEventListener_DateTimeGetBytes() + { + var configRefresherMock = new Mock(_timeProvider, null, string.Empty, null); + using var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresherMock.Object, _timeProvider); + + // Check DateTimeKind of Utc, Local, and Unspecified + DateTime[] datetimes = + { + DateTime.SpecifyKind(DateTime.Parse("1996-12-01T14:02:31.1234567-08:00", CultureInfo.InvariantCulture), DateTimeKind.Utc), + DateTime.SpecifyKind(DateTime.Parse("1996-12-01T14:02:31.1234567-08:00", CultureInfo.InvariantCulture), DateTimeKind.Local), + DateTime.SpecifyKind(DateTime.Parse("1996-12-01T14:02:31.1234567-08:00", CultureInfo.InvariantCulture), DateTimeKind.Unspecified), + DateTime.UtcNow, + DateTime.Now, + }; + + // Expect to match output string from DateTime.ToString("O") + string[] expected = new string[datetimes.Length]; + for (int i = 0; i < datetimes.Length; i++) + { + expected[i] = datetimes[i].ToString("O", CultureInfo.InvariantCulture); + } + + byte[] buffer = new byte[40 * datetimes.Length]; + int pos = 0; + + // Get string after DateTimeGetBytes() write into a buffer + string[] results = new string[datetimes.Length]; + for (int i = 0; i < datetimes.Length; i++) + { + int len = listener.DateTimeGetBytes(datetimes[i], buffer, pos); + results[i] = Encoding.Default.GetString(buffer, pos, len); + pos += len; + } + + Assert.Equal(expected, results); + } + + [InlineData(5, '+')] + [InlineData(-3, '-')] + [InlineData(0, '+')] + [Theory(Skip = "Flaky")] + public void TestSign(int hours, char expected) + { + Assert.Equal((byte)expected, SelfDiagnosticsEventListener.GetHoursSign(hours)); + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsEventListener_EmitEvent_OmitAsConfigured() + { + // Arrange + var configRefresherMock = new Mock(_timeProvider, null, string.Empty, null); + var memoryMappedFile = MemoryMappedFile.CreateFromFile(LogFilePath, FileMode.Create, null, 1024); + Stream stream = memoryMappedFile.CreateViewStream(); + configRefresherMock + .Setup(configRefresher => configRefresher.TryGetLogStream(It.IsAny(), out stream, out It.Ref.IsAny)) + .Returns(true); + using var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresherMock.Object, _timeProvider); + + // Act: emit an event with severity lower than configured + TestEventSource.Log.WarningEvent(); + + // Assert + configRefresherMock.Verify(refresher => refresher.TryGetLogStream(It.IsAny(), out stream, out It.Ref.IsAny), Times.Never()); + stream.Dispose(); + memoryMappedFile.Dispose(); + + using var file = File.Open(LogFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + var buffer = new byte[256]; + _ = file.Read(buffer, 0, buffer.Length); + Assert.Equal('\0', (char)buffer[0]); + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsEventListener_EmitEvent_CaptureAsConfigured() + { + // Arrange + var configRefresherMock = new Mock(_timeProvider, null, string.Empty, null); + var memoryMappedFile = MemoryMappedFile.CreateFromFile(LogFilePath, FileMode.Create, null, 1024); + Stream stream = memoryMappedFile.CreateViewStream(); + configRefresherMock + .Setup(configRefresher => configRefresher.TryGetLogStream(It.IsAny(), out stream, out It.Ref.IsAny)) + .Returns(true); + using var listener = new SelfDiagnosticsEventListener(EventLevel.Error, configRefresherMock.Object, _timeProvider); + + // Act: emit an event with severity equal to configured + TestEventSource.Log.ErrorEvent(); + + // Assert + configRefresherMock.Verify(refresher => refresher.TryGetLogStream(It.IsAny(), out stream, out It.Ref.IsAny)); + stream.Dispose(); + memoryMappedFile.Dispose(); + + AssertFileOutput(LogFilePath, TestEventSource.ErrorMessageText); + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsEventListener_EncodeInBuffer_Null() + { + byte[] buffer = new byte[20]; + int startPos = 0; + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer(null, false, buffer, startPos); + Assert.Equal(startPos, endPos); + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsEventListener_EncodeInBuffer_Empty() + { + byte[] buffer = new byte[20]; + int startPos = 0; + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer(string.Empty, false, buffer, startPos); + byte[] expected = Encoding.UTF8.GetBytes(string.Empty); + AssertBufferOutput(expected, buffer, startPos, endPos); + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsEventListener_EncodeInBuffer_EnoughSpace() + { + byte[] buffer = new byte[20]; + int startPos = buffer.Length - Ellipses.Length - 6; // Just enough space for "abc" even if "...\n" needs to be added. + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", false, buffer, startPos); + + // '\n' will be appended to the original string "abc" after EncodeInBuffer is called. + // The byte where '\n' will be placed should not be touched within EncodeInBuffer, so it stays as '\0'. + byte[] expected = Encoding.UTF8.GetBytes("abc\0"); + AssertBufferOutput(expected, buffer, startPos, endPos + 1); + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsEventListener_EncodeInBuffer_NotEnoughSpaceForFullString() + { + byte[] buffer = new byte[20]; + int startPos = buffer.Length - Ellipses.Length - 5; // Just not space for "abc" if "...\n" needs to be added. + + // It's a quick estimate by assumption that most Unicode characters takes up to 2 16-bit UTF-16 chars, + // which can be up to 4 bytes when encoded in UTF-8. + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", false, buffer, startPos); + byte[] expected = Encoding.UTF8.GetBytes("ab...\0"); + AssertBufferOutput(expected, buffer, startPos, endPos + 1); + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsEventListener_EncodeInBuffer_NotEvenSpaceForTruncatedString() + { + byte[] buffer = new byte[20]; + int startPos = buffer.Length - Ellipses.Length; // Just enough space for "...\n". + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", false, buffer, startPos); + byte[] expected = Encoding.UTF8.GetBytes("...\0"); + AssertBufferOutput(expected, buffer, startPos, endPos + 1); + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsEventListener_EncodeInBuffer_NotEvenSpaceForTruncationEllipses() + { + byte[] buffer = new byte[20]; + int startPos = buffer.Length - Ellipses.Length + 1; // Not enough space for "...\n". + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", false, buffer, startPos); + Assert.Equal(startPos, endPos); + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsEventListener_EncodeInBuffer_IsParameter_EnoughSpace() + { + byte[] buffer = new byte[20]; + int startPos = buffer.Length - EllipsesWithBrackets.Length - 6; // Just enough space for "abc" even if "...\n" need to be added. + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", true, buffer, startPos); + byte[] expected = Encoding.UTF8.GetBytes("{abc}\0"); + AssertBufferOutput(expected, buffer, startPos, endPos + 1); + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsEventListener_EncodeInBuffer_IsParameter_NotEnoughSpaceForFullString() + { + byte[] buffer = new byte[20]; + int startPos = buffer.Length - EllipsesWithBrackets.Length - 5; // Just not space for "...\n". + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", true, buffer, startPos); + byte[] expected = Encoding.UTF8.GetBytes("{ab...}\0"); + AssertBufferOutput(expected, buffer, startPos, endPos + 1); + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsEventListener_EncodeInBuffer_IsParameter_NotEvenSpaceForTruncatedString() + { + byte[] buffer = new byte[20]; + int startPos = buffer.Length - EllipsesWithBrackets.Length; // Just enough space for "{...}\n". + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", true, buffer, startPos); + byte[] expected = Encoding.UTF8.GetBytes("{...}\0"); + AssertBufferOutput(expected, buffer, startPos, endPos + 1); + } + + [Fact(Skip = "Flaky")] + public void SelfDiagnosticsEventListener_EncodeInBuffer_IsParameter_NotEvenSpaceForTruncationEllipses() + { + byte[] buffer = new byte[20]; + int startPos = buffer.Length - EllipsesWithBrackets.Length + 1; // Not enough space for "{...}\n". + int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", true, buffer, startPos); + Assert.Equal(startPos, endPos); + } + + private static void AssertFileOutput(string filePath, string eventMessage) + { + using var file = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + var buffer = new byte[256]; + _ = file.Read(buffer, 0, buffer.Length); + string logLine = Encoding.UTF8.GetString(buffer); + string logMessage = ParseLogMessage(logLine); + Assert.StartsWith(eventMessage, logMessage); + } + + private static string ParseLogMessage(string logLine) + { + int timestampPrefixLength = "2020-08-14T20:33:24.4788109Z:".Length; + Assert.Matches(@"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{7}Z:", logLine.Substring(0, timestampPrefixLength)); + return logLine.Substring(timestampPrefixLength); + } + + private static void AssertBufferOutput(byte[] expected, byte[] buffer, int startPos, int endPos) + { + Assert.Equal(expected.Length, endPos - startPos); + for (int i = 0, j = startPos; j < endPos; ++i, ++j) + { + Assert.Equal(expected[i], buffer[j]); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsEventSourceTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsEventSourceTests.cs new file mode 100644 index 0000000000..7d8e0d5e69 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsEventSourceTests.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.Tracing; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Internal.Test; + +public class SelfDiagnosticsEventSourceTests +{ + [Fact(Skip = "Flaky")] + public void EventSource_WritesEvent() + { + using var listener = new TestEventListener(SelfDiagnosticsEventSource.Log, EventLevel.Warning); + + SelfDiagnosticsEventSource.Log.SelfDiagnosticsFileCreateException("test", new ArgumentException("test as well.")); + + var lastEvent = listener.LastEvent; + + Assert.NotNull(lastEvent); + Assert.Equal(SelfDiagnosticsEventSource.FileCreateExceptionEventId, lastEvent!.EventId); + Assert.Equal(EventLevel.Warning, lastEvent!.Level); + Assert.Contains("test", lastEvent!.Payload!); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsTests.cs new file mode 100644 index 0000000000..3f19488f11 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/SelfDiagnosticsTests.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Internal.Test; + +public class SelfDiagnosticsTests +{ + [Fact] + public void SelfDiagnostics_EnsureInitialized_DoesNotThrow() + { + var exception = Record.Exception(SelfDiagnostics.EnsureInitialized); + Assert.Null(exception); + } + + [Fact] + public void SelfDiagnostics_DoesNotThrow() + { + var sd = new SelfDiagnostics(); + + Assert.Null(Record.Exception(() => sd.Dispose())); + + sd.Dispose(); + } + + [Fact] + public void SelfDiagnostics_OnProcessExit_DoesNotThrow() + { + var exception = Record.Exception(() => SelfDiagnostics.OnProcessExit(null, EventArgs.Empty)); + Assert.Null(exception); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/Telemetry.Internal.Test.xunit.runner.json b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/Telemetry.Internal.Test.xunit.runner.json new file mode 100644 index 0000000000..bf9611afd3 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/Telemetry.Internal.Test.xunit.runner.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "diagnosticMessages": true, + "longRunningTestSeconds": 300, + "maxParallelThreads": 1, + "parallelizeTestCollections": false +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/TelemetryCommonExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/TelemetryCommonExtensionsTests.cs new file mode 100644 index 0000000000..b41ec00ba8 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/TelemetryCommonExtensionsTests.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Telemetry; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Internal.Test; + +public class TelemetryCommonExtensionsTests +{ + [Fact] + public void GetDependencyName_DependencyNameMissing_ReturnsUnknown() + { + var requestMetadata = new RequestMetadata + { + RequestName = "sampleRequest", + RequestRoute = "/v1/users/{userId}/chats" + }; + + Assert.Equal(TelemetryConstants.Unknown, requestMetadata.DependencyName); + } + + [Fact] + public void GetRequestName_RequestNamePresent_ReturnsRequestName() + { + var requestMetadata = new RequestMetadata + { + DependencyName = "testDependency", + RequestName = "sampleRequest", + RequestRoute = "/v1/users/{userId}/chats" + }; + + Assert.Equal(requestMetadata.RequestName, requestMetadata.GetRequestName()); + } + + [Fact] + public void GetRequestName_RequestNameMissing_RequestRoutePresent_ReturnsRequestRoute() + { + var requestMetadata = new RequestMetadata + { + DependencyName = "testDependency", + RequestRoute = "/v1/users/{userId}/chats" + }; + + Assert.Equal(requestMetadata.RequestRoute, requestMetadata.GetRequestName()); + } + + [Fact] + public void GetRequestName_RequestNameMissing_RequestRouteMissing_ReturnsUnknown() + { + var requestMetadata = new RequestMetadata + { + DependencyName = "testDependency", + }; + + Assert.Equal(TelemetryConstants.Unknown, requestMetadata.GetRequestName()); + } + + [Fact] + public void GetRequestRoute_RequestRouteMissing_ReturnsUnknown() + { + var requestMetadata = new RequestMetadata + { + DependencyName = "testDependency", + RequestName = "sampleRequest" + }; + + Assert.Equal(TelemetryConstants.Unknown, requestMetadata.RequestRoute); + } + + [Fact] + public void AddHttpRouteProcessor_Registers_RouterParserAndFormatter() + { + var sp = new ServiceCollection().AddFakeRedaction().AddHttpRouteProcessor().BuildServiceProvider(); + + Assert.NotNull(sp.GetRequiredService()); + Assert.NotNull(sp.GetRequiredService()); + } + + [Fact] + public void AddHttpHeadersRedactor_NullArgument_Throws() + { + Assert.Throws(() => ((IServiceCollection)null!).AddHttpHeadersRedactor()); + } + + [Fact] + public void AddHttpHeadersRedactor_Registers_HttpHeadersRedactor() + { + var sp = new ServiceCollection().AddFakeRedaction().AddHttpHeadersRedactor().BuildServiceProvider(); + + Assert.NotNull(sp.GetRequiredService()); + } + + [Fact] + public void AsynContext_SetRequestMetadata_ValidRequestMetadata_CorrectlySet() + { + var serviceCollection = new ServiceCollection(); + var sp = serviceCollection.AddOutgoingRequestContext().BuildServiceProvider(); + + var requestMetadataContext = sp.GetService(); + + var metadata = new RequestMetadata + { + DependencyName = "testDependency", + RequestName = "sampleRequest", + RequestRoute = "/v1/users/{userId}/chats" + }; + + if (requestMetadataContext != null) + { + requestMetadataContext.RequestMetadata = metadata; + } + + var extractedMetadata = requestMetadataContext?.RequestMetadata; + Assert.NotNull(extractedMetadata); + Assert.Equal(metadata.DependencyName, extractedMetadata!.DependencyName); + Assert.Equal(metadata.RequestName, extractedMetadata!.RequestName); + Assert.Equal(metadata.RequestRoute, extractedMetadata!.RequestRoute); + } + + [Fact] + public void AsynContext_SetRequestMetadata_EmptyRequestMetadata_CorrectlySets() + { + var serviceCollection = new ServiceCollection(); + var sp = serviceCollection.AddOutgoingRequestContext().BuildServiceProvider(); + + var requestMetadataContext = sp.GetService()!; + var metadata = new RequestMetadata(); + + requestMetadataContext.RequestMetadata = metadata; + + var extractedMetadata = requestMetadataContext.RequestMetadata; + Assert.NotNull(extractedMetadata); + Assert.Equal(TelemetryConstants.Unknown, extractedMetadata!.DependencyName); + Assert.Equal(TelemetryConstants.Unknown, extractedMetadata!.RequestName); + Assert.Equal(TelemetryConstants.Unknown, extractedMetadata!.RequestRoute); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/TestEventListener.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/TestEventListener.cs new file mode 100644 index 0000000000..744c464750 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/TestEventListener.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Tracing; + +namespace Microsoft.Extensions.Telemetry.Internal.Test; + +internal sealed class TestEventListener : EventListener +{ + public TestEventListener(EventSource eventSource, EventLevel eventLevel) + { + EnableEvents(eventSource, eventLevel); + } + + public EventWrittenEventArgs? LastEvent { get; private set; } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + LastEvent = eventData; + } +} + diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/TestEventSource.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/TestEventSource.cs new file mode 100644 index 0000000000..c31f854606 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Telemetry.Internal/TestEventSource.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.Tracing; + +namespace Microsoft.Extensions.Telemetry.Internal.Test; + +[EventSource(Name = "R9-SelfDiagnostics-Test")] +internal sealed class TestEventSource : EventSource +{ + public const string WarningMessageText = "This is a warning event."; + public const string ErrorMessageText = "This is an error event."; + public const string CriticalMessageText = "This is a critical event."; + public const string VerboseMessageText = "This is a verbose event."; + + public static readonly TestEventSource Log = new(); + + [Event(1, Message = WarningMessageText, Level = EventLevel.Warning)] + public void WarningEvent() + { + if (IsEnabled(EventLevel.Warning, EventKeywords.All)) + { + WriteEvent(1); + } + } + + [Event(2, Message = ErrorMessageText, Level = EventLevel.Error)] + public void ErrorEvent() + { + if (IsEnabled(EventLevel.Error, EventKeywords.All)) + { + WriteEvent(2); + } + } + + [Event(3, Message = CriticalMessageText, Level = EventLevel.Critical)] + public void CriticalEvent(string message) + { + if (IsEnabled(EventLevel.Critical, EventKeywords.All)) + { + WriteEvent(3, message); + } + } + + [Event(4, Message = VerboseMessageText, Level = EventLevel.Verbose)] + public void VerboseEvent() + { + if (IsEnabled(EventLevel.Verbose, EventKeywords.All)) + { + WriteEvent(4); + } + } +} + diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Tracing.Sampling/SamplingExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Tracing.Sampling/SamplingExtensionsTests.cs new file mode 100644 index 0000000000..6c003509ae --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Tracing.Sampling/SamplingExtensionsTests.cs @@ -0,0 +1,240 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Options; +using Moq; +using OpenTelemetry; +using OpenTelemetry.Trace; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Tracing.Test; + +public sealed class SamplingExtensionsTests : IDisposable +{ + private readonly ActivitySource _activitySource = new("testTraceSource"); + + public void Dispose() + { + _activitySource.Dispose(); + } + + [Fact] + public void AddSampling_NullArguments_Throws() + { + Assert.Throws(() => + ((TracerProviderBuilder)null!).AddSampling(Mock.Of())); + Assert.Throws(() => + Sdk.CreateTracerProviderBuilder().AddSampling((IConfigurationSection)null!)); + + Assert.Throws(() => + ((TracerProviderBuilder)null!).AddSampling(Mock.Of>())); + Assert.Throws(() => + Sdk.CreateTracerProviderBuilder().AddSampling((IConfigurationSection)null!)); + } + + [Fact] + public async Task AddSampling_AlwaysOn_Records() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddSource(_activitySource.Name) + .AddSampling(o => o.SamplerType = SamplerType.AlwaysOn))) + .Build(); + + using var activity = await RunHostAndActivityAsync(host); + + Assert.True(activity.Recorded); + } + + [Fact] + public async Task AddSampling_AlwaysOff_DoesNotRecord() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddSource(_activitySource.Name) + .AddSampling(o => o.SamplerType = SamplerType.AlwaysOff))) + .Build(); + + using var activity = await RunHostAndActivityAsync(host); + + Assert.False(activity.Recorded); + } + + [Fact] + public async Task AddSampling_TraceIdRatioBased_Probability1_Records() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddSource(_activitySource.Name) + .AddSampling(o => + { + o.SamplerType = SamplerType.TraceIdRatioBased; + o.TraceIdRatioBasedSamplerOptions = new TraceIdRatioBasedSamplerOptions + { + Probability = 1 + }; + }))) + .Build(); + + using var activity = await RunHostAndActivityAsync(host); + + Assert.True(activity.Recorded); + } + + [Fact] + public async Task AddSampling_TraceIdRatioBased_Probability0_DoesNotRecord() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddSource(_activitySource.Name) + .AddSampling(o => + { + o.SamplerType = SamplerType.TraceIdRatioBased; + o.TraceIdRatioBasedSamplerOptions = new TraceIdRatioBasedSamplerOptions + { + Probability = 0 + }; + }))) + .Build(); + + using var activity = await RunHostAndActivityAsync(host); + + Assert.False(activity.Recorded); + } + + [Fact] + public async Task AddSampling_TraceIdRatioBased_InvalidProbability_Throws() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddSource(_activitySource.Name) + .AddSampling(o => + { + o.SamplerType = SamplerType.TraceIdRatioBased; + o.TraceIdRatioBasedSamplerOptions = new TraceIdRatioBasedSamplerOptions + { + Probability = 1.46 + }; + }))) + .Build(); + + await Assert.ThrowsAsync( + () => RunHostAndActivityAsync(host)); + } + + [Fact] + public async Task AddSampling_ParentBased_AlwaysOnRootSampler_Records() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddSource(_activitySource.Name) + .AddSampling(o => + { + o.SamplerType = SamplerType.ParentBased; + o.ParentBasedSamplerOptions = new ParentBasedSamplerOptions + { + RootSamplerType = SamplerType.AlwaysOn + }; + }))) + .Build(); + + using var activity = await RunHostAndActivityAsync(host); + + Assert.True(activity.Recorded); + } + + [Fact] + public async Task AddSampling_ParentBased_AlwaysOffRootSampler_DoesNotRecord() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddSource(_activitySource.Name) + .AddSampling(o => + { + o.SamplerType = SamplerType.ParentBased; + o.ParentBasedSamplerOptions = new ParentBasedSamplerOptions + { + RootSamplerType = SamplerType.AlwaysOff + }; + }))) + .Build(); + + using var activity = await RunHostAndActivityAsync(host); + + Assert.False(activity.Recorded); + } + + [Fact] + public async Task AddSampling_ParentBased_TraceIdRatioBasedRootSampler_Probability1_Records() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddSource(_activitySource.Name) + .AddSampling(o => + { + o.SamplerType = SamplerType.ParentBased; + o.ParentBasedSamplerOptions = new ParentBasedSamplerOptions + { + RootSamplerType = SamplerType.TraceIdRatioBased + }; + o.TraceIdRatioBasedSamplerOptions = new TraceIdRatioBasedSamplerOptions + { + Probability = 1 + }; + }))) + .Build(); + + using var activity = await RunHostAndActivityAsync(host); + + Assert.True(activity.Recorded); + } + + [Fact] + public async Task AddSampling_ParentBased_TraceIdRatioBasedRootSampler_Probability0_DoesNotRecord() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddSource(_activitySource.Name) + .AddSampling(o => + { + o.SamplerType = SamplerType.ParentBased; + o.ParentBasedSamplerOptions = new ParentBasedSamplerOptions + { + RootSamplerType = SamplerType.TraceIdRatioBased + }; + o.TraceIdRatioBasedSamplerOptions = new TraceIdRatioBasedSamplerOptions + { + Probability = 0 + }; + }))) + .Build(); + + using var activity = await RunHostAndActivityAsync(host); + + Assert.False(activity.Recorded); + } + + private async Task RunHostAndActivityAsync(IHost host) + { + await host.StartAsync(); + var activity = _activitySource.StartActivity("Test"); + await host.StopAsync(); + return activity!; + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Tracing.Sampling/SamplingOptionsCustomValidatorTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Tracing.Sampling/SamplingOptionsCustomValidatorTests.cs new file mode 100644 index 0000000000..4226b41bc8 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Tracing.Sampling/SamplingOptionsCustomValidatorTests.cs @@ -0,0 +1,194 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Microsoft.Extensions.Telemetry.Tracing.Internal; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Tracing.Test; + +public class SamplingOptionsCustomValidatorTests +{ + private static readonly SamplingOptionsCustomValidator _validator = new(); + + [Fact] + public void CorrectOptions_Empty_DefaultsToAlwaysOn() + { + var options = new SamplingOptions { }; + + _validator.Validate("test", options).Succeeded.Should().Be(true); + } + + [Fact] + public void CorrectOptions_AlwaysOn() + { + var options = new SamplingOptions + { + SamplerType = SamplerType.AlwaysOn + }; + + _validator.Validate("test", options).Succeeded.Should().Be(true); + } + + [Fact] + public void CorrectOptions_AlwaysOff() + { + var options = new SamplingOptions + { + SamplerType = SamplerType.AlwaysOff + }; + + _validator.Validate("test", options).Succeeded.Should().Be(true); + } + + [Fact] + public void CorrectOptions_TraceIdRatioBased_DefaultsTo1() + { + var options = new SamplingOptions + { + SamplerType = SamplerType.TraceIdRatioBased, + TraceIdRatioBasedSamplerOptions = new TraceIdRatioBasedSamplerOptions { } + }; + + _validator.Validate("test", options).Succeeded.Should().Be(true); + } + + [Fact] + public void CorrectOptions_TraceIdRatioBased() + { + var options = new SamplingOptions + { + SamplerType = SamplerType.TraceIdRatioBased, + TraceIdRatioBasedSamplerOptions = new TraceIdRatioBasedSamplerOptions + { + Probability = 0.42 + } + }; + + _validator.Validate("test", options).Succeeded.Should().Be(true); + } + + [Fact] + public void CorrectOptions_ParentBased_DefaultsToAlwaysOn() + { + var options = new SamplingOptions + { + SamplerType = SamplerType.ParentBased, + ParentBasedSamplerOptions = new ParentBasedSamplerOptions { } + }; + + _validator.Validate("test", options).Succeeded.Should().Be(true); + } + + [Fact] + public void CorrectOptions_ParentBased_AlwaysOn() + { + var options = new SamplingOptions + { + SamplerType = SamplerType.ParentBased, + ParentBasedSamplerOptions = new ParentBasedSamplerOptions + { + RootSamplerType = SamplerType.AlwaysOn + } + }; + + _validator.Validate("test", options).Succeeded.Should().Be(true); + } + + [Fact] + public void CorrectOptions_ParentBased_AlwaysOff() + { + var options = new SamplingOptions + { + SamplerType = SamplerType.ParentBased, + ParentBasedSamplerOptions = new ParentBasedSamplerOptions + { + RootSamplerType = SamplerType.AlwaysOff + } + }; + + _validator.Validate("test", options).Succeeded.Should().Be(true); + } + + [Fact] + public void CorrectOptions_ParentBased_TraceIdRatioBased() + { + var options = new SamplingOptions + { + SamplerType = SamplerType.ParentBased, + ParentBasedSamplerOptions = new ParentBasedSamplerOptions + { + RootSamplerType = SamplerType.TraceIdRatioBased + }, + TraceIdRatioBasedSamplerOptions = new TraceIdRatioBasedSamplerOptions + { + Probability = 0.42 + } + }; + + _validator.Validate("test", options).Succeeded.Should().Be(true); + } + + [Fact] + public void IncorrectOptions_InvalidSamplerType() + { + var options = new SamplingOptions + { + SamplerType = (SamplerType)42 + }; + + _validator.Validate("test", options).Succeeded.Should().Be(false); + } + + [Fact] + public void IncorrectOptions_TraceIdRatioBased_NoOptions() + { + var options = new SamplingOptions + { + SamplerType = SamplerType.TraceIdRatioBased + }; + + _validator.Validate("test", options).Succeeded.Should().Be(false); + } + + [Fact] + public void IncorrectOptions_ParentBased_NoOptions() + { + var options = new SamplingOptions + { + SamplerType = SamplerType.ParentBased + }; + + _validator.Validate("test", options).Succeeded.Should().Be(false); + } + + [Fact] + public void IncorrectOptions_ParentBased_InvalidRootSampler() + { + var options = new SamplingOptions + { + SamplerType = SamplerType.ParentBased, + ParentBasedSamplerOptions = new ParentBasedSamplerOptions + { + RootSamplerType = SamplerType.ParentBased + }, + }; + + _validator.Validate("test", options).Succeeded.Should().Be(false); + } + + [Fact] + public void IncorrectOptions_ParentBased_TraceIdRatioBasedRootSampler_NoOptions() + { + var options = new SamplingOptions + { + SamplerType = SamplerType.ParentBased, + ParentBasedSamplerOptions = new ParentBasedSamplerOptions + { + RootSamplerType = SamplerType.TraceIdRatioBased + } + }; + + _validator.Validate("test", options).Succeeded.Should().Be(false); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Tracing/EnricherExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Tracing/EnricherExtensionsTests.cs new file mode 100644 index 0000000000..898cb27c26 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/Tracing/EnricherExtensionsTests.cs @@ -0,0 +1,319 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Telemetry.Enrichment; +using OpenTelemetry.Trace; +using Xunit; + +namespace Microsoft.Extensions.Telemetry.Enrichment.Test; + +public class EnricherExtensionsTests +{ + private const string TestActivitySourceName = "testTraceSource"; + + [Fact] + public void AddTraceEnricher_GivenNullArgument_Throws() + { + Assert.Throws(() => + ((TracerProviderBuilder)null!).AddTraceEnricher()); + + Assert.Throws(() => + ((TracerProviderBuilder)null!).AddTraceEnricher(new TestTraceEnricher())); + + Assert.Throws(() => + ((IServiceCollection)null!).AddTraceEnricher()); + + Assert.Throws(() => + ((IServiceCollection)null!).AddTraceEnricher(new TestTraceEnricher())); + + var services = new ServiceCollection(); + Assert.Throws(() => + services.AddOpenTelemetry().WithTracing(builder => + builder.AddTraceEnricher(null!))); + } + + [Fact] + public async Task TracerProviderBuilder_AddTraceEnricherT_AddsEnricherAndAppliesEnrichment() + { + using var activitySource = new ActivitySource(TestActivitySourceName); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddSource(TestActivitySourceName) + .AddTraceEnricher())) + .StartAsync(); + + var enrichmentProcessor = host.Services.GetRequiredService(); + Assert.NotNull(enrichmentProcessor); + + var activity = activitySource.StartActivity("Test"); + activity?.AddTag("internalKey", "internalValue"); + activity?.Stop(); + await host.StopAsync(); + + Assert.Equal("internalValue", activity?.GetTagItem("internalKey")); + Assert.Equal(1, activity?.GetTagItem("enrichedKey")); +#if NETCOREAPP3_1_OR_GREATER + Assert.Equal("enrichedValueOnStart", activity?.GetTagItem("enrichedKey_onStart")); +#endif + } + + [Fact] + public async Task IServiceCollection_AddTraceEnricherT_AddsEnricherAndAppliesEnrichment() + { + using var activitySource = new ActivitySource(TestActivitySourceName); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddTraceEnricher() + .AddOpenTelemetry().WithTracing(builder => builder + .AddSource(TestActivitySourceName))) + .StartAsync(); + + var enrichmentProcessor = host.Services.GetRequiredService(); + Assert.NotNull(enrichmentProcessor); + + var activity = activitySource.StartActivity("Test"); + activity?.AddTag("internalKey", "internalValue"); + activity?.Stop(); + await host.StopAsync(); + + Assert.Equal("internalValue", activity?.GetTagItem("internalKey")); + Assert.Equal(1, activity?.GetTagItem("enrichedKey")); +#if NETCOREAPP3_1_OR_GREATER + Assert.Equal("enrichedValueOnStart", activity?.GetTagItem("enrichedKey_onStart")); +#endif + } +#if NETCOREAPP3_1_OR_GREATER + [Fact] + public async Task TracerProviderBuilder_AddTraceOnStartEnricherT_AddsEnricherAndAppliesEnrichment() + { + using var activitySource = new ActivitySource(TestActivitySourceName); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddSource(TestActivitySourceName) + .AddTraceEnricher())) + .StartAsync(); + + var enrichmentProcessor = host.Services.GetRequiredService(); + Assert.NotNull(enrichmentProcessor); + + var activity = activitySource.StartActivity("Test"); + activity?.AddTag("internalKey", "internalValue"); + activity?.Stop(); + await host.StopAsync(); + + Assert.Equal("internalValue", activity?.GetTagItem("internalKey")); + Assert.Equal("enrichedValueOnStart", activity?.GetTagItem("enrichedKey_onStart")); + } + + [Fact] + public async Task IServiceCollection_AddTraceOnStartEnricherT_AddsEnricherAndAppliesEnrichment() + { + using var activitySource = new ActivitySource(TestActivitySourceName); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddTraceEnricher() + .AddSource(TestActivitySourceName))) + .StartAsync(); + + var enrichmentProcessor = host.Services.GetRequiredService(); + Assert.NotNull(enrichmentProcessor); + + var activity = activitySource.StartActivity("Test"); + activity?.AddTag("internalKey", "internalValue"); + activity?.Stop(); + await host.StopAsync(); + + Assert.Equal("internalValue", activity?.GetTagItem("internalKey")); + Assert.Equal("enrichedValueOnStart", activity?.GetTagItem("enrichedKey_onStart")); + } +#endif + [Fact] + public async Task TracerProviderBuilder_AddTraceEnricher_AddsEnricherAndAppliesEnrichment() + { + using var activitySource = new ActivitySource(TestActivitySourceName); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddSource(TestActivitySourceName) + .AddTraceEnricher(new TestTraceEnricher()))) + .StartAsync(); + + var enrichmentProcessor = host.Services.GetRequiredService(); + Assert.NotNull(enrichmentProcessor); + + var activity = activitySource.StartActivity("Test"); + activity?.AddTag("internalKey", "internalValue"); + activity?.Stop(); + await host.StopAsync(); + + Assert.Equal("internalValue", activity?.GetTagItem("internalKey")); + Assert.Equal(1, activity?.GetTagItem("enrichedKey")); +#if NETCOREAPP3_1_OR_GREATER + Assert.Equal("enrichedValueOnStart", activity?.GetTagItem("enrichedKey_onStart")); +#endif + } + + [Fact] + public async Task IServiceCollection_AddTraceEnricher_AddsEnricherAndAppliesEnrichment() + { + using var activitySource = new ActivitySource(TestActivitySourceName); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddTraceEnricher(new TestTraceEnricher()) + .AddOpenTelemetry().WithTracing(builder => builder + .AddSource(TestActivitySourceName))) + .StartAsync(); + + var enrichmentProcessor = host.Services.GetRequiredService(); + Assert.NotNull(enrichmentProcessor); + + var activity = activitySource.StartActivity("Test"); + activity?.AddTag("internalKey", "internalValue"); + activity?.Stop(); + await host.StopAsync(); + + Assert.Equal("internalValue", activity?.GetTagItem("internalKey")); + Assert.Equal(1, activity?.GetTagItem("enrichedKey")); +#if NETCOREAPP3_1_OR_GREATER + Assert.Equal("enrichedValueOnStart", activity?.GetTagItem("enrichedKey_onStart")); +#endif + } + + [Fact] + public async Task TracerProviderBuilder_AddTraceEnricher_MultipleEnrichersShouldAddOnlyOneEnrichmentProcessor() + { + using ActivitySource activitySource = new ActivitySource(TestActivitySourceName); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOpenTelemetry().WithTracing(builder => builder + .AddSource(TestActivitySourceName) + .AddTraceEnricher() + .AddTraceEnricher(new TestTraceEnricher2()))) + .StartAsync(); + + var activity = activitySource.StartActivity("Test"); + activity?.AddTag("internalKey", "internalValue"); + activity?.Stop(); + await host.StopAsync(); + + Assert.Equal("internalValue", activity?.GetTagItem("internalKey")); + Assert.Equal(1, activity?.GetTagItem("enrichedKey")); + Assert.Equal(1, activity?.GetTagItem("enrichedKey2")); +#if NETCOREAPP3_1_OR_GREATER + Assert.Equal("enrichedValueOnStart", activity?.GetTagItem("enrichedKey_onStart")); + Assert.Equal("enrichedValueOnStart2", activity?.GetTagItem("enrichedKey_onStart2")); + int expectedTagsCount = 5; +#else + int expectedTagsCount = 3; +#endif + int actualTagsCount = 0; + if (activity?.TagObjects != null) + { + foreach (var tg in activity.TagObjects) + { + actualTagsCount++; + } + } + + Assert.Equal(expectedTagsCount, actualTagsCount); + } + + [Fact] + public async Task IServiceCollection_AddTraceEnricher_MultipleEnrichersShouldAddOnlyOneEnrichmentProcessor() + { + using ActivitySource activitySource = new ActivitySource(TestActivitySourceName); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddTraceEnricher() + .AddTraceEnricher(new TestTraceEnricher2()) + .AddOpenTelemetry().WithTracing(builder => builder + .AddSource(TestActivitySourceName))) + .StartAsync(); + + var activity = activitySource.StartActivity("Test"); + activity?.AddTag("internalKey", "internalValue"); + activity?.Stop(); + await host.StopAsync(); + + Assert.Equal("internalValue", activity?.GetTagItem("internalKey")); + Assert.Equal(1, activity?.GetTagItem("enrichedKey")); + Assert.Equal(1, activity?.GetTagItem("enrichedKey2")); +#if NETCOREAPP3_1_OR_GREATER + Assert.Equal("enrichedValueOnStart", activity?.GetTagItem("enrichedKey_onStart")); + Assert.Equal("enrichedValueOnStart2", activity?.GetTagItem("enrichedKey_onStart2")); + int expectedTagsCount = 5; +#else + int expectedTagsCount = 3; +#endif + int actualTagsCount = 0; + if (activity?.TagObjects != null) + { + foreach (var tg in activity.TagObjects) + { + actualTagsCount++; + } + } + + Assert.Equal(expectedTagsCount, actualTagsCount); + } + + internal sealed class TestTraceEnricher : ITraceEnricher + { + public int TimesCalled { get; private set; } + public void Enrich(Activity activity) + { + activity.SetTag("enrichedKey", ++TimesCalled); + } + + public void EnrichOnActivityStart(Activity activity) + { + activity.SetTag("enrichedKey_onStart", "enrichedValueOnStart"); + } + } + + internal sealed class TestTraceEnricher2 : ITraceEnricher + { + public int TimesCalled { get; private set; } + public void Enrich(Activity activity) + { + activity.SetTag("enrichedKey2", ++TimesCalled); + } + + public void EnrichOnActivityStart(Activity activity) + { + activity.SetTag("enrichedKey_onStart2", "enrichedValueOnStart2"); + } + } + +#if NETCOREAPP3_1_OR_GREATER + internal sealed class TestTraceOnStartEnricher : ITraceEnricher + { + public void Enrich(Activity activity) + { + // no-op. + } + + public void EnrichOnActivityStart(Activity activity) + { + activity.SetTag("enrichedKey_onStart", "enrichedValueOnStart"); + } + } +#endif +} diff --git a/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json new file mode 100644 index 0000000000..acb64d88c7 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Telemetry.Tests/appsettings.json @@ -0,0 +1,33 @@ +{ + "Logging": { + "UseFormattedMessage": true + }, + "MeteringWithOverrides": { + "MeterState": "Enabled", + "MeterStateOverrides": { + "testMeter2": "Disabled", + "R9.Test": "Disabled", + "R9.Test.Internal": "Enabled", + "R9.Test.External": "Disabled" + } + }, + "MeteringWithOverridesWithEmptyOverride": { + "MeterState": "Enabled", + "MeterStateOverrides": { + "": "Disabled" + } + }, + "SamplingOptions": { + }, + "ValidConfig": { + "SamplingInterval": "00:02:00", + "Counters": { + "Key1": [ "one", "two", "three", "four" ], + "Key2": [ "ABC" ] + }, + "EventListenerRecyclingInterval": "00:20:00" + }, + "InvalidConfig": { + "SamplingInterval": "00:00:00" + } +} diff --git a/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/FakeTimeProviderTests.cs b/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/FakeTimeProviderTests.cs new file mode 100644 index 0000000000..cca94a3e5b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/FakeTimeProviderTests.cs @@ -0,0 +1,289 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Microsoft.Extensions.Time.Testing.Test; + +public class FakeTimeProviderTests +{ + [Fact] + public void DefaultCtor() + { + var timeProvider = new FakeTimeProvider(); + + var now = timeProvider.GetUtcNow(); + var timestamp = timeProvider.GetTimestamp(); + var frequency = timeProvider.TimestampFrequency; + + Assert.Equal(2000, now.Year); + Assert.Equal(1, now.Month); + Assert.Equal(1, now.Day); + Assert.Equal(0, now.Hour); + Assert.Equal(0, now.Minute); + Assert.Equal(0, now.Second); + Assert.Equal(0, now.Millisecond); + Assert.Equal(TimeSpan.Zero, now.Offset); + Assert.Equal(10_000_000, frequency); + + var timestamp2 = timeProvider.GetTimestamp(); + var frequency2 = timeProvider.TimestampFrequency; + now = timeProvider.GetUtcNow(); + + Assert.Equal(2000, now.Year); + Assert.Equal(1, now.Month); + Assert.Equal(1, now.Day); + Assert.Equal(0, now.Hour); + Assert.Equal(0, now.Minute); + Assert.Equal(0, now.Second); + Assert.Equal(0, now.Millisecond); + Assert.Equal(10_000_000, frequency2); + Assert.Equal(timestamp2, timestamp); + } + + [Fact] + public void RichCtor() + { + var timeProvider = new FakeTimeProvider(new DateTimeOffset(2001, 2, 3, 4, 5, 6, TimeSpan.Zero)); + + timeProvider.Advance(TimeSpan.FromMilliseconds(8)); + var pnow = timeProvider.GetTimestamp(); + var frequency = timeProvider.TimestampFrequency; + var now = timeProvider.GetUtcNow(); + + Assert.Equal(2001, now.Year); + Assert.Equal(2, now.Month); + Assert.Equal(3, now.Day); + Assert.Equal(4, now.Hour); + Assert.Equal(5, now.Minute); + Assert.Equal(6, now.Second); + Assert.Equal(TimeSpan.Zero, now.Offset); + Assert.Equal(8, now.Millisecond); + Assert.Equal(10_000_000, frequency); + + timeProvider.Advance(TimeSpan.FromMilliseconds(8)); + var pnow2 = timeProvider.GetTimestamp(); + var frequency2 = timeProvider.TimestampFrequency; + now = timeProvider.GetUtcNow(); + + Assert.Equal(2001, now.Year); + Assert.Equal(2, now.Month); + Assert.Equal(3, now.Day); + Assert.Equal(4, now.Hour); + Assert.Equal(5, now.Minute); + Assert.Equal(6, now.Second); + Assert.Equal(16, now.Millisecond); + Assert.Equal(10_000_000, frequency2); + Assert.True(pnow2 > pnow); + } + + [Fact] + public void LocalTimeZoneIsUtc() + { + var timeProvider = new FakeTimeProvider(); + var localTimeZone = timeProvider.LocalTimeZone; + + Assert.Equal(TimeZoneInfo.Utc, localTimeZone); + } + + [Fact] + public void GetTimestampSyncWithUtcNow() + { + var timeProvider = new FakeTimeProvider(new DateTimeOffset(2001, 2, 3, 4, 5, 6, TimeSpan.Zero)); + + var initialTimeUtcNow = timeProvider.GetUtcNow(); + var initialTimestamp = timeProvider.GetTimestamp(); + + timeProvider.SetUtcNow(timeProvider.GetUtcNow().AddMilliseconds(1234)); + + var finalTimeUtcNow = timeProvider.GetUtcNow(); + var finalTimeTimestamp = timeProvider.GetTimestamp(); + + var utcDelta = finalTimeUtcNow - initialTimeUtcNow; + var perfDelta = finalTimeTimestamp - initialTimestamp; + var elapsedTime = timeProvider.GetElapsedTime(initialTimestamp, finalTimeTimestamp); + + Assert.Equal(1, utcDelta.Seconds); + Assert.Equal(234, utcDelta.Milliseconds); + Assert.Equal(1234D, utcDelta.TotalMilliseconds); + Assert.Equal(1.234D, (double)perfDelta / timeProvider.TimestampFrequency, 3); + Assert.Equal(1234, elapsedTime.TotalMilliseconds); + } + + [Fact] + public void AdvanceGoesForward() + { + var timeProvider = new FakeTimeProvider(new DateTimeOffset(2001, 2, 3, 4, 5, 6, TimeSpan.Zero)); + + var initialTimeUtcNow = timeProvider.GetUtcNow(); + var initialTimestamp = timeProvider.GetTimestamp(); + + timeProvider.Advance(TimeSpan.FromMilliseconds(1234)); + + var finalTimeUtcNow = timeProvider.GetUtcNow(); + var finalTimeTimestamp = timeProvider.GetTimestamp(); + + var utcDelta = finalTimeUtcNow - initialTimeUtcNow; + var perfDelta = finalTimeTimestamp - initialTimestamp; + var elapsedTime = timeProvider.GetElapsedTime(initialTimestamp, finalTimeTimestamp); + + Assert.Equal(1, utcDelta.Seconds); + Assert.Equal(234, utcDelta.Milliseconds); + Assert.Equal(1234D, utcDelta.TotalMilliseconds); + Assert.Equal(1.234D, (double)perfDelta / timeProvider.TimestampFrequency, 3); + Assert.Equal(1234, elapsedTime.TotalMilliseconds); + } + + [Fact] + public void ToStr() + { + var dto = new DateTimeOffset(new DateTime(2022, 1, 2, 3, 4, 5, 6), TimeSpan.Zero); + + var timeProvider = new FakeTimeProvider(dto); + Assert.Equal("2022-01-02T03:04:05.006", timeProvider.ToString()); + } + + private readonly TimeSpan _infiniteTimeout = TimeSpan.FromMilliseconds(-1); + + [Fact] + public void Delay_InvalidArgs() + { + var timeProvider = new FakeTimeProvider(); + _ = Assert.ThrowsAsync(() => timeProvider.Delay(TimeSpan.FromTicks(-1), CancellationToken.None)); + _ = Assert.ThrowsAsync(() => timeProvider.Delay(_infiniteTimeout, CancellationToken.None)); + } + + [Fact] + public async Task Delay_Zero() + { + var timeProvider = new FakeTimeProvider(); + var t = timeProvider.Delay(TimeSpan.Zero, CancellationToken.None); + await t; + + Assert.True(t.IsCompleted && !t.IsFaulted); + } + + [Fact] + public async Task Delay_Timeout() + { + var timeProvider = new FakeTimeProvider(); + + var delay = timeProvider.Delay(TimeSpan.FromMilliseconds(1), CancellationToken.None); + timeProvider.Advance(); + await delay; + + Assert.True(delay.IsCompleted); + Assert.False(delay.IsFaulted); + Assert.False(delay.IsCanceled); + } + + [Fact] + public async Task Delay_Cancelled() + { + var timeProvider = new FakeTimeProvider(); + + using var cs = new CancellationTokenSource(); + var delay = timeProvider.Delay(_infiniteTimeout, cs.Token); + Assert.False(delay.IsCompleted); + + cs.Cancel(); + +#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks + await Assert.ThrowsAsync(async () => await delay); +#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks + } + + [Fact] + public async Task CreateSource() + { + var timeProvider = new FakeTimeProvider(); + + using var cts = timeProvider.CreateCancellationTokenSource(TimeSpan.FromMilliseconds(1)); + timeProvider.Advance(); + + await Assert.ThrowsAsync(() => timeProvider.Delay(TimeSpan.FromTicks(1), cts.Token)); + } + + [Fact] + public async Task WaitAsync() + { + var timeProvider = new FakeTimeProvider(); + var source = new TaskCompletionSource(); + +#if NET8_0_OR_GREATER + await Assert.ThrowsAsync(() => source.Task.WaitAsync(TimeSpan.FromTicks(-1), timeProvider, CancellationToken.None)); +#else + await Assert.ThrowsAsync(() => source.Task.WaitAsync(TimeSpan.FromTicks(-1), timeProvider, CancellationToken.None)); +#endif + await Assert.ThrowsAsync(() => source.Task.WaitAsync(TimeSpan.FromMilliseconds(-2), timeProvider, CancellationToken.None)); + + var t = source.Task.WaitAsync(TimeSpan.FromSeconds(100000), timeProvider, CancellationToken.None); + while (!t.IsCompleted) + { + timeProvider.Advance(); + await Task.Delay(1); + _ = source.TrySetResult(true); + } + + Assert.True(t.IsCompleted); + Assert.False(t.IsFaulted); + Assert.False(t.IsCanceled); + } + + [Fact] + public async Task WaitAsync_InfiniteTimeout() + { + var timeProvider = new FakeTimeProvider(); + var source = new TaskCompletionSource(); + + var t = source.Task.WaitAsync(_infiniteTimeout, timeProvider, CancellationToken.None); + while (!t.IsCompleted) + { + timeProvider.Advance(); + await Task.Delay(1); + _ = source.TrySetResult(true); + } + + Assert.True(t.IsCompleted); + Assert.False(t.IsFaulted); + Assert.False(t.IsCanceled); + } + + [Fact] + public async Task WaitAsync_Timeout() + { + var timeProvider = new FakeTimeProvider(); + var source = new TaskCompletionSource(); + + var t = source.Task.WaitAsync(TimeSpan.FromMilliseconds(1), timeProvider, CancellationToken.None); + while (!t.IsCompleted) + { + timeProvider.Advance(); + await Task.Delay(1); + } + + Assert.True(t.IsCompleted); + Assert.True(t.IsFaulted); + Assert.False(t.IsCanceled); + } + + [Fact] + public async Task WaitAsync_Cancel() + { + var timeProvider = new FakeTimeProvider(); + var source = new TaskCompletionSource(); + using var cts = new CancellationTokenSource(); + + var t = source.Task.WaitAsync(_infiniteTimeout, timeProvider, cts.Token); + cts.Cancel(); + +#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks + await Assert.ThrowsAsync(() => t).ConfigureAwait(false); +#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks + } +} + diff --git a/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/FakeTimeProviderTimerTests.cs b/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/FakeTimeProviderTimerTests.cs new file mode 100644 index 0000000000..64cb4aecec --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/FakeTimeProviderTimerTests.cs @@ -0,0 +1,274 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Microsoft.Extensions.Time.Testing.Test; + +public class FakeTimeProviderTimerTests +{ + private void EmptyTimerTarget(object? o) + { + // no-op for timer callbacks + } + + [Fact] + public void TimerNonPeriodicPeriodZero() + { + var counter = 0; + var timeProvider = new FakeTimeProvider(); + using var timer = timeProvider.CreateTimer(_ => { counter++; }, null, TimeSpan.FromMilliseconds(10), TimeSpan.Zero); + + var value1 = counter; + timeProvider.Advance(TimeSpan.FromMilliseconds(20)); + + var value2 = counter; + + timeProvider.Advance(TimeSpan.FromMilliseconds(1000)); + + var value3 = counter; + + Assert.Equal(0, value1); + Assert.Equal(1, value2); + Assert.Equal(1, value3); + } + + [Fact] + public void TimerNonPeriodicPeriodInfinite() + { + var counter = 0; + var timeProvider = new FakeTimeProvider(); + using var timer = timeProvider.CreateTimer(_ => { counter++; }, null, TimeSpan.FromMilliseconds(10), Timeout.InfiniteTimeSpan); + + var value1 = counter; + timeProvider.Advance(TimeSpan.FromMilliseconds(20)); + + var value2 = counter; + + timeProvider.Advance(TimeSpan.FromMilliseconds(1000)); + + var value3 = counter; + + Assert.Equal(0, value1); + Assert.Equal(1, value2); + Assert.Equal(1, value3); + } + + [Fact] + public void TimerStartsImmediately() + { + var counter = 0; + var timeProvider = new FakeTimeProvider(); + using var timer = timeProvider.CreateTimer(_ => { counter++; }, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan); + + var value1 = counter; + + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + + var value2 = counter; + + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + + var value3 = counter; + + Assert.Equal(1, value1); + Assert.Equal(1, value2); + Assert.Equal(1, value3); + } + + [Fact] + public void NoDueTime_TimerDoesntStart() + { + var counter = 0; + var timeProvider = new FakeTimeProvider(); + var timer = timeProvider.CreateTimer(_ => { counter++; }, null, Timeout.InfiniteTimeSpan, TimeSpan.FromMilliseconds(10)); + + var value1 = counter; + + timeProvider.Advance(TimeSpan.FromMilliseconds(1)); + + var value2 = counter; + + timeProvider.Advance(TimeSpan.FromMilliseconds(50)); + + var value3 = counter; + + Assert.Equal(0, value1); + Assert.Equal(0, value2); + Assert.Equal(0, value3); + } + + [Fact] + public void TimerTriggersPeriodically() + { + var counter = 0; + var timeProvider = new FakeTimeProvider(); + var timer = timeProvider.CreateTimer(_ => { counter++; }, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(10)); + + var value1 = counter; + + timeProvider.Advance(TimeSpan.FromMilliseconds(1)); + + var value2 = counter; + + timeProvider.Advance(TimeSpan.FromMilliseconds(10)); + + var value3 = counter; + + timeProvider.Advance(TimeSpan.FromMilliseconds(10)); + + var value4 = counter; + + Assert.Equal(1, value1); + Assert.Equal(1, value2); + Assert.Equal(2, value3); + Assert.Equal(3, value4); + } + + [Fact] + public void LongPausesTriggerSingleCallback() + { + var counter = 0; + var timeProvider = new FakeTimeProvider(); + var timer = timeProvider.CreateTimer(_ => { counter++; }, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(10)); + + var value1 = counter; + + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + + var value2 = counter; + + Assert.Equal(1, value1); + Assert.Equal(2, value2); + } + + [Fact] + public async Task TaskDelayWithFakeTimeProviderAdvanced() + { + var fakeTimeProvider = new FakeTimeProvider(); + using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(1000)); + + var task = fakeTimeProvider.Delay(TimeSpan.FromMilliseconds(10000), cancellationTokenSource.Token).ConfigureAwait(false); + + fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(10000)); + + await task; + + Assert.False(cancellationTokenSource.Token.IsCancellationRequested); + } + + [Fact] + public async Task TaskDelayWithFakeTimeProviderStopped() + { + var fakeTimeProvider = new FakeTimeProvider(); + using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); + + await Assert.ThrowsAsync(async () => + { + await fakeTimeProvider.Delay( + TimeSpan.FromMilliseconds(10000), + cancellationTokenSource.Token) + .ConfigureAwait(false); + }); + } + + [Fact] + public void TimerChangeDueTimeOutOfRangeThrows() + { + using var t = new FakeTimeProviderTimer(new FakeTimeProvider(), TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(1), new TimerCallback(EmptyTimerTarget), null); + + Assert.Throws("dueTime", () => t.Change(TimeSpan.FromMilliseconds(-2), TimeSpan.FromMilliseconds(1))); + Assert.Throws("dueTime", () => t.Change(TimeSpan.FromMilliseconds(-2), TimeSpan.FromSeconds(1))); + Assert.Throws("dueTime", () => t.Change(TimeSpan.FromMilliseconds(0xFFFFFFFFL), TimeSpan.FromMilliseconds(1))); + Assert.Throws("dueTime", () => t.Change(TimeSpan.FromMilliseconds(0xFFFFFFFFL), TimeSpan.FromSeconds(1))); + } + + [Fact] + public void TimerChangePeriodOutOfRangeThrows() + { + using var t = new FakeTimeProviderTimer(new FakeTimeProvider(), TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(1), new TimerCallback(EmptyTimerTarget), null); + + Assert.Throws("period", () => t.Change(TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(-2))); + Assert.Throws("period", () => t.Change(TimeSpan.FromSeconds(1), TimeSpan.FromMilliseconds(-2))); + Assert.Throws("period", () => t.Change(TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(0xFFFFFFFFL))); + Assert.Throws("period", () => t.Change(TimeSpan.FromSeconds(1), TimeSpan.FromMilliseconds(0xFFFFFFFFL))); + } + + [Fact] + public void Timer_Change_AfterDispose_Test() + { + var t = new FakeTimeProviderTimer(new FakeTimeProvider(), TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(1), new TimerCallback(EmptyTimerTarget), null); + + Assert.True(t.Change(TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(1))); + t.Dispose(); + Assert.False(t.Change(TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(1))); + } + + [Fact] + public void WaiterRemovedAfterDispose() + { + var timer1Counter = 0; + var timer2Counter = 0; + + var timeProvider = new FakeTimeProvider(); + var waitersCountStart = timeProvider.Waiters.Count; + + var timer1 = timeProvider.CreateTimer(_ => timer1Counter++, null, TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(1)); + var timer2 = timeProvider.CreateTimer(_ => timer2Counter++, null, TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(1)); + + var waitersCountDuring = timeProvider.Waiters.Count; + + timeProvider.Advance(TimeSpan.FromMilliseconds(1)); + + timer1.Dispose(); + + timeProvider.Advance(TimeSpan.FromMilliseconds(1)); + + var waitersCountAfter = timeProvider.Waiters.Count; + + Assert.Equal(0, waitersCountStart); + Assert.Equal(2, waitersCountDuring); + Assert.Equal(1, timer1Counter); + Assert.Equal(2, timer2Counter); + Assert.Equal(1, waitersCountAfter); + } + +#if RELEASE // In Release only since this might not work if the timer reference being tracked by the debugger + [Fact] + public void WaiterRemovedWhenCollectedWithoutDispose() + { + var timer1Counter = 0; + var timer2Counter = 0; + + var timeProvider = new FakeTimeProvider(); + var waitersCountStart = timeProvider.Waiters.Count; + + var timer1 = timeProvider.CreateTimer(_ => timer1Counter++, null, TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(1)); + var timer2 = timeProvider.CreateTimer(_ => timer2Counter++, null, TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(1)); + + var waitersCountDuring = timeProvider.Waiters.Count; + + timeProvider.Advance(TimeSpan.FromMilliseconds(1)); + + // Force the finalizer on timer1 to ensure Dispose is releasing the waiter object + // even when a Timer is not disposed + timer1 = null; + GC.Collect(); + GC.WaitForPendingFinalizers(); + + timeProvider.Advance(TimeSpan.FromMilliseconds(1)); + + var waitersCountAfter = timeProvider.Waiters.Count; + + Assert.Equal(0, waitersCountStart); + Assert.Equal(2, waitersCountDuring); + Assert.Equal(1, timer1Counter); + Assert.Equal(2, timer2Counter); + Assert.Equal(1, waitersCountAfter); + } +#endif +} diff --git a/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/Microsoft.Extensions.TimeProvider.Testing.Tests.csproj b/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/Microsoft.Extensions.TimeProvider.Testing.Tests.csproj new file mode 100644 index 0000000000..82fe9b7d93 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.TimeProvider.Testing.Tests/Microsoft.Extensions.TimeProvider.Testing.Tests.csproj @@ -0,0 +1,10 @@ + + + Microsoft.Extensions.Time.Testing.Test + Tests for Microsoft.Extensions.TimeProvider.Testing.Testing + + + + + + diff --git a/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/BatchItemTests.cs b/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/BatchItemTests.cs new file mode 100644 index 0000000000..7e08bc01c8 --- /dev/null +++ b/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/BatchItemTests.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Cloud.DocumentDb.Test; + +public class BatchItemTests +{ + [Fact] + public void TestProperties() + { + BatchItem item = new( + BatchOperation.Delete, + 5, + "id", + "etag"); + + Assert.Equal(BatchOperation.Delete, item.Operation); + Assert.Equal("etag", item.ItemVersion); + Assert.Equal("id", item.Id); + Assert.Equal(5, item.Item); + } + + [Fact] + public void TestDefaults() + { + BatchItem item = new(BatchOperation.Delete); + + Assert.Equal(BatchOperation.Delete, item.Operation); + Assert.Null(item.ItemVersion); + Assert.Null(item.Id); + Assert.Null(item.Item); + } +} diff --git a/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/DatabaseOptionsTests.cs b/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/DatabaseOptionsTests.cs new file mode 100644 index 0000000000..6203a7d17f --- /dev/null +++ b/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/DatabaseOptionsTests.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; +using FluentAssertions; +using Xunit; + +namespace System.Cloud.DocumentDb.Test; + +public class DatabaseOptionsTests +{ + [Fact] + public void TestProperties() + { + TimeSpan testTimeout = new TimeSpan(0, 1, 4, 0); + + DatabaseOptions config = new() + { + DatabaseName = "name", + DefaultRegionalDatabaseName = "regional name", + PrimaryKey = "some value", + Endpoint = new Uri("https://endpoint"), + IdleTcpConnectionTimeout = testTimeout, + Throughput = new(5), + }; + + Assert.Equal("name", config.DatabaseName); + + Assert.Equal("regional name", config.DefaultRegionalDatabaseName); + Assert.Equal("some value", config.PrimaryKey); + Assert.Equal(new Uri("https://endpoint"), config.Endpoint); + Assert.Equal(testTimeout, config.IdleTcpConnectionTimeout); + Assert.Equal(5, config.Throughput.Value); + + Assert.Equal(0, config.FailoverRegions.Count); + Assert.Equal(0, config.RegionalDatabaseOptions.Count); + } + + [Fact] + public void TestDefaults() + { + DatabaseOptions config = new DatabaseOptions(); + + Assert.Equal(string.Empty, config.DatabaseName); + Throughput.Unlimited.Should().Be(config.Throughput); + } + + [Fact] + public void TestRangeValues() + { + TestIdleTimeoutValue(new TimeSpan(0), false); + TestIdleTimeoutValue(new TimeSpan(0, 9, 59), false); + TestIdleTimeoutValue(new TimeSpan(0, 10, 00), true); + TestIdleTimeoutValue(new TimeSpan(30, 0, 0, 0), true); + TestIdleTimeoutValue(new TimeSpan(30, 0, 0, 1), false); + } + + private static void TestIdleTimeoutValue(TimeSpan timeToTest, bool expectedResult) + { + DatabaseOptions config = new DatabaseOptions + { + DatabaseName = "required", + IdleTcpConnectionTimeout = timeToTest, + Endpoint = new Uri("https://endpoint") + }; + bool result = Validator.TryValidateObject( + config, + new ValidationContext(config, null, null), + null, + true); + Assert.Equal(expectedResult, result); + } +} diff --git a/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/ExceptionsTests.cs b/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/ExceptionsTests.cs new file mode 100644 index 0000000000..2eb6303f47 --- /dev/null +++ b/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/ExceptionsTests.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using FluentAssertions; +using Xunit; + +namespace System.Cloud.DocumentDb.Test; + +public class ExceptionsTests +{ + private const string TestMessage = "message"; + + private const int TestStatus = (int)HttpStatusCode.Accepted; + private const int DefaultStatus = (int)HttpStatusCode.InternalServerError; + + private readonly TimeSpan _testTime = TimeSpan.MaxValue; + private readonly TimeSpan _defaultTime = TimeSpan.Zero; + + private readonly RequestInfo _requestInfo = new("region", "table", 5); + + private readonly Exception _testException = new DatabaseException("test"); + + [Fact] + public void TestConstructors() + { + VerifyException(new DatabaseException( + TestMessage, _testException, TestStatus, 0, _requestInfo), + TestMessage, _testException, TestStatus, _requestInfo); + VerifyException(new DatabaseServerException( + TestMessage, _testException, TestStatus, 0, _requestInfo), + TestMessage, _testException, TestStatus, _requestInfo); + VerifyRetryableException(new DatabaseRetryableException( + TestMessage, _testException, TestStatus, 0, _testTime, _requestInfo), + TestMessage, _testException, TestStatus, _testTime, _requestInfo); + + VerifyException(new DatabaseException( + TestMessage, _testException), + TestMessage, _testException, DefaultStatus, default); + VerifyException(new DatabaseClientException( + TestMessage, _testException), + TestMessage, _testException, DefaultStatus, default); + VerifyException(new DatabaseServerException( + TestMessage, _testException), + TestMessage, _testException, DefaultStatus, default); + VerifyRetryableException(new DatabaseRetryableException( + TestMessage, _testException), + TestMessage, _testException, DefaultStatus, _defaultTime, default); + + VerifyException(new DatabaseException( + TestMessage, DefaultStatus, 0, _requestInfo), + TestMessage, null, DefaultStatus, _requestInfo); + VerifyException(new DatabaseServerException( + TestMessage, DefaultStatus, 0, _requestInfo), + TestMessage, null, DefaultStatus, _requestInfo); + + VerifyException(new DatabaseException( + TestMessage), + TestMessage, null, DefaultStatus, default); + VerifyException(new DatabaseClientException( + TestMessage), + TestMessage, null, DefaultStatus, default); + VerifyException(new DatabaseServerException( + TestMessage), + TestMessage, null, DefaultStatus, default); + VerifyRetryableException(new DatabaseRetryableException( + TestMessage), + TestMessage, null, DefaultStatus, _defaultTime, default); + + VerifyException(new DatabaseException(), + null, null, DefaultStatus, default); + VerifyException(new DatabaseClientException(), + null, null, DefaultStatus, default); + VerifyException(new DatabaseServerException(), + null, null, DefaultStatus, default); + VerifyRetryableException(new DatabaseRetryableException(), + null, null, DefaultStatus, _defaultTime, default); + } + + private static void VerifyException(DatabaseException exception, + string? testMessage, Exception? testException, int? httpStatusCode, RequestInfo info) + { + Assert.Equal(testMessage ?? $"Exception of type '{exception.GetType().FullName}' was thrown.", exception.Message); + Assert.Equal(testException, exception.InnerException); + Assert.Equal((HttpStatusCode?)httpStatusCode, exception.HttpStatusCode); + Assert.Equal(0, exception.SubStatusCode); + exception.RequestInfo.Should().Be(info); + } + + private static void VerifyRetryableException(DatabaseRetryableException exception, + string? testMessage, Exception? testException, int httpStatusCode, + TimeSpan testTime, RequestInfo info) + { + Assert.Equal(testMessage ?? $"Exception of type '{exception.GetType().FullName}' was thrown.", exception.Message); + Assert.Equal(testException, exception.InnerException); + Assert.Equal((HttpStatusCode)httpStatusCode, exception.HttpStatusCode); + Assert.Equal(0, exception.SubStatusCode); + Assert.Equal(testTime, exception.RetryAfter); + exception.RequestInfo.Should().Be(info); + } +} diff --git a/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/PatchOperationTest.cs b/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/PatchOperationTest.cs new file mode 100644 index 0000000000..38072d229e --- /dev/null +++ b/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/PatchOperationTest.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Xunit; + +namespace System.Cloud.DocumentDb.Test; + +public class PatchOperationTest +{ + [Fact] + public void TestProperties() + { + string path = "/path"; + string stringValue = "sval"; + long longValue = 5; + double doubleValue = 6; + + Validate(PatchOperation.Add(path, stringValue), path, PatchOperationType.Add, stringValue); + Validate(PatchOperation.Remove(path), path, PatchOperationType.Remove, string.Empty); + Validate(PatchOperation.Replace(path, stringValue), path, PatchOperationType.Replace, stringValue); + Validate(PatchOperation.Set(path, stringValue), path, PatchOperationType.Set, stringValue); + Validate(PatchOperation.Increment(path, longValue), path, PatchOperationType.Increment, longValue); + Validate(PatchOperation.Increment(path, doubleValue), path, PatchOperationType.Increment, doubleValue); + } + + private static void Validate(PatchOperation operation, string path, PatchOperationType type, object value) + { + operation.Path.Should().Be(path); + operation.OperationType.Should().Be(type); + operation.Value.Should().Be(value); + } +} diff --git a/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/QueryTest.cs b/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/QueryTest.cs new file mode 100644 index 0000000000..7063339a49 --- /dev/null +++ b/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/QueryTest.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Cloud.DocumentDb.Test; + +public class QueryTest +{ + [Fact] + public void TestProperties() + { + const string QueryText = "query"; + + Query query = new Query(QueryText); + + Assert.Equal(QueryText, query.QueryText); + Assert.Equal(0, query.Parameters.Count); + } +} diff --git a/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/RegionalDatabaseOptionsTests.cs b/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/RegionalDatabaseOptionsTests.cs new file mode 100644 index 0000000000..a295e767a6 --- /dev/null +++ b/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/RegionalDatabaseOptionsTests.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace System.Cloud.DocumentDb.Test; + +public class RegionalDatabaseOptionsTests +{ + [Fact] + public void TestProperties() + { + RegionalDatabaseOptions config = new RegionalDatabaseOptions + { + DatabaseName = "name", + PrimaryKey = "key", + Endpoint = new Uri("https://endpoint") + }; + + Assert.Equal("name", config.DatabaseName); + Assert.Equal(new Uri("https://endpoint"), config.Endpoint); + Assert.Equal("key", config.PrimaryKey); + + Assert.Equal(0, config.FailoverRegions.Count); + } +} diff --git a/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/RequestOptionsTests.cs b/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/RequestOptionsTests.cs new file mode 100644 index 0000000000..d95cb89af0 --- /dev/null +++ b/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/RequestOptionsTests.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Cloud.DocumentDb.Test; + +public class RequestOptionsTests +{ + [Fact] + public void TestProperties() + { + QueryRequestOptions options = new QueryRequestOptions + { + PartitionKey = new[] { "partition" }, + ConsistencyLevel = ConsistencyLevel.Eventual, + SessionToken = "session", + ItemVersion = "etag", + Document = 5, + Region = "region", + ResponseContinuationTokenLimitInKb = 6, + ContentResponseOnWrite = true, + EnableScan = true, + EnableLowPrecisionOrderBy = true, + MaxBufferedItemCount = 7, + MaxResults = 8, + MaxConcurrency = 9, + ContinuationToken = "10", + }; + + Assert.Equal("partition", options.PartitionKey[0]); + Assert.Equal("session", options.SessionToken); + Assert.Equal("etag", options.ItemVersion); + Assert.Equal(5, options.Document); + Assert.Equal(ConsistencyLevel.Eventual, options.ConsistencyLevel); + Assert.Equal("region", options.Region); + + Assert.Equal(6, options.ResponseContinuationTokenLimitInKb); + Assert.True(options.ContentResponseOnWrite); + Assert.True(options.EnableScan); + Assert.True(options.EnableLowPrecisionOrderBy); + Assert.Equal(7, options.MaxBufferedItemCount); + Assert.Equal(8, options.MaxResults); + Assert.Equal(9, options.MaxConcurrency); + Assert.Equal("10", options.ContinuationToken); + Assert.Equal(FetchMode.FetchAll, options.FetchCondition); + } +} diff --git a/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/System.Cloud.DatabaseDb.Abstractions.Tests.csproj b/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/System.Cloud.DatabaseDb.Abstractions.Tests.csproj new file mode 100644 index 0000000000..6a66205f81 --- /dev/null +++ b/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/System.Cloud.DatabaseDb.Abstractions.Tests.csproj @@ -0,0 +1,10 @@ + + + System.Cloud.DocumentDb + Unit tests for System.Cloud.DocumentDb.Abstractions. + + + + + + diff --git a/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/TableOptionsTests.cs b/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/TableOptionsTests.cs new file mode 100644 index 0000000000..280bb75ae1 --- /dev/null +++ b/test/Libraries/System.Cloud.DocumentDb.Abstractions.Tests/TableOptionsTests.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using Xunit; + +namespace System.Cloud.DocumentDb.Test; + +public class TableOptionsTests +{ + [Fact] + public void TestProperties() + { + const string StoreName = "store name"; + TimeSpan timeToLive = new TimeSpan(1); + const string PartitionIdPath = "partition"; + + TableOptions options = new() + { + TableName = StoreName, + TimeToLive = timeToLive, + PartitionIdPath = PartitionIdPath, + IsRegional = true, + IsLocatorRequired = true, + Throughput = new(5), + }; + + Assert.Equal(StoreName, options.TableName); + Assert.Equal(timeToLive, options.TimeToLive); + Assert.Equal(PartitionIdPath, options.PartitionIdPath); + Assert.True(options.IsRegional); + Assert.True(options.IsLocatorRequired); + Assert.Equal(5, options.Throughput.Value); + } + + [Fact] + public void TestDefaults() + { + TableOptions config = new TableOptions(); + + Assert.Equal(Timeout.InfiniteTimeSpan, config.TimeToLive); + Assert.Equal(string.Empty, config.TableName); + + TableInfo info = new TableInfo(config); + + info = new TableInfo(info); + Assert.False(info.IsRegional); + Assert.Equal(string.Empty, info.TableName); + + info = new TableInfo(info, "123", true); + Assert.True(info.IsRegional); + Assert.Equal("123", info.TableName); + } +} diff --git a/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Consumers/DerivedConsumer.cs b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Consumers/DerivedConsumer.cs new file mode 100644 index 0000000000..6bc71473f2 --- /dev/null +++ b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Consumers/DerivedConsumer.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace System.Cloud.Messaging.Tests.Data.Consumers; + +internal class DerivedConsumer : BaseMessageConsumer +{ + public DerivedConsumer(IMessageSource source, IMessageDelegate messageDelegate, ILogger logger) + : base(source, messageDelegate, logger) + { + } + + /// + protected override ValueTask OnMessageProcessingFailureAsync(MessageContext context, Exception exception) => default; +} diff --git a/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Consumers/OverridenConsumer.cs b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Consumers/OverridenConsumer.cs new file mode 100644 index 0000000000..69b69b9ae6 --- /dev/null +++ b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Consumers/OverridenConsumer.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace System.Cloud.Messaging.Tests.Data.Consumers; + +internal class OverridenConsumer : BaseMessageConsumer +{ + public OverridenConsumer(IMessageSource source, IMessageDelegate messageDelegate, ILogger logger) + : base(source, messageDelegate, logger) + { + } + + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = $"Handled by {nameof(OnMessageProcessingFailureAsync)}")] + public override async ValueTask ExecuteAsync(CancellationToken cancellationToken) + { + while (true) + { + try + { + MessageContext messageContext = await MessageSource.ReadAsync(CancellationToken.None); + + _ = messageContext.TryGetSourcePayloadAsUTF8String(out string message); + if (string.IsNullOrEmpty(message)) + { + return; + } + + try + { + await MessageDelegate.InvokeAsync(messageContext); + await OnMessageProcessingCompletionAsync(messageContext); + } + catch (Exception exception) + { + await OnMessageProcessingFailureAsync(messageContext, exception); + } + } + catch (Exception exception) + { + throw new InvalidOperationException("Failure during processing.", exception); + } + } + } + + /// + protected override ValueTask OnMessageProcessingFailureAsync(MessageContext context, Exception exception) => default; +} diff --git a/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Consumers/SampleConsumer.cs b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Consumers/SampleConsumer.cs new file mode 100644 index 0000000000..802ce0414b --- /dev/null +++ b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Consumers/SampleConsumer.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Cloud.Messaging.Tests.Data.Consumers; + +internal class SampleConsumer : IMessageConsumer +{ + private readonly IMessageSource _messageSource; + private readonly IMessageDelegate _messageDelegate; + + public SampleConsumer(IMessageSource messageSource, IMessageDelegate messageDelegate) + { + _messageSource = messageSource; + _messageDelegate = messageDelegate; + } + + /// + public async ValueTask ExecuteAsync(CancellationToken cancellationToken) + { + while (true) + { + try + { + MessageContext messageContext = await _messageSource.ReadAsync(CancellationToken.None); + + _ = messageContext.TryGetSourcePayloadAsUTF8String(out string message); + if (string.IsNullOrEmpty(message)) + { + return; + } + + await _messageDelegate.InvokeAsync(messageContext); + } + catch (Exception exception) + { + throw new InvalidOperationException("Failure during processing.", exception); + } + } + } +} diff --git a/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Consumers/SingleMessageConsumer.cs b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Consumers/SingleMessageConsumer.cs new file mode 100644 index 0000000000..dc23b571f0 --- /dev/null +++ b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Consumers/SingleMessageConsumer.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace System.Cloud.Messaging.Tests.Data.Consumers; + +internal class SingleMessageConsumer : BaseMessageConsumer +{ + private readonly bool _shouldThrowExceptionDuringHandlingCompletion; + private readonly bool _shouldThrowExceptionDuringHandlingFailure; + + public SingleMessageConsumer(IMessageSource source, + IMessageDelegate messageDelegate, + ILogger logger, + bool shouldThrowExceptionDuringHandlingCompletion = false, + bool shouldThrowExceptionDuringHandlingFailure = false) + : base(source, messageDelegate, logger) + { + _shouldThrowExceptionDuringHandlingCompletion = shouldThrowExceptionDuringHandlingCompletion; + _shouldThrowExceptionDuringHandlingFailure = shouldThrowExceptionDuringHandlingFailure; + } + + public override async ValueTask ExecuteAsync(CancellationToken cancellationToken) + { + await FetchAndProcessMessageAsync(cancellationToken); + } + + /// + protected override ValueTask OnMessageProcessingCompletionAsync(MessageContext context) + { + if (_shouldThrowExceptionDuringHandlingCompletion) + { + throw new InvalidOperationException("Exception thrown during handling completion."); + } + + return default; + } + + /// + protected override ValueTask OnMessageProcessingFailureAsync(MessageContext context, Exception exception) + { + if (_shouldThrowExceptionDuringHandlingFailure) + { + throw new InvalidOperationException("Exception thrown during handling failure."); + } + + return default; + } +} diff --git a/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Delegates/SampleWriterDelegate.cs b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Delegates/SampleWriterDelegate.cs new file mode 100644 index 0000000000..f867da7906 --- /dev/null +++ b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Delegates/SampleWriterDelegate.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; + +namespace System.Cloud.Messaging.Tests.Data.Delegates; + +internal class SampleWriterDelegate : IMessageDelegate +{ + public IMessageDestination MessageDestination { get; } + + public SampleWriterDelegate(IMessageDestination messageWriter) + { + MessageDestination = messageWriter; + } + + /// + public async ValueTask InvokeAsync(MessageContext context) + { + await MessageDestination.WriteAsync(context); + } +} diff --git a/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Middlewares/SampleMiddleware.cs b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Middlewares/SampleMiddleware.cs new file mode 100644 index 0000000000..5301809fdd --- /dev/null +++ b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Middlewares/SampleMiddleware.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; + +namespace System.Cloud.Messaging.Tests.Data.Middlewares; + +internal class SampleMiddleware : IMessageMiddleware +{ + public IMessageDelegate MessageDelegate { get; } + + public SampleMiddleware(IMessageDelegate messageDelegate) + { + MessageDelegate = messageDelegate; + } + + /// + public async ValueTask InvokeAsync(MessageContext context, IMessageDelegate nextHandler) + { + await MessageDelegate.InvokeAsync(context).ConfigureAwait(false); + await nextHandler.InvokeAsync(context).ConfigureAwait(false); + } +} diff --git a/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Sources/AnotherSource.cs b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Sources/AnotherSource.cs new file mode 100644 index 0000000000..9611a463f6 --- /dev/null +++ b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Sources/AnotherSource.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace System.Cloud.Messaging.Tests.Data.Sources; + +internal class AnotherSource : IMessageSource +{ + private static MessageContext CreateContext(IFeatureCollection sourceFeatures) + { + var context = new MessageContext(new FeatureCollection()); + context.SetMessageSourceFeatures(sourceFeatures); + return context; + } + + private int _count; + + public string[] Messages { get; } + + public AnotherSource(string[] messages) + { + Messages = messages; + _count = 0; + } + + /// + public ValueTask ReadAsync(CancellationToken cancellationToken) + { + if (_count < Messages.Length) + { + var message = Messages[_count]; + Interlocked.Increment(ref _count); + + MessageContext messageContext = CreateContext(new FeatureCollection()); + messageContext.SetSourcePayload(Encoding.UTF8.GetBytes(message)); + return new(messageContext); + } + + var emptyContext = CreateContext(new FeatureCollection()); + emptyContext.SetSourcePayload(Encoding.UTF8.GetBytes(string.Empty)); + return new(emptyContext); + } + + /// + public void Release(MessageContext context) + { + // No-op: Intentionally left empty. + } +} diff --git a/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Sources/SampleSource.cs b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Sources/SampleSource.cs new file mode 100644 index 0000000000..7af22e789f --- /dev/null +++ b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Data/Sources/SampleSource.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace System.Cloud.Messaging.Tests.Data.Sources; + +internal class SampleSource : IMessageSource +{ + private static MessageContext CreateContext(IFeatureCollection sourceFeatures) + { + var context = new MessageContext(new FeatureCollection()); + context.SetMessageSourceFeatures(sourceFeatures); + return context; + } + + private int _count; + + public string[] Messages { get; } + + public SampleSource(string[] messages) + { + Messages = messages; + _count = 0; + } + + /// + public ValueTask ReadAsync(CancellationToken cancellationToken) + { + if (_count < Messages.Length) + { + var message = Messages[_count]; + Interlocked.Increment(ref _count); + + MessageContext messageContext = CreateContext(new FeatureCollection()); + messageContext.SetSourcePayload(Encoding.UTF8.GetBytes(message)); + return new(messageContext); + } + + MessageContext emptyContext = CreateContext(new FeatureCollection()); + emptyContext.SetSourcePayload(Encoding.UTF8.GetBytes(string.Empty)); + return new(emptyContext); + } + + /// + public void Release(MessageContext context) + { + // No-op: Intentionally left empty. + } +} diff --git a/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Extensions/MessageContext/SerializedMessagePayloadExtensionsTests.cs b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Extensions/MessageContext/SerializedMessagePayloadExtensionsTests.cs new file mode 100644 index 0000000000..385e1fc35a --- /dev/null +++ b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Extensions/MessageContext/SerializedMessagePayloadExtensionsTests.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.Http.Features; +using Xunit; + +namespace System.Cloud.Messaging.Tests.Extensions.Context; + +/// +/// Tests for . +/// +public class SerializedMessagePayloadExtensionsTests +{ + [Fact] + public void GetSerializedMessagePayload_ShouldThrowException_WhenSerializedPayloadIsNotSet() + { + var context = new MessageContext(new FeatureCollection()); + Assert.Throws(context.GetSerializedPayload); + } + + [Fact] + public void TryGetSerializedMessagePayload_ShouldReturnFalse_WhenSerializedPayloadIsNotSet() + { + var context = new MessageContext(new FeatureCollection()); + Assert.False(context.TryGetSerializedPayload(out _)); + } + + [Theory] + [InlineData("abc")] + public void GetSerializedMessagePayload_ShouldReturnValue_WhenSerializedPayloadIsSet(string payload) + { + var context = new MessageContext(new FeatureCollection()); + context.SetSerializedPayload(payload); + Assert.Equal(payload, context.GetSerializedPayload()); + } + + [Theory] + [InlineData("abc")] + public void TryGetSerializedMessagePayload_ShouldReturnValue_WhenSerializedPayloadIsSet(string payload) + { + var context = new MessageContext(new FeatureCollection()); + context.SetSerializedPayload(payload); + + Assert.True(context.TryGetSerializedPayload(out string? value)); + Assert.Equal(payload, value); + } + + [Theory] + [InlineData(1, "abc")] + public void GetSerializedMessagePayload_ShouldReturnValue_WhenSerializedPayloadIsSetForDifferentTypes(int intPayload, string stringPayload) + { + var context = new MessageContext(new FeatureCollection()); + context.SetSerializedPayload(intPayload); + context.SetSerializedPayload(stringPayload); + + Assert.Equal(intPayload, context.GetSerializedPayload()); + Assert.Equal(stringPayload, context.GetSerializedPayload()); + } + + [Theory] + [InlineData(1, "abc")] + public void TryGetSerializedMessagePayload_ShouldReturnValue_WhenSerializedPayloadIsSetForDifferentTypes(int intPayload, string stringPayload) + { + var context = new MessageContext(new FeatureCollection()); + context.SetSerializedPayload(intPayload); + context.SetSerializedPayload(stringPayload); + + Assert.True(context.TryGetSerializedPayload(out int intValue)); + Assert.Equal(intPayload, intValue); + + Assert.True(context.TryGetSerializedPayload(out string? stringValue)); + Assert.Equal(stringPayload, stringValue); + } +} diff --git a/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Extensions/Startup/ServiceCollectionExtensionsTests.cs b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Extensions/Startup/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000000..37e37e5406 --- /dev/null +++ b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Extensions/Startup/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,257 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Cloud.Messaging.Internal; +using System.Cloud.Messaging.Tests.Data.Consumers; +using System.Cloud.Messaging.Tests.Data.Delegates; +using System.Cloud.Messaging.Tests.Data.Middlewares; +using System.Cloud.Messaging.Tests.Data.Sources; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Latency; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Moq; +using Xunit; + +namespace System.Cloud.Messaging.Tests.Extensions.Startup; + +/// +/// Tests for . +/// +public class ServiceCollectionExtensionsTests +{ + [Theory] + [InlineData("pipeline-1", "abc", false)] + public async Task AddNamedMessageProcessingPipeline_ShouldWorkCorrectly_WhenMessageSourceKeepsProducingMessage(string pipelineName, string message, bool mocksVerificationEnabled) + { + var context = new MessageContext(new FeatureCollection()); + context.SetMessageSourceFeatures(new FeatureCollection()); + context.SetSourcePayload(Encoding.UTF8.GetBytes(message)); + + Mock mockSource = new(); + mockSource.Setup(x => x.ReadAsync(It.IsAny())).Returns(new ValueTask(context)); + + var mocks = new TestMocks(); + IHostBuilder hostBuilder = FakeHost.CreateBuilder(); + hostBuilder.ConfigureServices(services => + { + mocks.RegisterCommonServices(services); + + // Create a message consumer pipeline. + services.AddAsyncPipeline(pipelineName) + .ConfigureMessageSource(_ => mockSource.Object) + .AddMessageMiddleware(_ => new SampleMiddleware(mocks.MockDelegate.Object)) + .ConfigureTerminalMessageDelegate(_ => new SampleWriterDelegate(mocks.MockMessageDestination.Object)) + .ConfigureMessageConsumer(sp => + { + var messageSource = sp.GetRequiredService>().GetRequiredService(pipelineName); + var messageDelegate = sp.GetRequiredService>().GetRequiredService(pipelineName); + var logger = sp.GetRequiredService(); + return new DerivedConsumer(messageSource, messageDelegate, logger); + }) + .RunConsumerAsBackgroundService(); + }); + + // Build the host. + using IHost host = hostBuilder.Build(); + + // Validate the hosted services include ConsumerBackgroundService. + var hostedServices = host.Services.GetServices().ToList(); + int consumerServicesCount = hostedServices.Count(x => x is ConsumerBackgroundService); + Assert.Equal(1, consumerServicesCount); + + // Start and stop the host. + await host.StartAsync(); + await host.StopAsync(); + + // Verify Mocks + if (mocksVerificationEnabled) + { + int minInteractions = 1; + mockSource.Verify(x => x.ReadAsync(It.IsAny()), Times.AtLeast(minInteractions)); + mocks.VerifyMocksHavingCountAtleast(minInteractions); + } + } + + [Theory] + [InlineData(1, "pipeline-1", "abc,pqr,xyz", 3)] + [InlineData(2, "pipeline-2", "abc,pqr,xyz", 3)] + public async Task AddNamedMessageProcessingPipeline_ShouldWorkCorrectly(int numberOfBackgroundServices, string pipelineName, string messages, int countOfMessages) + { + var mocks = new TestMocks(); + + IHostBuilder hostBuilder = FakeHost.CreateBuilder(); + hostBuilder.ConfigureServices(services => + { + mocks.RegisterCommonServices(services); + + MeasureToken overallSuccessToken = new("overallSuccess", 0); + MeasureToken overallFailureToken = new("overallFailure", 0); + + MeasureToken delegateSuccessToken = new("delegateSuccess", 1); + MeasureToken delegateFailureToken = new("delegateFailure", 1); + + // Create a message consumer pipeline builder. + var builder = services.AddAsyncPipeline(pipelineName) + .ConfigureMessageSource(_ => new SampleSource(TestMocks.GetMessages(messages))) + .AddLatencyContextMiddleware() + .AddLatencyRecorderMessageMiddleware(overallSuccessToken, overallFailureToken) + .AddMessageMiddleware(_ => new SampleMiddleware(mocks.MockDelegate.Object)) + .AddLatencyRecorderMessageMiddleware(delegateSuccessToken, delegateFailureToken) + .ConfigureTerminalMessageDelegate(_ => new SampleWriterDelegate(mocks.MockMessageDestination.Object)) + .ConfigureMessageConsumer(sp => + { + var messageSource = sp.GetRequiredService>().GetRequiredService(pipelineName); + var messageDelegate = sp.GetRequiredService>().GetRequiredService(pipelineName); + return new SampleConsumer(messageSource, messageDelegate); + }); + + // Run multiple background services for message consumer. + for (int i = 0; i < numberOfBackgroundServices; i++) + { + builder.RunConsumerAsBackgroundService(); + } + }); + + // Build the host. + using IHost host = hostBuilder.Build(); + + // Validate the hosted services include ConsumerBackgroundServices. + var hostedServices = host.Services.GetServices().ToList(); + int consumerServicesCount = hostedServices.Count(x => x is ConsumerBackgroundService); + Assert.Equal(numberOfBackgroundServices, consumerServicesCount); + + // Start and stop the host. + await host.StartAsync(); + await host.StopAsync(); + + // Verify Mocks + if (numberOfBackgroundServices == 1) + { + mocks.VerifyMocksHavingCount(countOfMessages); + mocks.MockLatencyContextProvider.Verify(x => x.CreateContext(), Times.Exactly(countOfMessages)); + mocks.MockLatencyContext.Verify(x => x.AddMeasure(It.IsAny(), It.IsAny()), Times.Exactly(2 * countOfMessages)); + mocks.MockLatencyContext.Verify(x => x.Freeze(), Times.Exactly(countOfMessages)); + mocks.MockLatencyDataExporter.Verify(x => x.ExportAsync(It.IsAny(), It.IsAny()), Times.Exactly(countOfMessages)); + } + else + { + mocks.VerifyMocksHavingCountAtleast(countOfMessages); + mocks.MockLatencyContextProvider.Verify(x => x.CreateContext(), Times.AtLeast(countOfMessages)); + mocks.MockLatencyContext.Verify(x => x.AddMeasure(It.IsAny(), It.IsAny()), Times.AtLeast(2 * countOfMessages)); + mocks.MockLatencyContext.Verify(x => x.Freeze(), Times.AtLeast(countOfMessages)); + mocks.MockLatencyDataExporter.Verify(x => x.ExportAsync(It.IsAny(), It.IsAny()), Times.AtLeast(countOfMessages)); + } + } + + [Theory] + [InlineData("pipeline-", "abc,pqr,xyz", 6)] + public async Task AddMultipleNamedMessageProcessingPipeline_ShouldWorkCorrectly(string pipelineNamePrefix, string messages, int countOfMessages) + { + var mocks = new TestMocks(); + + IHostBuilder hostBuilder = FakeHost.CreateBuilder(); + hostBuilder.ConfigureServices(services => + { + mocks.RegisterCommonServices(services); + + // Create a message consumer pipeline. + string pipelineName1 = pipelineNamePrefix + "1"; + services.AddAsyncPipeline(pipelineName1) + .ConfigureMessageSource(_ => new SampleSource(TestMocks.GetMessages(messages))) + .AddMessageMiddleware(_ => new SampleMiddleware(mocks.MockDelegate.Object)) + .ConfigureTerminalMessageDelegate(_ => new SampleWriterDelegate(mocks.MockMessageDestination.Object)) + .ConfigureMessageConsumer(sp => + { + var messageSource = sp.GetRequiredService>().GetRequiredService(pipelineName1); + var messageDelegate = sp.GetRequiredService>().GetRequiredService(pipelineName1); + return new SampleConsumer(messageSource, messageDelegate); + }) + .RunConsumerAsBackgroundService(); + + // Create another message consumer pipeline. + string pipelineName2 = pipelineNamePrefix + "2"; + services.AddAsyncPipeline(pipelineName2) + .ConfigureMessageSource(_ => new AnotherSource(TestMocks.GetMessages(messages))) + .AddMessageMiddleware(_ => new SampleMiddleware(mocks.MockDelegate.Object)) + .ConfigureTerminalMessageDelegate(_ => new SampleWriterDelegate(mocks.MockMessageDestination.Object)) + .ConfigureMessageConsumer(sp => + { + var messageSource = sp.GetRequiredService>().GetRequiredService(pipelineName2); + var messageDelegate = sp.GetRequiredService>().GetRequiredService(pipelineName2); + return new OverridenConsumer(messageSource, messageDelegate, sp.GetRequiredService()); + }) + .RunConsumerAsBackgroundService(); + }); + + // Build the host. + using IHost host = hostBuilder.Build(); + + // Validate the hosted services include ConsumerBackgroundServices. + var hostedServices = host.Services.GetServices().ToList(); + int consumerServicesCount = hostedServices.Count(x => x is ConsumerBackgroundService); + Assert.Equal(2, consumerServicesCount); + + // Start and stop the host. + await host.StartAsync(); + await host.StopAsync(); + + // Verify Mocks + mocks.VerifyMocksHavingCount(countOfMessages); + } + + private class TestMocks + { + public Mock MockMessageDestination = new(); + public Mock MockDelegate = new(); + public Mock MockLatencyContextProvider = new(); + public Mock MockLatencyContext = new(); + public Mock MockLatencyDataExporter = new(); + public ILogger Logger = new FakeLogger(); + + public TestMocks() + { + // Setup Mocks + MockMessageDestination.Setup(x => x.WriteAsync(It.IsAny())).Returns(new ValueTask(Task.CompletedTask)); + MockDelegate.Setup(x => x.InvokeAsync(It.IsAny())).Returns(new ValueTask(Task.CompletedTask)); + MockLatencyContextProvider.Setup(x => x.CreateContext()).Returns(MockLatencyContext.Object); + } + + public static string[] GetMessages(string csv) + { + return csv.Split(','); + } + + public void RegisterCommonServices(IServiceCollection services) + { + // Register logger. + services.TryAddSingleton(Logger); + + // Register latency context provider. + services.TryAddSingleton(MockLatencyContextProvider.Object); + + // Register exporters. + services.AddSingleton(MockLatencyDataExporter.Object); + } + + public void VerifyMocksHavingCount(int count) + { + MockDelegate.Verify(x => x.InvokeAsync(It.IsAny()), Times.Exactly(count)); + MockMessageDestination.Verify(x => x.WriteAsync(It.IsAny()), Times.Exactly(count)); + } + + public void VerifyMocksHavingCountAtleast(int count) + { + MockDelegate.Verify(x => x.InvokeAsync(It.IsAny()), Times.AtLeast(count)); + MockMessageDestination.Verify(x => x.WriteAsync(It.IsAny()), Times.AtLeast(count)); + } + } +} diff --git a/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Internal/Implementations/BaseMessageConsumerTests.cs b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Internal/Implementations/BaseMessageConsumerTests.cs new file mode 100644 index 0000000000..13cff5b581 --- /dev/null +++ b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Internal/Implementations/BaseMessageConsumerTests.cs @@ -0,0 +1,201 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Cloud.Messaging.Tests.Data.Consumers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Telemetry.Testing.Logging; +using Moq; +using Xunit; + +namespace System.Cloud.Messaging.Tests.Implementations; + +/// +/// Tests for . +/// +public class BaseMessageConsumerTests +{ + private static MessageContext CreateContext(IFeatureCollection sourceFeatures) + { + var context = new MessageContext(new FeatureCollection()); + context.SetMessageSourceFeatures(sourceFeatures); + return context; + } + + [Fact] + public async Task BaseMessageConsumer_ShouldNotStartProcessing_WhenCancellationTokenIsExpired() + { + var mockMessageSource = new Mock(); + var mockMessageDelegate = new Mock(); + var mockLogger = new Mock(); + + using var cts = new CancellationTokenSource(); + cts.Cancel(false); + + var messageConsumer = new DerivedConsumer(mockMessageSource.Object, mockMessageDelegate.Object, mockLogger.Object); + await messageConsumer.ExecuteAsync(cts.Token); + + mockMessageSource.VerifyNoOtherCalls(); + mockMessageDelegate.VerifyNoOtherCalls(); + mockLogger.VerifyNoOtherCalls(); + } + + [Fact(Skip = "Flaky")] + public async Task BaseMessageConsumer_ShouldStartProcessing_WhenCancellationTokenIsExpiredLater() + { + var mockMessageSource = new Mock(); + var mockMessageDelegate = new Mock(); + var mockLogger = new Mock(); + + using var cts = new CancellationTokenSource(); + cts.CancelAfter(millisecondsDelay: 100); + + var messageConsumer = new DerivedConsumer(mockMessageSource.Object, mockMessageDelegate.Object, mockLogger.Object); + await messageConsumer.ExecuteAsync(cts.Token); + + mockMessageSource.Verify(x => x.ReadAsync(It.IsAny()), Times.AtLeastOnce); + } + + [Fact] + public async Task StartAsync_ShouldLogExceptions_WhenExceptionIsThrownWhileFetchingMessageFromSource() + { + var exception = new InvalidOperationException("Test Exception"); + + var mockMessageSource = new Mock(); + mockMessageSource.Setup(x => x.ReadAsync(It.IsAny())) + .ThrowsAsync(exception); + + var mockMessageDelegate = new Mock(); + var fakelogger = new FakeLogger(); + + var messageConsumer = new SingleMessageConsumer(mockMessageSource.Object, mockMessageDelegate.Object, fakelogger); + await messageConsumer.ExecuteAsync(CancellationToken.None); + + mockMessageSource.Verify(x => x.ReadAsync(It.IsAny()), Times.Once); + Assert.Contains("MessageSource failed during reading message.", fakelogger.LatestRecord.Message, StringComparison.Ordinal); + Assert.Equal(exception, fakelogger.LatestRecord.Exception); + + mockMessageDelegate.Verify(x => x.InvokeAsync(It.IsAny()), Times.Never); + mockMessageSource.Verify(x => x.Release(It.IsAny()), Times.Never); + } + + [Theory] + [InlineData("message")] + public async Task StartAsync_ShouldLogExceptions_WhenExceptionIsThrownDuringMessageProcessingCompletion(string message) + { + var mockFeatures = new Mock(); + MessageContext messageContext = CreateContext(mockFeatures.Object); + messageContext.SetSourcePayload(Encoding.UTF8.GetBytes(message)); + + var mockMessageSource = new Mock(); + mockMessageSource.Setup(x => x.ReadAsync(It.IsAny())) + .Returns(new ValueTask(messageContext)); + + var mockMessageDelegate = new Mock(); + var fakelogger = new FakeLogger(); + + var messageConsumer = new SingleMessageConsumer(mockMessageSource.Object, mockMessageDelegate.Object, fakelogger, true, false); + await messageConsumer.ExecuteAsync(CancellationToken.None); + + mockMessageSource.Verify(x => x.ReadAsync(It.IsAny()), Times.Once); + mockMessageDelegate.Verify(x => x.InvokeAsync(It.IsAny()), Times.Once); + + Assert.Contains("Handling message procesing completion failed.", fakelogger.LatestRecord.Message, StringComparison.Ordinal); + Assert.Equal(typeof(InvalidOperationException), fakelogger.LatestRecord.Exception?.GetType()); + + mockMessageSource.Verify(x => x.Release(messageContext), Times.Once); + } + + [Theory] + [InlineData("message")] + public async Task StartAsync_ShouldHandleExceptions_WhenExceptionIsThrownDuringProcessing(string message) + { + var exception = new InvalidOperationException("Error during processing."); + var mockFeatures = new Mock(); + + MessageContext messageContext = CreateContext(mockFeatures.Object); + messageContext.SetSourcePayload(Encoding.UTF8.GetBytes(message)); + + var mockMessageSource = new Mock(); + mockMessageSource.Setup(x => x.ReadAsync(It.IsAny())) + .Returns(new ValueTask(messageContext)); + + var mockMessageDelegate = new Mock(); + mockMessageDelegate.Setup(x => x.InvokeAsync(It.IsAny())) + .Throws(exception); + + var fakelogger = new FakeLogger(); + + var messageConsumer = new SingleMessageConsumer(mockMessageSource.Object, mockMessageDelegate.Object, fakelogger); + await messageConsumer.ExecuteAsync(CancellationToken.None); + + mockMessageSource.Verify(x => x.ReadAsync(It.IsAny()), Times.Once); + mockMessageDelegate.Verify(x => x.InvokeAsync(It.IsAny()), Times.Once); + mockMessageSource.Verify(x => x.Release(messageContext), Times.Once); + } + + [Theory] + [InlineData("message")] + public async Task StartAsync_ShouldLogExceptions_WhenExceptionIsThrownWhileHandlingFailureInProcessing(string message) + { + var processingException = new InvalidProgramException("Error during processing"); + var mockFeatures = new Mock(); + + MessageContext messageContext = CreateContext(mockFeatures.Object); + messageContext.SetSourcePayload(Encoding.UTF8.GetBytes(message)); + + var mockMessageSource = new Mock(); + mockMessageSource.Setup(x => x.ReadAsync(It.IsAny())) + .Returns(new ValueTask(messageContext)); + + var mockMessageDelegate = new Mock(); + mockMessageDelegate.Setup(x => x.InvokeAsync(It.IsAny())) + .Throws(processingException); + + var fakelogger = new FakeLogger(); + + var messageConsumer = new SingleMessageConsumer(mockMessageSource.Object, mockMessageDelegate.Object, fakelogger, false, true); + InvalidOperationException exception = await Assert.ThrowsAsync(() => messageConsumer.ExecuteAsync(CancellationToken.None).AsTask()); + + mockMessageSource.Verify(x => x.ReadAsync(It.IsAny()), Times.Once); + mockMessageDelegate.Verify(x => x.InvokeAsync(It.IsAny()), Times.Once); + mockMessageSource.Verify(x => x.Release(messageContext), Times.Once); + + Assert.Contains("Handling message procesing failure failed with", fakelogger.LatestRecord.Message, StringComparison.Ordinal); + Assert.Contains(nameof(InvalidOperationException), fakelogger.LatestRecord.Message, StringComparison.Ordinal); + Assert.Equal(processingException, fakelogger.LatestRecord.Exception); + } + + [Theory] + [InlineData("message")] + public async Task StartAsync_ShouldLogExceptions_WhenMessageSourceThrowsExceptionDuringRelease(string message) + { + var exception = new InvalidOperationException("Error during releasing."); + var mockFeatures = new Mock(); + + MessageContext messageContext = CreateContext(mockFeatures.Object); + messageContext.SetSourcePayload(Encoding.UTF8.GetBytes(message)); + + var mockMessageSource = new Mock(); + mockMessageSource.Setup(x => x.ReadAsync(It.IsAny())) + .Returns(new ValueTask(messageContext)); + mockMessageSource.Setup(x => x.Release(It.IsAny())) + .Throws(exception); + + var mockMessageDelegate = new Mock(); + var fakelogger = new FakeLogger(); + + var messageConsumer = new SingleMessageConsumer(mockMessageSource.Object, mockMessageDelegate.Object, fakelogger); + await messageConsumer.ExecuteAsync(CancellationToken.None); + + mockMessageSource.Verify(x => x.ReadAsync(It.IsAny()), Times.Once); + mockMessageDelegate.Verify(x => x.InvokeAsync(It.IsAny()), Times.Once); + mockMessageSource.Verify(x => x.Release(messageContext), Times.Once); + + Assert.Contains("MessageSource failed during releasing context.", fakelogger.LatestRecord.Message, StringComparison.Ordinal); + Assert.Equal(exception, fakelogger.LatestRecord.Exception); + } +} diff --git a/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Internal/Startup/ConsumerBackgroundServiceTests.cs b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Internal/Startup/ConsumerBackgroundServiceTests.cs new file mode 100644 index 0000000000..32397b7363 --- /dev/null +++ b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Internal/Startup/ConsumerBackgroundServiceTests.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Cloud.Messaging.Internal; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using Xunit; + +namespace System.Cloud.Messaging.Tests.Internal.Startup; + +/// +/// Tests for . +/// +public class ConsumerBackgroundServiceTests +{ + [Fact] + public void StartHost_ReturnsCompletedTask() + { + var mockConsumer = new Mock(); + using var consumerBackgroundService = new ConsumerBackgroundService(mockConsumer.Object); + + var task = consumerBackgroundService.StartAsync(CancellationToken.None); + Assert.True(task.IsCompleted); + } + + [Fact] + public async Task StopHostWithoutStart_ShouldNotCallConsumerStartMethod() + { + var mockConsumer = new Mock(); + using var consumerBackgroundService = new ConsumerBackgroundService(mockConsumer.Object); + + await consumerBackgroundService.StopAsync(CancellationToken.None); + + mockConsumer.Verify(x => x.ExecuteAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task StopHostAfterStartingIt_ShouldCallConsumerMethods() + { + var mockConsumer = new Mock(); + using var consumerBackgroundService = new ConsumerBackgroundService(mockConsumer.Object); + + await consumerBackgroundService.StartAsync(CancellationToken.None); + await consumerBackgroundService.StopAsync(CancellationToken.None); + + mockConsumer.Verify(x => x.ExecuteAsync(It.IsAny()), Times.Once); + } +} diff --git a/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Internal/Startup/PipelineDelegateFactoryTests.cs b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Internal/Startup/PipelineDelegateFactoryTests.cs new file mode 100644 index 0000000000..1de5820a5c --- /dev/null +++ b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/Internal/Startup/PipelineDelegateFactoryTests.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Cloud.Messaging.Tests.Data.Delegates; +using System.Cloud.Messaging.Tests.Data.Middlewares; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Moq; +using Xunit; + +namespace System.Cloud.Messaging.Tests.Internal.Startup; + +/// +/// Tests for . +/// +public class PipelineDelegateFactoryTests +{ + [Fact] + public void PipelineBuild_ShouldThrowException_WhenTerminalDelegateIsNotConfigured() + { + IHostBuilder hostBuilder = new HostBuilder(); + hostBuilder.ConfigureServices(services => { }); + using IHost host = hostBuilder.Build(); + + IServiceProvider serviceProvider = host.Services; + var exception = Assert.Throws(serviceProvider.GetRequiredService); + } + + [Theory] + [InlineData("pipeline-1")] + public void PipelineBuild_ShouldWorkCorrectly_WhenTerminalDelegateIsConfigured(string pipelineName) + { + IHostBuilder hostBuilder = new HostBuilder(); + hostBuilder.ConfigureServices(services => + { + services.AddAsyncPipeline(pipelineName) + .ConfigureTerminalMessageDelegate(_ => new SampleWriterDelegate(new Mock().Object)) + .ConfigureMessageConsumer(_ => new Mock().Object); + }); + + using IHost host = hostBuilder.Build(); + IServiceProvider serviceProvider = host.Services; + + var messageDelegate = serviceProvider.GetRequiredService>().GetService(pipelineName); + Assert.NotNull(messageDelegate); + } + + [Theory] + [InlineData("pipeline-2")] + public void PipelineBuild_ShouldWorkCorrectly_WhenMiddlewareAndTerminalDelegateIsConfigured(string pipelineName) + { + IHostBuilder hostBuilder = new HostBuilder(); + hostBuilder.ConfigureServices(services => + { + services.AddAsyncPipeline(pipelineName) + .AddMessageMiddleware(_ => new SampleMiddleware(new Mock().Object)) + .ConfigureTerminalMessageDelegate(_ => new SampleWriterDelegate(new Mock().Object)) + .ConfigureMessageConsumer(_ => new Mock().Object); + }); + + using IHost host = hostBuilder.Build(); + IServiceProvider serviceProvider = host.Services; + + var messageDelegate = serviceProvider.GetRequiredService>().GetService(pipelineName); + Assert.NotNull(messageDelegate); + } +} diff --git a/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/System.Cloud.Messaging.Abstractions.Tests.csproj b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/System.Cloud.Messaging.Abstractions.Tests.csproj new file mode 100644 index 0000000000..8633e7a42b --- /dev/null +++ b/test/Libraries/System.Cloud.Messaging.Abstractions.Tests/System.Cloud.Messaging.Abstractions.Tests.csproj @@ -0,0 +1,16 @@ + + + System.Cloud.Messaging.Abstractions + Unit tests for System.Cloud.Messaging.Abstractions. + + + + + + + + + + + + diff --git a/test/Shared/Data.Validation/ExclusiveRangeAttributeTests.cs b/test/Shared/Data.Validation/ExclusiveRangeAttributeTests.cs new file mode 100644 index 0000000000..c8d9b3ce49 --- /dev/null +++ b/test/Shared/Data.Validation/ExclusiveRangeAttributeTests.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Xunit; + +namespace Microsoft.Shared.Data.Validation.Test; + +public class ExclusiveRangeAttributeTests +{ + public class TestOptions0 + { + [ExclusiveRange(0, 10)] + public int? Number { get; set; } + } + + [Fact] + public void Basic() + { + var options = new TestOptions0(); + var context = new ValidationContext(options); + var results = new List(); + + options.Number = null; + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + options.Number = 0; + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Number), results[0].MemberNames); + Assert.Contains(nameof(options.Number), results[0].ErrorMessage); + + options.Number = 10; + results.Clear(); + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Number), results[0].MemberNames); + Assert.Contains(nameof(options.Number), results[0].ErrorMessage); + + options.Number = 1; + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + options.Number = 9; + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + } + + public class TestOptionsDouble + { + [ExclusiveRange(0.0, 10.0)] + public double? Number { get; set; } + } + + [Fact] + public void BasicDouble() + { + var options = new TestOptionsDouble(); + var context = new ValidationContext(options); + var results = new List(); + + options.Number = null; + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + options.Number = 0; + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Number), results[0].MemberNames); + Assert.Contains(nameof(options.Number), results[0].ErrorMessage); + + options.Number = 10; + results.Clear(); + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Number), results[0].MemberNames); + Assert.Contains(nameof(options.Number), results[0].ErrorMessage); + + options.Number = 0.00001; + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + options.Number = 10.0 - 0.00001; + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + } + + public class BadOptions1 + { + [ExclusiveRange(10.0, 9)] + public int? Number { get; set; } + } + + [Fact] + public void BadAttributeUse() + { + var options1 = new BadOptions1 + { + Number = 4, + }; + var context = new ValidationContext(options1); + var results = new List(); + Assert.Throws(() => _ = Validator.TryValidateObject(options1, context, results, true)); + } + + public class BadOptions2 + { + [ExclusiveRange(10, 10)] + public int? Number { get; set; } + } + + [Fact] + public void BadAttributeUse_MinEqualsMax() + { + var options1 = new BadOptions2 + { + Number = 10, + }; + var context = new ValidationContext(options1); + var results = new List(); + Assert.Throws(() => _ = Validator.TryValidateObject(options1, context, results, true)); + } + + [Fact] + public void NakedContext() + { + var value = 0.0; + var context = new ValidationContext(value); + var attr = new ExclusiveRangeAttribute(0.0, 10.0); + + var result = attr.GetValidationResult(value, context); + + Assert.NotEqual(ValidationResult.Success, result); + Assert.Empty(result!.MemberNames); + } +} diff --git a/test/Shared/Data.Validation/LengthAttributeTests.cs b/test/Shared/Data.Validation/LengthAttributeTests.cs new file mode 100644 index 0000000000..d517b9a768 --- /dev/null +++ b/test/Shared/Data.Validation/LengthAttributeTests.cs @@ -0,0 +1,418 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Xunit; + +namespace Microsoft.Shared.Data.Validation.Test; + +public class LengthAttributeTests +{ + public class TestOptions + { + [Length(5)] + public string? Name { get; set; } + + [Length(5, 7)] + public string? Address { get; set; } + } + + [Fact] + public void Basic() + { + var options = new TestOptions(); + var context = new ValidationContext(options); + var results = new List(); + + // Assertions on null values. + options.Name = null; + options.Address = null; + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + // Assertions on values bounded by minimum only without upper bound. + options.Name = "abcd"; + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Name), results[0].MemberNames); + Assert.Contains(nameof(options.Name), results[0].ErrorMessage); + + options.Name = "abcde"; + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + // Assertions on values bounded by minimum and maximum. + options.Name = null; + options.Address = "abcd"; + results.Clear(); + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Address), results[0].MemberNames); + Assert.Contains(nameof(options.Address), results[0].ErrorMessage); + + options.Address = "abcdefghi"; + results.Clear(); + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Address), results[0].MemberNames); + Assert.Contains(nameof(options.Address), results[0].ErrorMessage); + + options.Address = "abcdefg"; + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + } + + public class TestOptionsExclusive + { + [Length(5, Exclusive = true)] + public string? Name { get; set; } + + [Length(5, 7, Exclusive = true)] + public string? Address { get; set; } + } + + [Fact] + public void BasicWithExclusive() + { + var options = new TestOptionsExclusive(); + var context = new ValidationContext(options); + var results = new List(); + + // Assertions on null values. + options.Name = null; + options.Address = null; + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + // Assertions on values bounded by minimum only without upper bound. + options.Name = "abcde"; + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Name), results[0].MemberNames); + Assert.Contains(nameof(options.Name), results[0].ErrorMessage); + + options.Name = "abcdef"; + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + // Assertions on values bounded by minimum and maximum. + options.Name = null; + options.Address = "abcdefg"; + results.Clear(); + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Address), results[0].MemberNames); + Assert.Contains(nameof(options.Address), results[0].ErrorMessage); + + options.Name = null; + options.Address = "abcde"; + results.Clear(); + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Address), results[0].MemberNames); + Assert.Contains(nameof(options.Address), results[0].ErrorMessage); + + options.Address = "abcdef"; + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + } + + public class Enumerable : IEnumerable + { + public IEnumerator GetEnumerator() + { + for (int i = 0; i < Count; i++) + { + yield return i; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count { get; set; } + } + + public class TestOptionsEnumerable + { + [Length(5)] + public Enumerable? Names { get; set; } + + [Length(5, 7)] + public Enumerable? Addresses { get; set; } + } + + [Fact] + public void BasicEnumerable() + { + var options = new TestOptionsEnumerable(); + var context = new ValidationContext(options); + var results = new List(); + + options.Names = null; + options.Addresses = null; + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + options.Names = new Enumerable + { + Count = 4, + }; + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Names), results[0].MemberNames); + Assert.Contains(nameof(options.Names), results[0].ErrorMessage); + + options.Names = new Enumerable + { + Count = 5, + }; + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + options.Names = null; + options.Addresses = new Enumerable + { + Count = 8, + }; + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Addresses), results[0].MemberNames); + Assert.Contains(nameof(options.Addresses), results[0].ErrorMessage); + + options.Addresses = new Enumerable + { + Count = 7, + }; + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + } + + public class TestOptionsCollection + { + [Length(5)] + public ICollection? Names { get; set; } + + [Length(5, 7)] + public ICollection? Addresses { get; set; } + } + + [Fact] + public void BasicCollection() + { + var options = new TestOptionsCollection(); + var context = new ValidationContext(options); + var results = new List(); + + options.Names = null; + options.Addresses = null; + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + options.Names = new[] { "a", "b", "c", "d" }; + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Names), results[0].MemberNames); + Assert.Contains(nameof(options.Names), results[0].ErrorMessage); + + options.Names = new[] { "a", "b", "c", "d", "e" }; + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + options.Names = null; + options.Addresses = new[] { "a", "b", "c", "d", "e", "f", "g", "h" }; + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Addresses), results[0].MemberNames); + Assert.Contains(nameof(options.Addresses), results[0].ErrorMessage); + + options.Addresses = new[] { "a", "b", "c", "d", "e", "f", "g" }; + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + } + + public class Countable + { + public int Count { get; set; } + } + + public class TestOptionsCountable + { + [Length(5)] + public Countable? Names { get; set; } + + [Length(5, 7)] + public Countable? Addresses { get; set; } + } + + [Fact] + public void BasicCountable() + { + var options = new TestOptionsCountable(); + var context = new ValidationContext(options); + var results = new List(); + + options.Names = null; + options.Addresses = null; + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + options.Names = new Countable + { + Count = 4, + }; + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Names), results[0].MemberNames); + Assert.Contains(nameof(options.Names), results[0].ErrorMessage); + + options.Names = new Countable + { + Count = 5, + }; + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + options.Names = null; + options.Addresses = new Countable + { + Count = 8, + }; + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Addresses), results[0].MemberNames); + Assert.Contains(nameof(options.Addresses), results[0].ErrorMessage); + + options.Addresses = new Countable + { + Count = 7, + }; + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + } + + public class BadOptions0 + { + [Length(5)] + public int Time { get; set; } + } + + public class BadOptions1 + { + [Length(-1)] + public string? Time { get; set; } + } + + public class BadOptions2 + { + [Length(5, 10)] + public int Time { get; set; } + } + + public class BadOptions3 + { + [Length(10, 5)] + public int Time { get; set; } + } + + public class BadOptions4 + { + [Length(-5, -10)] + public int Time { get; set; } + } + + [Fact] + public void BadAttributeUse() + { + var options0 = new BadOptions0(); + var context = new ValidationContext(options0); + var results = new List(); + Assert.Throws(() => _ = Validator.TryValidateObject(options0, context, results, true)); + + var options1 = new BadOptions1(); + context = new ValidationContext(options1); + results = new List(); + Assert.Throws(() => _ = Validator.TryValidateObject(options1, context, results, true)); + + var options2 = new BadOptions2(); + context = new ValidationContext(options2); + results = new List(); + Assert.Throws(() => _ = Validator.TryValidateObject(options2, context, results, true)); + + var options3 = new BadOptions3(); + context = new ValidationContext(options3); + results = new List(); + Assert.Throws(() => _ = Validator.TryValidateObject(options3, context, results, true)); + + var options4 = new BadOptions4(); + context = new ValidationContext(options4); + results = new List(); + Assert.Throws(() => _ = Validator.TryValidateObject(options4, context, results, true)); + } + + [Fact] + public void NakedContext() + { + var value = "abcd"; + var context = new ValidationContext(value); + var attr = new LengthAttribute(5); + + var result = attr.GetValidationResult(value, context); + + Assert.NotEqual(ValidationResult.Success, result); + Assert.Empty(result!.MemberNames); + } + + public class TestOptionsCustomMessage + { + [Length(5, ErrorMessage = "My custom message for '{0}'.")] + public List CustomMessage { get; set; } = new List(); + } + + [Fact] + public void CustomErrorMessage() + { + var options = new TestOptionsCustomMessage(); + var context = new ValidationContext(options); + var results = new List(); + + // Assertions on null values. + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.NotEmpty(results); + + Assert.Equal("My custom message for 'CustomMessage'.", results.Single().ErrorMessage); + } + + public class TestOptionsDefaultMessage + { + [Length(5)] + public List DefaultMessage { get; set; } = new List(); + } + + [Fact] + public void DefaultErrorMessage() + { + var options = new TestOptionsDefaultMessage(); + var context = new ValidationContext(options); + var results = new List(); + + // Assertions on null values. + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.NotEmpty(results); + + Assert.Equal("The field DefaultMessage length must be greater or equal than 5.", results.Single().ErrorMessage); + } +} diff --git a/test/Shared/Data.Validation/TimeSpanAttributeTests.cs b/test/Shared/Data.Validation/TimeSpanAttributeTests.cs new file mode 100644 index 0000000000..b7f535ec28 --- /dev/null +++ b/test/Shared/Data.Validation/TimeSpanAttributeTests.cs @@ -0,0 +1,255 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Xunit; + +namespace Microsoft.Shared.Data.Validation.Test; + +public class TimeSpanAttributeTests +{ + public class TestOptions + { + [TimeSpan(0, 10)] + public TimeSpan? Time { get; set; } + + [TimeSpan(0)] + public TimeSpan? Time2 { get; set; } + } + + [Fact] + public void Basic() + { + var options = new TestOptions(); + var context = new ValidationContext(options); + var results = new List(); + + // Assertions on null values. + options.Time = null; + options.Time2 = null; + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + // Assertions on values bounded by minimum and maximum. + options.Time = TimeSpan.FromTicks(-1); + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Time), results[0].MemberNames); + Assert.Contains(nameof(options.Time), results[0].ErrorMessage); + + options.Time = TimeSpan.FromTicks(TimeSpan.FromMilliseconds(10).Ticks + 1); + results.Clear(); + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Time), results[0].MemberNames); + Assert.Contains(nameof(options.Time), results[0].ErrorMessage); + + options.Time = TimeSpan.FromTicks(0); + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + options.Time = TimeSpan.FromMilliseconds(10); + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + // Assertions on values bounded by minimum only without upper bound. + options.Time = null; + options.Time2 = TimeSpan.FromTicks(-1); + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Time2), results[0].MemberNames); + Assert.Contains(nameof(options.Time2), results[0].ErrorMessage); + + options.Time2 = TimeSpan.FromTicks(0); + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + options.Time2 = TimeSpan.FromMilliseconds(int.MaxValue); + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + } + + public class TestOptionsExclusive + { + [TimeSpan(0, 10, Exclusive = true)] + public TimeSpan? Time { get; set; } + + [TimeSpan(0, Exclusive = true)] + public TimeSpan? Time2 { get; set; } + } + + [Fact] + public void BasicWithExclusive() + { + var options = new TestOptionsExclusive(); + var context = new ValidationContext(options); + var results = new List(); + + // Assertions on null values. + options.Time = null; + options.Time2 = null; + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + // Assertions on values bounded by minimum and maximum. + options.Time = TimeSpan.FromTicks(0); + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Time), results[0].MemberNames); + Assert.Contains(nameof(options.Time), results[0].ErrorMessage); + + options.Time = TimeSpan.FromMilliseconds(10); + results.Clear(); + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Time), results[0].MemberNames); + Assert.Contains(nameof(options.Time), results[0].ErrorMessage); + + options.Time = TimeSpan.FromMilliseconds(5); + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + // Assertions on values bounded by minimum only without upper bound. + options.Time = null; + options.Time2 = TimeSpan.FromTicks(-1); + results.Clear(); + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Time2), results[0].MemberNames); + Assert.Contains(nameof(options.Time2), results[0].ErrorMessage); + + options.Time2 = TimeSpan.FromTicks(0); + results.Clear(); + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Time2), results[0].MemberNames); + Assert.Contains(nameof(options.Time2), results[0].ErrorMessage); + + options.Time2 = TimeSpan.FromMilliseconds(int.MaxValue); + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + } + + public class TestOptionsString + { + [TimeSpan("00:00:00", "00:00:00.01")] + public TimeSpan? Time { get; set; } + + [TimeSpan("00:00:00")] + public TimeSpan? Time2 { get; set; } + } + + [Fact] + public void BasicString() + { + var options = new TestOptionsString(); + var context = new ValidationContext(options); + var results = new List(); + + // Assertions on null values. + options.Time = null; + options.Time2 = null; + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + // Assertions on values bounded by minimum and maximum. + options.Time = TimeSpan.FromTicks(-1); + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Time), results[0].MemberNames); + Assert.Contains(nameof(options.Time), results[0].ErrorMessage); + + options.Time = TimeSpan.FromTicks(TimeSpan.FromMilliseconds(10).Ticks + 1); + results.Clear(); + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Time), results[0].MemberNames); + Assert.Contains(nameof(options.Time), results[0].ErrorMessage); + + options.Time = TimeSpan.FromTicks(0); + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + options.Time = TimeSpan.FromMilliseconds(10); + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + // Assertions on values bounded by minimum only without upper bound. + options.Time = null; + options.Time2 = TimeSpan.FromTicks(-1); + Assert.False(Validator.TryValidateObject(options, context, results, true)); + Assert.Single(results); + Assert.Contains(nameof(options.Time2), results[0].MemberNames); + Assert.Contains(nameof(options.Time2), results[0].ErrorMessage); + + options.Time2 = TimeSpan.FromTicks(0); + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + + options.Time2 = TimeSpan.FromMilliseconds(int.MaxValue); + results.Clear(); + Assert.True(Validator.TryValidateObject(options, context, results, true)); + Assert.Empty(results); + } + + public class BadOptions0 + { + [TimeSpan(0, 10)] + public int Time { get; set; } + } + + public class BadOptions1 + { + [TimeSpan(10, 9)] + public TimeSpan Time { get; set; } + } + + public class BadOptions3 + { + [TimeSpan(10, 10)] + public TimeSpan Time { get; set; } + } + + [Fact] + public void BadAttributeUse() + { + var options0 = new BadOptions0(); + var context = new ValidationContext(options0); + var results = new List(); + Assert.Throws(() => _ = Validator.TryValidateObject(options0, context, results, true)); + + var options1 = new BadOptions1(); + context = new ValidationContext(options1); + results = new List(); + Assert.Throws(() => _ = Validator.TryValidateObject(options1, context, results, true)); + + var options3 = new BadOptions3(); + context = new ValidationContext(options3); + results = new List(); + Assert.Throws(() => _ = Validator.TryValidateObject(options3, context, results, true)); + } + + [Fact] + public void NakedContext() + { + var value = TimeSpan.FromTicks(-1); + var context = new ValidationContext(value); + var attr = new TimeSpanAttribute(0, 10); + + var result = attr.GetValidationResult(value, context); + + Assert.NotEqual(ValidationResult.Success, result); + Assert.Empty(result!.MemberNames); + } +} diff --git a/test/Shared/Debugger/DebuggerTest.cs b/test/Shared/Debugger/DebuggerTest.cs new file mode 100644 index 0000000000..d832f17adf --- /dev/null +++ b/test/Shared/Debugger/DebuggerTest.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Shared.Diagnostics.Test; + +public class DebuggerTest +{ + [Fact] + public void Debugger_Classes_Return_Booleans() + { + Assert.False(DebuggerState.System.IsAttached); + Assert.True(DebuggerState.Attached.IsAttached); + Assert.False(DebuggerState.Detached.IsAttached); + } + + [Fact] + public void System_Debugger_From_Service_Collection_Is_Detached() + { + using var provider = new ServiceCollection() + .AddSystemDebuggerState() + .BuildServiceProvider(); + + var debugger = provider.GetRequiredService(); + + Assert.IsAssignableFrom(debugger); + Assert.False(debugger.IsAttached); + } + + [Fact] + public void Detached_Debugger_From_Service_Collection_Is_Detached() + { + using var provider = new ServiceCollection() + .AddDetachedDebuggerState() + .BuildServiceProvider(); + + var debugger = provider.GetRequiredService(); + + Assert.IsAssignableFrom(debugger); + Assert.False(debugger.IsAttached); + } + + [Fact] + public void Attached_Debugger_From_Service_Collection_Is_Attached() + { + using var provider = new ServiceCollection() + .AddAttachedDebuggerState() + .BuildServiceProvider(); + + var debugger = provider.GetRequiredService(); + + Assert.IsAssignableFrom(debugger); + Assert.True(debugger.IsAttached); + } + + [Fact] + public void Debugger_Extensions_Does_Not_Allow_Nulls() + { + Assert.Throws(() => ((IServiceCollection)null!).AddAttachedDebuggerState()); + Assert.Throws(() => ((IServiceCollection)null!).AddDetachedDebuggerState()); + Assert.Throws(() => ((IServiceCollection)null!).AddSystemDebuggerState()); + } +} diff --git a/test/Shared/EmptyCollections/EmptyCollectionExtensionsTests.cs b/test/Shared/EmptyCollections/EmptyCollectionExtensionsTests.cs new file mode 100644 index 0000000000..f4aaf3640d --- /dev/null +++ b/test/Shared/EmptyCollections/EmptyCollectionExtensionsTests.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using FluentAssertions; +using Xunit; + +namespace Microsoft.Shared.Collections.Test; + +public class EmptyCollectionExtensionsTests +{ + internal static void Verify() + where T : notnull + { + EmptyCollectionExtensions.EmptyIfNull((IEnumerable?)null).Should().BeEmpty(); + EmptyCollectionExtensions.EmptyIfNull((IReadOnlyCollection?)null).Should().BeEmpty(); + EmptyCollectionExtensions.EmptyIfNull((IReadOnlyList?)null).Should().BeEmpty(); + EmptyCollectionExtensions.EmptyIfNull((ICollection?)null).Should().BeEmpty(); + EmptyCollectionExtensions.EmptyIfNull((IList?)null).Should().BeEmpty(); + EmptyCollectionExtensions.EmptyIfNull((T[]?)null).Should().BeEmpty(); + EmptyCollectionExtensions.EmptyIfNull((IReadOnlyDictionary?)null).Should().BeEmpty(); + + var input = new List(); + EmptyCollectionExtensions.EmptyIfNull((IEnumerable)input).Should().BeEmpty().And.NotBeSameAs(input); + EmptyCollectionExtensions.EmptyIfNull((IReadOnlyCollection)input).Should().BeEmpty().And.NotBeSameAs(input); + EmptyCollectionExtensions.EmptyIfNull((IReadOnlyList)input).Should().BeEmpty().And.NotBeSameAs(input); + EmptyCollectionExtensions.EmptyIfNull((ICollection)input).Should().BeEmpty().And.NotBeSameAs(input); + EmptyCollectionExtensions.EmptyIfNull((IList)input).Should().BeEmpty().And.NotBeSameAs(input); + + var empty = new T[0]; + EmptyCollectionExtensions.EmptyIfNull(empty).Should().BeEmpty().And.NotBeSameAs(empty); + + var nonempty = new T[1]; + EmptyCollectionExtensions.EmptyIfNull((IEnumerable)nonempty).Should().BeSameAs(nonempty); + EmptyCollectionExtensions.EmptyIfNull((IReadOnlyCollection)nonempty).Should().BeSameAs(nonempty); + EmptyCollectionExtensions.EmptyIfNull((IReadOnlyList)nonempty).Should().BeSameAs(nonempty); + EmptyCollectionExtensions.EmptyIfNull((ICollection)nonempty).Should().BeSameAs(nonempty); + EmptyCollectionExtensions.EmptyIfNull((IList)nonempty).Should().BeSameAs(nonempty); + EmptyCollectionExtensions.EmptyIfNull(nonempty).Should().BeSameAs(nonempty); + + var enumerable = new Enumerable(); + EmptyCollectionExtensions.EmptyIfNull(enumerable).Should().BeSameAs(enumerable); + + var coll = new Collection(); + EmptyCollectionExtensions.EmptyIfNull((IEnumerable)coll).Should().NotBeSameAs(coll); + + var dictionary = new Dictionary(); + EmptyCollectionExtensions.EmptyIfNull((IReadOnlyDictionary?)dictionary).Should().NotBeSameAs(dictionary); + + dictionary.Add(default!, default!); + EmptyCollectionExtensions.EmptyIfNull((IReadOnlyDictionary?)dictionary).Should().BeSameAs(dictionary); + } + + [Fact] + public void Tests() + { + Verify(); + } + + [Fact] + public void EmptyReadOnlyListTests() + { + var nothing = EmptyReadOnlyList.Instance; + Assert.Empty(nothing); + + var count = 0; + foreach (var _ in nothing) + { + count++; + } + + Assert.Equal(0, count); + Assert.Throws(() => nothing[0]); + } + + private sealed class Enumerable : IEnumerable + { + public IEnumerator GetEnumerator() + { + yield break; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + private sealed class Collection : ICollection + { + public IEnumerator GetEnumerator() + { + yield break; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Clear() + { + // nothing to clear + } + + public void CopyTo(T[] array, int arrayIndex) + { + // nothing to copy + } + + public void Add(T item) => throw new NotSupportedException(); + public bool Contains(T item) => false; + public bool Remove(T item) => false; + public int Count => 0; + public bool IsReadOnly => true; + } +} diff --git a/test/Shared/EmptyCollections/EmptyReadOnlyListTests.cs b/test/Shared/EmptyCollections/EmptyReadOnlyListTests.cs new file mode 100644 index 0000000000..d8eba0c9d9 --- /dev/null +++ b/test/Shared/EmptyCollections/EmptyReadOnlyListTests.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using FluentAssertions; +using Xunit; + +namespace Microsoft.Shared.Collections.Test; + +public static class EmptyReadOnlyListTests +{ + [Fact] + public static void InstanceTests() + { + // Verify multiple invocations of Instance return the same object instance + Assert.Same(EmptyReadOnlyList.Instance, EmptyReadOnlyList.Instance); + + var instance = EmptyReadOnlyList.Instance; + + // Verify multiple invocations of GetEnumerator return the same object instance + Assert.Same(instance.GetEnumerator(), instance.GetEnumerator()); + Assert.Same(instance.GetEnumerator(), instance.GetEnumerator()); + Assert.Same(((IEnumerable)instance).GetEnumerator(), ((IEnumerable)instance).GetEnumerator()); + + instance.Count.Should().Be(0); + Assert.Throws(() => instance[0]); + + bool enumerated = false; + foreach (var i in EmptyReadOnlyList.Instance) + { + enumerated = true; + } + + enumerated.Should().BeFalse(); + } + + [Fact] + public static void EnumeratorTests() + { + var enumerator = EmptyReadOnlyList.Instance.GetEnumerator(); + enumerator.MoveNext().Should().BeFalse(); + enumerator.Reset(); // should not throw + enumerator.Dispose(); // should not throw, nop method. + enumerator.MoveNext().Should().BeFalse(); + + Assert.Throws(() => enumerator.Current); + Assert.Throws(() => ((IEnumerator)enumerator).Current); + } + + [Fact] + public static void ICollection() + { + var coll = EmptyReadOnlyList.Instance as ICollection; + + Assert.Throws(() => coll.Add(1)); + Assert.False(coll.Remove(1)); + Assert.False(coll.Contains(1)); + Assert.True(coll.IsReadOnly); + + // nop + coll.Clear(); + coll.CopyTo(Array.Empty(), 0); + } +} diff --git a/test/Shared/EmptyCollections/EmptyReadonlyDictionaryTests.cs b/test/Shared/EmptyCollections/EmptyReadonlyDictionaryTests.cs new file mode 100644 index 0000000000..2a73218d1c --- /dev/null +++ b/test/Shared/EmptyCollections/EmptyReadonlyDictionaryTests.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Shared.Collections.Test; + +public static class EmptyReadOnlyDictionaryTests +{ + [Fact] + public static void InstanceTest() + { + EmptyReadOnlyDictionary instance = EmptyReadOnlyDictionary.Instance; + + Assert.Throws(() => instance[5]); + + Assert.Equal(EmptyReadOnlyList.Instance, instance.Keys); + Assert.Equal(EmptyReadOnlyList.Instance, instance.Values); + +#pragma warning disable xUnit2013 // Need to test count. + Assert.Equal(0, instance.Count); +#pragma warning restore xUnit2013 + + Assert.False(instance.ContainsKey(5)); + Assert.False(instance.TryGetValue(5, out _)); + + Assert.Empty(instance); + } + + [Fact] + public static void IDictionary() + { + var dict = EmptyReadOnlyDictionary.Instance as IDictionary; + + Assert.Throws(() => dict.Add(1, "One")); + Assert.False(dict.Remove(1)); + Assert.False(dict.Contains(new KeyValuePair(1, "One"))); + Assert.False(dict.ContainsKey(1)); + Assert.True(dict.IsReadOnly); + Assert.Empty(dict.Keys); + Assert.Empty(dict.Values); + Assert.False(dict.TryGetValue(1, out string? value)); + Assert.Null(value); + Assert.Throws(() => dict[1]); + Assert.Throws(() => dict[1] = "One"); + Assert.Throws(() => dict.Add(1, "One")); + + // nop + dict.Clear(); + dict.CopyTo(Array.Empty>(), 0); + } + + [Fact] + public static void ICollection() + { + var coll = EmptyReadOnlyDictionary.Instance as ICollection>; + + Assert.Throws(() => coll.Add(new KeyValuePair(1, "One"))); + Assert.False(coll.Remove(new KeyValuePair(1, "One"))); + Assert.False(coll.Contains(new KeyValuePair(1, "One"))); + } +} diff --git a/test/Shared/EmptyCollections/EmptyTests.cs b/test/Shared/EmptyCollections/EmptyTests.cs new file mode 100644 index 0000000000..360923a95f --- /dev/null +++ b/test/Shared/EmptyCollections/EmptyTests.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Shared.Collections.Test; + +public class EmptyTests +{ + [Fact] + public void Basic() + { + Assert.Empty(Empty.ReadOnlyCollection()); + Assert.Empty(Empty.ReadOnlyList()); + Assert.Empty(Empty.Enumerable()); + Assert.Empty(Empty.ReadOnlyDictionary()); + } +} diff --git a/test/Shared/Memoization/MemoizeTests.cs b/test/Shared/Memoization/MemoizeTests.cs new file mode 100644 index 0000000000..9e5a10dacd --- /dev/null +++ b/test/Shared/Memoization/MemoizeTests.cs @@ -0,0 +1,343 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Shared.Memoization.Test; + +public class MemoizeTests +{ + [Fact] + public void MemoizeFunction_Arity1_CanInvoke() + { + Func doubler = x => x * 2; + var memoized = Memoize.Function(doubler); + Assert.Equal(4, memoized(2)); + Assert.Equal(6, memoized(3)); + } + + [Fact] + public async Task MemoizeFunction_TaskReturningMethod_CanInvoke() + { + Func> doubler = x => Task.FromResult(x * 2); + var memoized = Memoize.Function(doubler); + Assert.Equal(4, await memoized(2)); + Assert.Equal(6, await memoized(3)); + } + + [Fact] + public void MemoizeFunction_Arity2_CanInvoke() + { + Func adder = (x, y) => x + y; + var memoized = Memoize.Function(adder); + Assert.Equal(4, memoized(2, 2)); + Assert.Equal(8, memoized(3, 5)); + } + + [Fact] + public void MemoizeFunction_Arity3_CanInvoke() + { + Func adder = (x, y, z) => x + y + z; + var memoized = Memoize.Function(adder); + Assert.Equal(6, memoized(1, 2, 3)); + Assert.Equal(9, memoized(3, 5, 1)); + } + + [Fact] + public void MemoizeFunctionArity1_InvokedMultipleTimes_InvokesFunctionOnlyOnce() + { + var callCount = 0; + Func doubler = x => + { + callCount++; + return x * 2; + }; + var memoized = Memoize.Function(doubler); + Assert.Equal(4, memoized(2)); + Assert.Equal(4, memoized(2)); + Assert.Equal(1, callCount); + + Assert.Equal(6, memoized(3)); + Assert.Equal(2, callCount); + } + + [Fact] + public void MemoizeFunctionArity1_InvokedMultipleTimesWithNull_InvokesFunctionOnlyOnce() + { + var callCount = 0; + Func toString = x => + { + callCount++; + return x?.ToString() ?? "null"; + }; + + var memoized = Memoize.Function(toString); + Assert.Equal("null", memoized(null)); + Assert.Equal("null", memoized(null)); + Assert.Equal(1, callCount); + + Assert.Equal("3", memoized(3)); + Assert.Equal(2, callCount); + } + + [Theory] + [InlineData(0, 1)] + [InlineData(1, 1)] + public void MemoizeFunctionArity2_InvokedMultipleTimes_InvokesFunctionOnlyOnce(int a, int b) + { + var callCount = 0; + Func adder = (x, y) => + { + callCount++; + return x + y; + }; + var memoized = Memoize.Function(adder); + Assert.Equal(a + b, memoized(a, b)); + Assert.Equal(a + b, memoized(a, b)); + Assert.Equal(1, callCount); + + Assert.Equal(a + b + 1, memoized(a, b + 1)); + Assert.Equal(2, callCount); + } + + [Theory] + [InlineData(null, 0)] + [InlineData(0, null)] + public void MemoizeFunctionArity2_InvokedMultipleTimesWithNull_InvokesFunctionOnlyOnce(int? a, int? b) + { + var callCount = 0; + Func toString = (_, _) => + { + callCount++; + return "return value"; + }; + + var memoized = Memoize.Function(toString); + Assert.Equal("return value", memoized(a, b)); + Assert.Equal("return value", memoized(a, b)); + Assert.Equal(1, callCount); + + Assert.Equal("return value", memoized(a ?? 0 + 1, b ?? 0 + 1)); + Assert.Equal(2, callCount); + } + + [Theory] + [InlineData(0, 1, 1)] + [InlineData(1, 0, 1)] + [InlineData(1, 0, 0)] + public void MemoizeFunctionArity3_InvokedMultipleTimes_InvokesFunctionOnlyOnce(int a, int b, int c) + { + var callCount = 0; + Func adder = (x, y, z) => + { + callCount++; + return x + y + z; + }; + var memoized = Memoize.Function(adder); + Assert.Equal(a + b + c, memoized(a, b, c)); + Assert.Equal(a + b + c, memoized(a, b, c)); + Assert.Equal(1, callCount); + + Assert.Equal(a + b + c + 1, memoized(a, b, c + 1)); + Assert.Equal(2, callCount); + } + + [Fact] + public void Arg1_Equals_Reflexive() + { + var a = new MemoizedFunction.Arg(0); + Assert.Equal(a, a); + Assert.True(a.Equals(a)); + Assert.True(a.Equals((object)a)); + + Assert.Equal(a.GetHashCode(), a.GetHashCode()); + } + + [Fact] + public void Arg1_Equals_Symmetric() + { + var a = new MemoizedFunction.Arg(0); + var b = new MemoizedFunction.Arg(0); + Assert.Equal(a, b); + Assert.Equal(b, a); + + Assert.True(a.Equals(b)); + Assert.True(b.Equals(a)); + + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + Assert.Equal(b.GetHashCode(), a.GetHashCode()); + } + + [Fact] + public void Arg1_Equals_Transitive() + { + var a = new MemoizedFunction.Arg(1); + var b = new MemoizedFunction.Arg(1); + var c = new MemoizedFunction.Arg(1); + Assert.Equal(a, b); + Assert.Equal(b, c); + Assert.Equal(c, a); + + Assert.True(a.Equals(b)); + Assert.True(b.Equals(c)); + Assert.True(c.Equals(a)); + + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + Assert.Equal(b.GetHashCode(), c.GetHashCode()); + Assert.Equal(a.GetHashCode(), a.GetHashCode()); + } + + [Fact] + public void Arg1_Equals_UnequalThingsNotEqual() + { + static MemoizedFunction.Arg Args(int x) => new(x); + Assert.NotEqual(Args(0), Args(1)); + + Assert.NotEqual(Args(0).GetHashCode(), Args(1).GetHashCode()); + + Assert.False(Args(0).Equals(null)); + } + + [Fact] + public void Arg2_Equals_Reflexive() + { + var a = new MemoizedFunction.Args(0, 0); + Assert.Equal(a, a); + Assert.True(a.Equals(a)); + Assert.True(a.Equals((object?)a)); + + Assert.Equal(a.GetHashCode(), a.GetHashCode()); + } + + [Fact] + public void Arg2_Equals_Symmetric() + { + var a = new MemoizedFunction.Args(0, 0); + var b = new MemoizedFunction.Args(0, 0); + Assert.Equal(a, b); + Assert.Equal(b, a); + + Assert.True(a.Equals(b)); + Assert.True(b.Equals(a)); + + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + Assert.Equal(b.GetHashCode(), a.GetHashCode()); + } + + [Fact] + public void Arg2_Equals_Transitive() + { + var a = new MemoizedFunction.Args(1, 1); + var b = new MemoizedFunction.Args(1, 1); + var c = new MemoizedFunction.Args(1, 1); + Assert.Equal(a, b); + Assert.Equal(b, c); + Assert.Equal(c, a); + + Assert.True(a.Equals(b)); + Assert.True(b.Equals(c)); + Assert.True(c.Equals(a)); + + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + Assert.Equal(b.GetHashCode(), c.GetHashCode()); + Assert.Equal(a.GetHashCode(), a.GetHashCode()); + } + + [Fact] + public void Arg2_Equals_UnequalThingsNotEqual() + { + static MemoizedFunction.Args Args(int x, int y) => new(x, y); + + Assert.NotEqual(Args(0, 0), Args(0, 1)); + Assert.NotEqual(Args(0, 0), Args(1, 0)); + + Assert.NotEqual(Args(0, 0).GetHashCode(), Args(0, 1).GetHashCode()); + + Assert.False(Args(0, 0).Equals(null)); + } + + [Fact] + public void Arg3_Equals_Reflexive() + { + var a = new MemoizedFunction.Args(0, 0, 0); + Assert.Equal(a, a); + Assert.True(a.Equals(a)); + Assert.True(a.Equals((object?)a)); + + Assert.Equal(a.GetHashCode(), a.GetHashCode()); + } + + [Fact] + public void Arg3_Equals_Symmetric() + { + var a = new MemoizedFunction.Args(0, 0, 0); + var b = new MemoizedFunction.Args(0, 0, 0); + Assert.Equal(a, b); + Assert.Equal(b, a); + + Assert.True(a.Equals(b)); + Assert.True(b.Equals(a)); + + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + Assert.Equal(b.GetHashCode(), a.GetHashCode()); + } + + [Fact] + public void Arg3_Equals_Transitive() + { + var a = new MemoizedFunction.Args(1, 1, 1); + var b = new MemoizedFunction.Args(1, 1, 1); + var c = new MemoizedFunction.Args(1, 1, 1); + Assert.Equal(a, b); + Assert.Equal(b, c); + Assert.Equal(c, a); + + Assert.True(a.Equals(b)); + Assert.True(b.Equals(c)); + Assert.True(c.Equals(a)); + + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + Assert.Equal(b.GetHashCode(), c.GetHashCode()); + Assert.Equal(a.GetHashCode(), a.GetHashCode()); + } + + [Fact] + public void Arg3_Equals_UnequalThingsNotEqual() + { + static MemoizedFunction.Args Args(int x, int y, int z) => new(x, y, z); + + Assert.NotEqual(Args(0, 0, 0), Args(1, 0, 0)); + Assert.NotEqual(Args(0, 0, 0), Args(0, 1, 0)); + Assert.NotEqual(Args(0, 0, 0), Args(0, 0, 1)); + + Assert.NotEqual(Args(0, 0, 0).GetHashCode(), Args(1, 0, 0).GetHashCode()); + Assert.NotEqual(Args(0, 0, 0).GetHashCode(), Args(0, 1, 0).GetHashCode()); + Assert.NotEqual(Args(0, 0, 0).GetHashCode(), Args(0, 0, 1).GetHashCode()); + + Assert.False(Args(0, 0, 0).Equals(null)); + } + + [Theory] + [InlineData(null, 0, 0)] + [InlineData(0, null, 0)] + [InlineData(0, 0, null)] + public void MemoizeFunctionArity3_InvokedMultipleTimesWithNull_InvokesFunctionOnlyOnce(int? a, int? b, int? c) + { + var callCount = 0; + Func toString = (_, _, _) => + { + callCount++; + return "return value"; + }; + + var memoized = Memoize.Function(toString); + Assert.Equal("return value", memoized(a, b, c)); + Assert.Equal("return value", memoized(a, b, c)); + Assert.Equal(1, callCount); + + Assert.Equal("return value", memoized(a ?? 0 + 1, b ?? 0 + 1, c ?? 0 + 1)); + Assert.Equal(2, callCount); + } +} diff --git a/test/Shared/NumericExtensions/NumericExtensionsTests.cs b/test/Shared/NumericExtensions/NumericExtensionsTests.cs new file mode 100644 index 0000000000..8511520e98 --- /dev/null +++ b/test/Shared/NumericExtensions/NumericExtensionsTests.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using Xunit; + +namespace Microsoft.Shared.Text.Test; + +public static class NumericExtensionsTests +{ + [Fact] + public static void Int() + { + for (int i = -2000; i < 2000; i++) + { + var expected = i.ToString(CultureInfo.InvariantCulture); + var actual = i.ToInvariantString(); + Assert.Equal(expected, actual); + } + } + + [Fact] + public static void Long() + { + for (long i = -2000; i < 2000; i++) + { + var expected = i.ToString(CultureInfo.InvariantCulture); + var actual = i.ToInvariantString(); + Assert.Equal(expected, actual); + } + } +} diff --git a/test/Shared/Pools/PoolTests.cs b/test/Shared/Pools/PoolTests.cs new file mode 100644 index 0000000000..b871bca219 --- /dev/null +++ b/test/Shared/Pools/PoolTests.cs @@ -0,0 +1,316 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.ObjectPool; +using Xunit; + +namespace Microsoft.Shared.Pools.Test; + +public class PoolTests +{ + private static int _fooSequenceNum; + + private sealed class Foo : IResettable + { + public int SequenceNum { get; } + public int ResetCount { get; private set; } + public volatile bool Busy; + + public Foo() + { + SequenceNum = Interlocked.Increment(ref _fooSequenceNum); + } + + public bool TryReset() + { + ResetCount++; + return true; + } + } + + private class FooPolicy : IPooledObjectPolicy + { + public Foo Create() + { + return new Foo(); + } + + public bool Return(Foo obj) + { + if (obj.SequenceNum % 2 == 0) + { + return obj.TryReset(); + } + + return false; + } + } + + [Fact] + public void Basic() + { + const int Capacity = 200; + const int Extra = 50; + + _fooSequenceNum = -1; + var pool = PoolFactory.CreatePool(Capacity); + var set = new HashSet(); + + for (int i = 0; i < Capacity + Extra; i++) + { + set.Add(pool.Get()); + } + + foreach (var f in set) + { + pool.Return(f); + } + + // ensure we get back the original objects for anything within the capacity range + for (int i = 0; i < Capacity; i++) + { + var f = pool.Get(); + Assert.True(f.SequenceNum < Capacity, $"{i}"); + } + + // ensure we get back fresh objects for anything beyond the capacity range, demonstrating that the pool only kept capacity's worth of objects. + for (int i = Capacity; i < Capacity + Extra; i++) + { + var f = pool.Get(); + Assert.True(f.SequenceNum >= Capacity + Extra); + } + } + + [Fact] + public void Resettable() + { + _fooSequenceNum = -1; + var pool = PoolFactory.CreateResettingPool(); + + var f = pool.Get(); + Assert.Equal(0, f.ResetCount); + + pool.Return(f); + Assert.Equal(1, f.ResetCount); + } + + [Fact] + public void RespectPolicy() + { + _fooSequenceNum = -1; + var pool = PoolFactory.CreatePool(new FooPolicy()); + + var f0 = pool.Get(); + var f1 = pool.Get(); + var f2 = pool.Get(); + var f3 = pool.Get(); + + pool.Return(f0); + pool.Return(f1); + pool.Return(f2); + pool.Return(f3); + + Assert.Equal(1, f0.ResetCount); + Assert.Equal(0, f1.ResetCount); + Assert.Equal(1, f2.ResetCount); + Assert.Equal(0, f3.ResetCount); + } + + [Fact] + public void SharedStringBuilderPool() + { + var pool = PoolFactory.SharedStringBuilderPool; + var sb = pool.Get(); + Assert.NotNull(sb); + pool.Return(sb); + } + + [Fact] + public void StringBuilderPool() + { + var pool = PoolFactory.CreateStringBuilderPool(123, 2048); + + var sb = pool.Get(); + sb.Append('x', 4096); + pool.Return(sb); + + var sb2 = pool.Get(); + + Assert.NotSame(sb, sb2); + } + + [Fact] + public void ListPool() + { + var pool = PoolFactory.CreateListPool(123); + + var l = pool.Get(); + l.Add(42); + pool.Return(l); + + var l2 = pool.Get(); + + Assert.Same(l, l2); + Assert.Empty(l2); + } + + [Fact] + public void DictionaryPool() + { + var pool = PoolFactory.CreateDictionaryPool(); + + var d = pool.Get(); + d.Add("One", 1); + pool.Return(d); + + var d2 = pool.Get(); + + Assert.Same(d, d2); + Assert.Empty(d2); + } + + [Fact] + public void HashSetPool() + { + var pool = PoolFactory.CreateHashSetPool(); + + var s = pool.Get(); + s.Add(42); + pool.Return(s); + + var s2 = pool.Get(); + + Assert.Same(s, s2); + Assert.Empty(s2); + } + + [Fact] + public void CancellationTokenSourcePool_NotTriggered() + { + var pool = PoolFactory.CreateCancellationTokenSourcePool(); + + var s = pool.Get(); + pool.Return(s); + var s2 = pool.Get(); + +#if NET6_0_OR_GREATER + Assert.Same(s, s2); +#else + Assert.NotSame(s, s2); +#endif + } + + [Fact] + public void CancellationTokenSourcePool_Triggered() + { + var pool = PoolFactory.CreateCancellationTokenSourcePool(); + + var s = pool.Get(); + s.Cancel(); + pool.Return(s); + + var s2 = pool.Get(); + Assert.NotSame(s, s2); + } + + [Fact] + public void ArgChecks() + { + Assert.Throws(() => PoolFactory.CreatePool(0)); + + Assert.Throws(() => PoolFactory.CreatePool(null!, 0)); + Assert.Throws(() => PoolFactory.CreatePool(new FooPolicy(), 0)); + + Assert.Throws(() => PoolFactory.CreateResettingPool(0)); + + Assert.Throws(() => PoolFactory.CreateStringBuilderPool(0, 200)); + Assert.Throws(() => PoolFactory.CreateStringBuilderPool(200, 0)); + } + + [Fact] + public async Task Threading() + { + const int Capacity = 150; + const int Delta = 10; + + _fooSequenceNum = -1; + var pool = PoolFactory.CreatePool(maxCapacity: Capacity); + + await Task.WhenAll(new[] + { + Task.Run(() => FunWithPools(pool, 1)), + Task.Run(() => FunWithPools(pool, 2)), + Task.Run(() => FunWithPools(pool, 3)), + Task.Run(() => FunWithPools(pool, 4)) + }); + + var uniques = new HashSet(); + + // this loop does two things: + // + // #1. It ensures the pool isn't returning any duplicate objects + // #2. It ensures none of the returned items are busy + for (int i = 0; i < Capacity + Delta; i++) + { + var o = pool.Get(); + Assert.False(o.Busy); + uniques.Add(o); + } + + static void FunWithPools(ObjectPool pool, int seed) + { + var r = new Random(seed); + + var objects = new HashSet(); + for (int i = 0; i < 1000; i++) + { + // allocate some random number of objects + for (int j = 0; j < r.Next() % 256; j++) + { + var o = pool.Get(); + Assert.False(o.Busy); + o.Busy = true; + objects.Add(o); + } + + // return some random number of random objects + for (int j = 0; j < r.Next() % 256; j++) + { + if (objects.Count > 0) + { + int target = r.Next() % objects.Count; + foreach (var o in objects) + { + if (target == 0) + { + _ = objects.Remove(o); + o.Busy = false; + pool.Return(o); + break; + } + + target--; + } + } + } + } + + // return remaining objects + foreach (var o in objects) + { + o.Busy = false; + pool.Return(o); + } + } + } + + [Fact] + public void NoopPolicy() + { + Assert.True(NoopPooledObjectPolicy.Instance.Return(new object())); + } +} diff --git a/test/Shared/Pools/TestResources/ITestClass.cs b/test/Shared/Pools/TestResources/ITestClass.cs new file mode 100644 index 0000000000..9a3a9bd85e --- /dev/null +++ b/test/Shared/Pools/TestResources/ITestClass.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Shared.Pools.Test.TestResources; + +public interface ITestClass +{ + int ResetCalled { get; } + string ReadMessage(); +} diff --git a/test/Shared/Pools/TestResources/TestClass.cs b/test/Shared/Pools/TestResources/TestClass.cs new file mode 100644 index 0000000000..01b8d32e54 --- /dev/null +++ b/test/Shared/Pools/TestResources/TestClass.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.Shared.Pools.Test.TestResources; + +public class TestClass : IResettable, ITestClass +{ + public int ResetCalled { get; private set; } + private readonly TestDependency _testClass; + + public TestClass(TestDependency testClass) + { + _testClass = testClass; + } + + public string ReadMessage() => _testClass.ReadMessage(); + + public bool TryReset() + { + ResetCalled++; + return true; + } +} diff --git a/test/Shared/Pools/TestResources/TestDependency.cs b/test/Shared/Pools/TestResources/TestDependency.cs new file mode 100644 index 0000000000..ef839ee1ae --- /dev/null +++ b/test/Shared/Pools/TestResources/TestDependency.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Shared.Pools.Test.TestResources; + +public class TestDependency +{ + public const string Message = "I'm here!"; + +#pragma warning disable CA1822 + public string ReadMessage() => Message; +#pragma warning restore CA1822 +} diff --git a/test/Shared/RentedSpan/RentedSpanTest.cs b/test/Shared/RentedSpan/RentedSpanTest.cs new file mode 100644 index 0000000000..ff81b1af15 --- /dev/null +++ b/test/Shared/RentedSpan/RentedSpanTest.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Shared.Pools.Test; + +public static class RentedSpanTest +{ + [Fact] + public static void Basic() + { + using var rental1 = new RentedSpan(0); + Assert.False(rental1.Rented); + Assert.Equal(0, rental1.Span.Length); + + using var rental2 = new RentedSpan(1); + Assert.False(rental2.Rented); + Assert.Equal(0, rental2.Span.Length); + + using var rental3 = new RentedSpan(RentedSpan.MinimumRentalSpace - 1); + Assert.False(rental3.Rented); + Assert.Equal(0, rental3.Span.Length); + + using var rental4 = new RentedSpan(RentedSpan.MinimumRentalSpace); + Assert.True(rental4.Rented); + Assert.Equal(RentedSpan.MinimumRentalSpace, rental4.Span.Length); + + using var rental5 = new RentedSpan(RentedSpan.MinimumRentalSpace + 1); + Assert.True(rental5.Rented); + Assert.Equal(RentedSpan.MinimumRentalSpace + 1, rental5.Span.Length); + } +} diff --git a/test/Shared/Shared.Tests.csproj b/test/Shared/Shared.Tests.csproj new file mode 100644 index 0000000000..7fae4a8b2c --- /dev/null +++ b/test/Shared/Shared.Tests.csproj @@ -0,0 +1,26 @@ + + + Microsoft.Shared.Test + Unit tests for Microsoft.Shared + + + + $(NoWarn);CA1716 + $(NetCoreTargetFrameworks) + $(NetCoreTargetFrameworks)$(ConditionalNet462) + + + + + true + + + + + + + + + + + diff --git a/test/Shared/Text.Formatting/CompositeFormatTests.cs b/test/Shared/Text.Formatting/CompositeFormatTests.cs new file mode 100644 index 0000000000..5fde443859 --- /dev/null +++ b/test/Shared/Text.Formatting/CompositeFormatTests.cs @@ -0,0 +1,876 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Text; +using Xunit; + +#pragma warning disable S4056 // Overloads with a "CultureInfo" or an "IFormatProvider" parameter should be used + +namespace Microsoft.Shared.Text.Formatting.Test; + +public class CompositeFormatTests +{ + private static void CheckExpansion(T arg) + { + var format = "{0,256} {1}"; + + var expectedResult = string.Format(format, 3.14, arg); + var cf = CompositeFormat.Parse(format); + var actualResult1 = cf.Format(null, 3.14, arg); + var actualResult3 = new StringBuilder().AppendFormat(cf, null, 3.14, arg).ToString(); + + Assert.Equal(expectedResult, actualResult1); + Assert.Equal(expectedResult, actualResult3); + } + + private static void CheckFormatWithString(string? expectedResult, string format, T arg) + { + var cf = CompositeFormat.Parse(format); + var actualResult1 = cf.Format(null, arg); + var actualResult3 = new StringBuilder().AppendFormat((IFormatProvider?)null, cf, arg).ToString(); + + Assert.Equal(expectedResult, actualResult1); + Assert.Equal(expectedResult, actualResult3); + } + + private static void CheckFormatWithString(string? expectedResult, string format, T0 arg0, T1 arg1) + { + var cf = CompositeFormat.Parse(format); + var actualResult1 = cf.Format(null, arg0, arg1); + var actualResult3 = new StringBuilder().AppendFormat(cf, null, arg0, arg1).ToString(); + + Assert.Equal(expectedResult, actualResult1); + Assert.Equal(expectedResult, actualResult3); + } + + private static void CheckFormatWithString(string? expectedResult, string format, T0 arg0, T1 arg1, T2 arg2) + { + var cf = CompositeFormat.Parse(format); + var actualResult1 = cf.Format(null, arg0, arg1, arg2); + var actualResult3 = new StringBuilder().AppendFormat(cf, null, arg0, arg1, arg2).ToString(); + + Assert.Equal(expectedResult, actualResult1); + Assert.Equal(expectedResult, actualResult3); + } + + private static void CheckFormatWithString(string? expectedResult, string format, T0 arg0, T1 arg1, T2 arg2, params object?[]? args) + { + var cf = CompositeFormat.Parse(format); + var actualResult1 = cf.Format(null, arg0, arg1, arg2, args); + var actualResult3 = new StringBuilder().AppendFormat(cf, null, arg0, arg1, arg2, args).ToString(); + + Assert.Equal(expectedResult, actualResult1); + Assert.Equal(expectedResult, actualResult3); + } + + private static void CheckFormatWithString(string? expectedResult, string format, params object?[]? args) + { + Assert.True(CompositeFormat.TryParse(format, out var cf, out var error)); + Assert.Null(error); + + var actualResult1 = cf.Format(null, args); + var actualResult3 = new StringBuilder().AppendFormat(cf, null, args).ToString(); + + Assert.Equal(expectedResult, actualResult1); + Assert.Equal(expectedResult, actualResult3); + } + + private static void CheckFormatWithSpan(string? expectedResult, string format, T arg) + { + var cf = CompositeFormat.Parse(format); + var s = new Span(new char[Math.Max(65536, (format.Length * 2) + 128)]); + Assert.True(cf.TryFormat(s, out int charsWritten, null, arg)); + var actualResult = s.Slice(0, charsWritten).ToString(); + + Assert.Equal(expectedResult, actualResult); + } + + private static void CheckFormatWithSpan(string? expectedResult, string format, T0 arg0, T1 arg1) + { + var cf = CompositeFormat.Parse(format); + var s = new Span(new char[(format.Length * 2) + 128]); + Assert.True(cf.TryFormat(s, out int charsWritten, null, arg0, arg1)); + var actualResult = s.Slice(0, charsWritten).ToString(); + + Assert.Equal(expectedResult, actualResult); + } + + private static void CheckFormatWithSpan(string? expectedResult, string format, T0 arg0, T1 arg1, T2 arg2) + { + var cf = CompositeFormat.Parse(format); + var s = new Span(new char[(format.Length * 2) + 128]); + Assert.True(cf.TryFormat(s, out int charsWritten, null, arg0, arg1, arg2)); + var actualResult = s.Slice(0, charsWritten).ToString(); + + Assert.Equal(expectedResult, actualResult); + } + + private static void CheckFormatWithSpan(string? expectedResult, string format, T0 arg0, T1 arg1, T2 arg2, params object?[]? args) + { + var cf = CompositeFormat.Parse(format); + var s = new Span(new char[(format.Length * 2) + 128]); + Assert.True(cf.TryFormat(s, out int charsWritten, null, arg0, arg1, arg2, args)); + var actualResult = s.Slice(0, charsWritten).ToString(); + + Assert.Equal(expectedResult, actualResult); + } + + private static void CheckFormatWithSpan(string? expectedResult, string format, params object?[]? args) + { + var cf = CompositeFormat.Parse(format); + var s = new Span(new char[(format.Length * 2) + 128]); + Assert.True(cf.TryFormat(s, out int charsWritten, null, args)); + var actualResult = s.Slice(0, charsWritten).ToString(); + + Assert.Equal(expectedResult, actualResult); + } + + private static void CheckFormat(string format, T arg) + { + var expectedResult = string.Format(format, arg); + CheckFormatWithString(expectedResult, format, arg); + CheckFormatWithSpan(expectedResult, format, arg); + } + + private static void CheckFormat(string format, T0 arg0, T1 arg1) + { + var expectedResult = string.Format(format, arg0, arg1); + CheckFormatWithString(expectedResult, format, arg0, arg1); + CheckFormatWithSpan(expectedResult, format, arg0, arg1); + } + + private static void CheckFormat(string format, T0 arg0, T1 arg1, T2 arg2) + { + var expectedResult = string.Format(format, arg0, arg1, arg2); + CheckFormatWithString(expectedResult, format, arg0, arg1, arg2); + CheckFormatWithSpan(expectedResult, format, arg0, arg1, arg2); + } + + private static void CheckFormat(string format, T0 arg0, T1 arg1, T2 arg2, params object?[]? args) + { + int argLen = 3 + args!.Length; + var a = new object?[argLen]; + a[0] = arg0; + a[1] = arg1; + a[2] = arg2; + for (int i = 3; i < a.Length; i++) + { + a[i] = args![i - 3]; + } + + var expectedResult = string.Format(format, a); + CheckFormatWithString(expectedResult, format, arg0, arg1, arg2, args); + CheckFormatWithSpan(expectedResult, format, arg0, arg1, arg2, args); + } + + private static void CheckFormat(string format, params object?[] args) + { + var expectedResult = string.Format(format, args); + CheckFormatWithString(expectedResult, format, args); + CheckFormatWithSpan(expectedResult, format, args); + } + + [Theory] + [InlineData("")] + [InlineData("X")] + [InlineData("XX")] + public void NoArgs(string format) + { + CheckFormat(format); + } + + [Fact] + public void NoArgsLarge() + { + CheckFormat(new StringBuilder().Append('X', 32767).ToString()); + CheckFormat(new StringBuilder().Append('X', 32768).ToString()); + CheckFormat(new StringBuilder().Append('X', 65535).ToString()); + CheckFormat(new StringBuilder().Append('X', 65536).ToString()); + } + + [Theory] + [InlineData("{0}", 42)] + [InlineData("X{0}", 42)] + [InlineData("{0}Y", 42)] + [InlineData("X{0}Y", 42)] + [InlineData("XZ{0}ZY", 42)] + [InlineData("{0,9}", 42)] + [InlineData("{0,10}", 42)] + [InlineData("{0,19}", 42)] + [InlineData("{0,32767}", 1)] + public void OneArg(string format, int arg) + { + CheckFormat(format, arg); + } + + [Fact] + public void OneArgLarge() + { + CheckFormat(new StringBuilder().Append('X', 65535) + "{0}", 42); + CheckFormat("{0}" + new StringBuilder().Append('X', 65535), 42); + CheckFormat(new StringBuilder().Append('X', 65535) + "{0}" + new StringBuilder().Append('X', 65535), 42); + } + + [Theory] + [InlineData("{0} {1}", 42, 3.14)] + [InlineData("X{0}{1}", 42, 3.14)] + [InlineData("{0} {1}Y", 42, 3.14)] + [InlineData("X{0}{1}Y", 42, 3.14)] + [InlineData("XZ{0} {1}ZY", 42, 3.14)] + [InlineData("{0} {1} {0}", 42, 3.14)] + [InlineData("X{0}{1} {0}", 42, 3.14)] + [InlineData("{0} {1}Y {0}", 42, 3.14)] + [InlineData("X{0}{1}Y {0}", 42, 3.14)] + [InlineData("XZ{0} {1}ZY {0}", 42, 3.14)] + public void TwoArgs(string format, int arg0, double arg1) + { + CheckFormat(format, arg0, arg1); + } + + [Theory] + [InlineData("{0} {1} {2}", 42, 3.14, "XX")] + [InlineData("X{0}{1}{2}", 42, 3.14, "XX")] + [InlineData("{0} {1} {2}Y", 42, 3.14, "XX")] + [InlineData("X{0}{1}{2}Y", 42, 3.14, "XX")] + [InlineData("XZ{0} {1} {2}ZY", 42, 3.14, "XX")] + public void ThreeArgs(string format, int arg0, double arg1, string arg2) + { + CheckFormat(format, arg0, arg1, arg2); + } + + [Theory] + [InlineData("{0} {1} {2} {3}", 42, 3.14, "XX", true)] + [InlineData("X{0}{1}{2}{3}", 42, 3.14, "XX", true)] + [InlineData("{0} {1} {2} {3}Y", 42, 3.14, "XX", false)] + [InlineData("X{0}{1}{2}{3}Y", 42, 3.14, "XX", true)] + [InlineData("XZ{0} {1} {2} {3}ZY", 42, 3.14, "XX", false)] + public void FourArgs(string format, int arg0, double arg1, string arg2, bool arg3) + { + CheckFormat(format, arg0, arg1, arg2, arg3); + } + + [Fact] + public void ArgArray() + { + CheckFormat("{32767}", new object[32768]); + CheckFormat("{10}", new object[11]); + CheckFormat("{19}", new object[20]); + + CheckFormat("", Array.Empty()); + CheckFormat("X", Array.Empty()); + CheckFormat("XY", Array.Empty()); + + CheckFormat("{0}", new object[] { 42 }); + CheckFormat("X{0}", new object[] { 42 }); + CheckFormat("{0}Y", new object[] { 42 }); + CheckFormat("X{0}Y", new object[] { 42 }); + CheckFormat("XZ{0}ZY", new object[] { 42 }); + + CheckFormat("{0} {1}", new object[] { 42, 3.14 }); + CheckFormat("X{0}{1}", new object[] { 42, 3.14 }); + CheckFormat("{0} {1}Y", new object[] { 42, 3.14 }); + CheckFormat("X{0}{1}Y", new object[] { 42, 3.14 }); + CheckFormat("XZ{0} {1}ZY", new object[] { 42, 3.14 }); + + CheckFormat("{0} {1} {2}", new object[] { 42, 3.14, "XX" }); + CheckFormat("X{0}{1}{2}", new object[] { 42, 3.14, "XX" }); + CheckFormat("{0} {1} {2}Y", new object[] { 42, 3.14, "XX" }); + CheckFormat("X{0}{1}{2}Y", new object[] { 42, 3.14, "XX" }); + CheckFormat("XZ{0} {1} {2}ZY", new object[] { 42, 3.14, "XX" }); + + CheckFormat("{0} {1} {2} {3}", new object[] { 42, 3.14, "XX", true }); + CheckFormat("X{0}{1}{2}{3}", new object[] { 42, 3.14, "XX", true }); + CheckFormat("{0} {1} {2} {3}Y", new object[] { 42, 3.14, "XX", false }); + CheckFormat("X{0}{1}{2}{3}Y", new object[] { 42, 3.14, "XX", true }); + CheckFormat("XZ{0} {1} {2} {3}ZY", new object[] { 42, 3.14, "XX", false }); + + CheckFormat("XZ{0} {1} {2} {3}ZY", new object[] { "42", "3.14", "XX", "false" }); + + CheckFormat("XZ{0} {1} {2} {9}ZY", new object[] { "42", "3.14", "XX", 0, 1, 2, 3, 4, 5, "false" }); + } + + [Theory] + [InlineData("{/}")] + [InlineData("{:}")] + [InlineData("{0/}")] + [InlineData("{0,/}")] + [InlineData("{0,:}")] + [InlineData("{0,0/}")] + [InlineData("{32768}")] + [InlineData("{0,32768}")] + [InlineData("{")] + [InlineData("X{")] + [InlineData("}")] + [InlineData("X}")] + [InlineData("{X}")] + [InlineData("{100000000000000000000,2}")] + [InlineData("{0")] + [InlineData("{0,")] + [InlineData("{0,}")] + [InlineData("{0,-")] + [InlineData("{0,-}")] + [InlineData("{0,0")] + [InlineData("{0,0X")] + [InlineData("{0,1000000000000000000}")] + [InlineData("{0,0:")] + [InlineData("{0,0:{")] + [InlineData("{ 0,0}")] + [InlineData("{0,0:{{")] + [InlineData("{0,0:}}")] + [InlineData("{0,0:{{X}}")] + [InlineData("{0 ")] + [InlineData("{0, ")] + [InlineData("{0 X")] + [InlineData("{0, {")] + public void BadFormatString(string format) + { + var e = Assert.Throws(() => _ = CompositeFormat.Parse(format)); + Assert.NotEqual("", e.Message); + Assert.Equal("format", e.ParamName); + + Assert.False(CompositeFormat.TryParse(format, out var _, out var err)); + Assert.False(string.IsNullOrWhiteSpace(err)); + } + + [Theory] + [InlineData("{0, 0}", 42)] + [InlineData("{0 ,0}", 42)] + [InlineData("{0 }", 42)] + [InlineData("{0,0 }", 42)] + [InlineData("{0,0 :x}", 42)] + [InlineData("{0,0: X}", 42)] + [InlineData("{0,0:X }", 42)] + public void CheckWhitespace(string format, int arg) + { + CheckFormat(format, arg); + } + + [Fact] + public void CheckWidth() + { + for (int width = -10; width < 10; width++) + { + CheckFormat($"{{0,{width}}}", "X"); + CheckFormat($"{{0,{width}}}", "XY"); + CheckFormat($"{{0,{width}}}", "XYZ"); + } + } + + [Theory] + [InlineData("{{{0}", 42)] + [InlineData("{{{0}}}", 42)] + public void CheckEscapes(string format, int arg) + { + CheckFormat(format, arg); + } + + [Fact] + public void BadNumArgs() + { + var cf = CompositeFormat.Parse("{0} {2}"); + + Assert.Throws(() => cf.Format(null, 1)); + Assert.Throws(() => cf.Format(null, 1, 2)); + Assert.Equal("1 3", cf.Format(null, 1, 2, 3)); + Assert.Equal("1 3", cf.Format(null, 1, 2, 3, 4)); + } + + private struct Custom1 : IFormattable + { + public string ToString(string? format, IFormatProvider? formatProvider) + { + return "IFormattable Output"; + } + } + +#if NET6_0_OR_GREATER + private struct Custom2 : ISpanFormattable + { + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) + { + if (destination.Length < 16) + { + charsWritten = 0; + return false; + } + + "TryFormat Output".AsSpan().CopyTo(destination); + charsWritten = 16; + return true; + } + + // NOTE: If/when this test is built as part of the .NET release, + // this should be removed. It's needed because String.Format + // doesn't recognize my hacky ISpanFormattable. + public string ToString(string? format, IFormatProvider? formatProvider) + { + return "TryFormat Output"; + } + } +#endif + + [Fact] + public void ArgTypes() + { + CheckFormat("{0}", (sbyte)42); + CheckFormat("{0}", (short)42); + CheckFormat("{0}", 42); + CheckFormat("{0}", 42L); + CheckFormat("{0}", (byte)42); + CheckFormat("{0}", (ushort)42); + CheckFormat("{0}", 42U); + CheckFormat("{0}", 42UL); + CheckFormat("{0}", 42.0F); + CheckFormat("{0}", 42.0); + CheckFormat("{0}", 'x'); + CheckFormat("{0}", new DateTime(2000, 1, 1)); + CheckFormat("{0}", new TimeSpan(42)); + CheckFormat("{0}", true); + CheckFormat("{0}", new decimal(42.0)); + CheckFormat("{0}", new Guid(new byte[] { 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 })); + CheckFormat("{0}", "XYZ"); + CheckFormat("{0}", new object?[] { null }); + CheckFormat("{0}", default(Custom1)); + +#if NET6_0_OR_GREATER + CheckFormat("{0}", default(Custom2)); +#endif + } + + [Fact] + public void BufferExpansion() + { + CheckExpansion((sbyte)42); + CheckExpansion((short)42); + CheckExpansion(42); + CheckExpansion(42L); + CheckExpansion((byte)42); + CheckExpansion((ushort)42); + CheckExpansion(42U); + CheckExpansion(42UL); + CheckExpansion(42.0F); + CheckExpansion(42.0); + CheckExpansion('X'); + CheckExpansion(new DateTime(2000, 1, 1)); + CheckExpansion(new TimeSpan(42)); + CheckExpansion(true); + CheckExpansion(new decimal(42.0)); + CheckExpansion(new Guid(new byte[] { 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 })); + CheckExpansion("XYZ"); + CheckExpansion(new object?[] { null }); + CheckExpansion(default(Custom1)); + +#if NET6_0_OR_GREATER + CheckExpansion(default(Custom2)); +#endif + } + + [Theory] + [InlineData("{0:d}", 0)] + [InlineData("{0:d}", 5)] + [InlineData("{0:d}", 10)] + [InlineData("{0:d}", 15)] + [InlineData("{0:d}", 100)] + [InlineData("{0:d}", 123)] + [InlineData("{0:d}", 1024)] + [InlineData("{0:d}", -5)] + [InlineData("{0:d}", -10)] + [InlineData("{0:d}", -15)] + [InlineData("{0:d}", -100)] + [InlineData("{0:d}", -123)] + [InlineData("{0:d}", -1024)] + [InlineData("{0:d1}", 0)] + [InlineData("{0:d1}", 5)] + [InlineData("{0:d1}", 10)] + [InlineData("{0:d1}", 15)] + [InlineData("{0:d1}", 100)] + [InlineData("{0:d1}", 123)] + [InlineData("{0:d1}", 1024)] + [InlineData("{0:d1}", -5)] + [InlineData("{0:d1}", -10)] + [InlineData("{0:d1}", -15)] + [InlineData("{0:d1}", -100)] + [InlineData("{0:d1}", -123)] + [InlineData("{0:d1}", -1024)] + [InlineData("{0:d2}", 0)] + [InlineData("{0:d2}", 5)] + [InlineData("{0:d2}", 10)] + [InlineData("{0:d2}", 15)] + [InlineData("{0:d2}", 100)] + [InlineData("{0:d2}", 123)] + [InlineData("{0:d2}", 1024)] + [InlineData("{0:d2}", -5)] + [InlineData("{0:d2}", -10)] + [InlineData("{0:d2}", -15)] + [InlineData("{0:d2}", -100)] + [InlineData("{0:d2}", -123)] + [InlineData("{0:d2}", -1024)] + [InlineData("{0:d3}", 0)] + [InlineData("{0:d3}", 5)] + [InlineData("{0:d3}", 10)] + [InlineData("{0:d3}", 15)] + [InlineData("{0:d3}", 100)] + [InlineData("{0:d3}", 123)] + [InlineData("{0:d3}", 1024)] + [InlineData("{0:d3}", -5)] + [InlineData("{0:d3}", -10)] + [InlineData("{0:d3}", -15)] + [InlineData("{0:d3}", -100)] + [InlineData("{0:d3}", -123)] + [InlineData("{0:d3}", -1024)] + [InlineData("{0:d4}", 0)] + [InlineData("{0:d4}", 5)] + [InlineData("{0:d4}", 10)] + [InlineData("{0:d4}", 15)] + [InlineData("{0:d4}", 100)] + [InlineData("{0:d4}", 123)] + [InlineData("{0:d4}", 1024)] + [InlineData("{0:d4}", -5)] + [InlineData("{0:d4}", -10)] + [InlineData("{0:d4}", -15)] + [InlineData("{0:d4}", -100)] + [InlineData("{0:d4}", -123)] + [InlineData("{0:d4}", -1024)] + public void TestStringFormatD(string format, int arg) + { + CheckFormat(format, arg); + } + + [Theory] + [InlineData("{0,1:d}", 0)] + [InlineData("{0,1:d}", 5)] + [InlineData("{0,1:d}", 10)] + [InlineData("{0,1:d}", 15)] + [InlineData("{0,1:d}", 100)] + [InlineData("{0,1:d}", 123)] + [InlineData("{0,1:d}", 1024)] + [InlineData("{0,1:d}", -5)] + [InlineData("{0,1:d}", -10)] + [InlineData("{0,1:d}", -15)] + [InlineData("{0,1:d}", -100)] + [InlineData("{0,1:d}", -123)] + [InlineData("{0,1:d}", -1024)] + [InlineData("{0,1:d1}", 0)] + [InlineData("{0,1:d1}", 5)] + [InlineData("{0,1:d1}", 10)] + [InlineData("{0,1:d1}", 15)] + [InlineData("{0,1:d1}", 100)] + [InlineData("{0,1:d1}", 123)] + [InlineData("{0,1:d1}", 1024)] + [InlineData("{0,1:d1}", -5)] + [InlineData("{0,1:d1}", -10)] + [InlineData("{0,1:d1}", -15)] + [InlineData("{0,1:d1}", -100)] + [InlineData("{0,1:d1}", -123)] + [InlineData("{0,1:d1}", -1024)] + [InlineData("{0,1:d2}", 0)] + [InlineData("{0,1:d2}", 5)] + [InlineData("{0,1:d2}", 10)] + [InlineData("{0,1:d2}", 15)] + [InlineData("{0,1:d2}", 100)] + [InlineData("{0,1:d2}", 123)] + [InlineData("{0,1:d2}", 1024)] + [InlineData("{0,1:d2}", -5)] + [InlineData("{0,1:d2}", -10)] + [InlineData("{0,1:d2}", -15)] + [InlineData("{0,1:d2}", -100)] + [InlineData("{0,1:d2}", -123)] + [InlineData("{0,1:d2}", -1024)] + [InlineData("{0,1:d3}", 0)] + [InlineData("{0,1:d3}", 5)] + [InlineData("{0,1:d3}", 10)] + [InlineData("{0,1:d3}", 15)] + [InlineData("{0,1:d3}", 100)] + [InlineData("{0,1:d3}", 123)] + [InlineData("{0,1:d3}", 1024)] + [InlineData("{0,1:d3}", -5)] + [InlineData("{0,1:d3}", -10)] + [InlineData("{0,1:d3}", -15)] + [InlineData("{0,1:d3}", -100)] + [InlineData("{0,1:d3}", -123)] + [InlineData("{0,1:d3}", -1024)] + [InlineData("{0,1:d4}", 0)] + [InlineData("{0,1:d4}", 5)] + [InlineData("{0,1:d4}", 10)] + [InlineData("{0,1:d4}", 15)] + [InlineData("{0,1:d4}", 100)] + [InlineData("{0,1:d4}", 123)] + [InlineData("{0,1:d4}", 1024)] + [InlineData("{0,1:d4}", -5)] + [InlineData("{0,1:d4}", -10)] + [InlineData("{0,1:d4}", -15)] + [InlineData("{0,1:d4}", -100)] + [InlineData("{0,1:d4}", -123)] + [InlineData("{0,1:d4}", -1024)] + public void TestStringFormatD1(string format, int arg) + { + CheckFormat(format, arg); + } + + [Theory] + [InlineData("{0,2:d}", 0)] + [InlineData("{0,2:d}", 5)] + [InlineData("{0,2:d}", 10)] + [InlineData("{0,2:d}", 15)] + [InlineData("{0,2:d}", 100)] + [InlineData("{0,2:d}", 123)] + [InlineData("{0,2:d}", 1024)] + [InlineData("{0,2:d}", -5)] + [InlineData("{0,2:d}", -10)] + [InlineData("{0,2:d}", -15)] + [InlineData("{0,2:d}", -100)] + [InlineData("{0,2:d}", -123)] + [InlineData("{0,2:d}", -1024)] + [InlineData("{0,2:d1}", 0)] + [InlineData("{0,2:d1}", 5)] + [InlineData("{0,2:d1}", 10)] + [InlineData("{0,2:d1}", 15)] + [InlineData("{0,2:d1}", 100)] + [InlineData("{0,2:d1}", 123)] + [InlineData("{0,2:d1}", 1024)] + [InlineData("{0,2:d1}", -5)] + [InlineData("{0,2:d1}", -10)] + [InlineData("{0,2:d1}", -15)] + [InlineData("{0,2:d1}", -100)] + [InlineData("{0,2:d1}", -123)] + [InlineData("{0,2:d1}", -1024)] + [InlineData("{0,2:d2}", 0)] + [InlineData("{0,2:d2}", 5)] + [InlineData("{0,2:d2}", 10)] + [InlineData("{0,2:d2}", 15)] + [InlineData("{0,2:d2}", 100)] + [InlineData("{0,2:d2}", 123)] + [InlineData("{0,2:d2}", 1024)] + [InlineData("{0,2:d2}", -5)] + [InlineData("{0,2:d2}", -10)] + [InlineData("{0,2:d2}", -15)] + [InlineData("{0,2:d2}", -100)] + [InlineData("{0,2:d2}", -123)] + [InlineData("{0,2:d2}", -1024)] + [InlineData("{0,2:d3}", 0)] + [InlineData("{0,2:d3}", 5)] + [InlineData("{0,2:d3}", 10)] + [InlineData("{0,2:d3}", 15)] + [InlineData("{0,2:d3}", 100)] + [InlineData("{0,2:d3}", 123)] + [InlineData("{0,2:d3}", 1024)] + [InlineData("{0,2:d3}", -5)] + [InlineData("{0,2:d3}", -10)] + [InlineData("{0,2:d3}", -15)] + [InlineData("{0,2:d3}", -100)] + [InlineData("{0,2:d3}", -123)] + [InlineData("{0,2:d3}", -1024)] + [InlineData("{0,2:d4}", 0)] + [InlineData("{0,2:d4}", 5)] + [InlineData("{0,2:d4}", 10)] + [InlineData("{0,2:d4}", 15)] + [InlineData("{0,2:d4}", 100)] + [InlineData("{0,2:d4}", 123)] + [InlineData("{0,2:d4}", 1024)] + [InlineData("{0,2:d4}", -5)] + [InlineData("{0,2:d4}", -10)] + [InlineData("{0,2:d4}", -15)] + [InlineData("{0,2:d4}", -100)] + [InlineData("{0,2:d4}", -123)] + [InlineData("{0,2:d4}", -1024)] + public void TestStringFormatD2(string format, int arg) + { + CheckFormat(format, arg); + } + + [Theory] + [InlineData("{0,3:d}", 0)] + [InlineData("{0,3:d}", 5)] + [InlineData("{0,3:d}", 10)] + [InlineData("{0,3:d}", 15)] + [InlineData("{0,3:d}", 100)] + [InlineData("{0,3:d}", 123)] + [InlineData("{0,3:d}", 1024)] + [InlineData("{0,3:d}", -5)] + [InlineData("{0,3:d}", -10)] + [InlineData("{0,3:d}", -15)] + [InlineData("{0,3:d}", -100)] + [InlineData("{0,3:d}", -123)] + [InlineData("{0,3:d}", -1024)] + [InlineData("{0,3:d1}", 0)] + [InlineData("{0,3:d1}", 5)] + [InlineData("{0,3:d1}", 10)] + [InlineData("{0,3:d1}", 15)] + [InlineData("{0,3:d1}", 100)] + [InlineData("{0,3:d1}", 123)] + [InlineData("{0,3:d1}", 1024)] + [InlineData("{0,3:d1}", -5)] + [InlineData("{0,3:d1}", -10)] + [InlineData("{0,3:d1}", -15)] + [InlineData("{0,3:d1}", -100)] + [InlineData("{0,3:d1}", -123)] + [InlineData("{0,3:d1}", -1024)] + [InlineData("{0,3:d2}", 0)] + [InlineData("{0,3:d2}", 5)] + [InlineData("{0,3:d2}", 10)] + [InlineData("{0,3:d2}", 15)] + [InlineData("{0,3:d2}", 100)] + [InlineData("{0,3:d2}", 123)] + [InlineData("{0,3:d2}", 1024)] + [InlineData("{0,3:d2}", -5)] + [InlineData("{0,3:d2}", -10)] + [InlineData("{0,3:d2}", -15)] + [InlineData("{0,3:d2}", -100)] + [InlineData("{0,3:d2}", -123)] + [InlineData("{0,3:d2}", -1024)] + [InlineData("{0,3:d3}", 0)] + [InlineData("{0,3:d3}", 5)] + [InlineData("{0,3:d3}", 10)] + [InlineData("{0,3:d3}", 15)] + [InlineData("{0,3:d3}", 100)] + [InlineData("{0,3:d3}", 123)] + [InlineData("{0,3:d3}", 1024)] + [InlineData("{0,3:d3}", -5)] + [InlineData("{0,3:d3}", -10)] + [InlineData("{0,3:d3}", -15)] + [InlineData("{0,3:d3}", -100)] + [InlineData("{0,3:d3}", -123)] + [InlineData("{0,3:d3}", -1024)] + [InlineData("{0,3:d4}", 0)] + [InlineData("{0,3:d4}", 5)] + [InlineData("{0,3:d4}", 10)] + [InlineData("{0,3:d4}", 15)] + [InlineData("{0,3:d4}", 100)] + [InlineData("{0,3:d4}", 123)] + [InlineData("{0,3:d4}", 1024)] + [InlineData("{0,3:d4}", -5)] + [InlineData("{0,3:d4}", -10)] + [InlineData("{0,3:d4}", -15)] + [InlineData("{0,3:d4}", -100)] + [InlineData("{0,3:d4}", -123)] + [InlineData("{0,3:d4}", -1024)] + public void TestStringFormatD3(string format, int arg) + { + CheckFormat(format, arg); + } + + [Theory] + [InlineData("{0,4:d}", 0)] + [InlineData("{0,4:d}", 5)] + [InlineData("{0,4:d}", 10)] + [InlineData("{0,4:d}", 15)] + [InlineData("{0,4:d}", 100)] + [InlineData("{0,4:d}", 123)] + [InlineData("{0,4:d}", 1024)] + [InlineData("{0,4:d}", -5)] + [InlineData("{0,4:d}", -10)] + [InlineData("{0,4:d}", -15)] + [InlineData("{0,4:d}", -100)] + [InlineData("{0,4:d}", -123)] + [InlineData("{0,4:d}", -1024)] + [InlineData("{0,4:d1}", 0)] + [InlineData("{0,4:d1}", 5)] + [InlineData("{0,4:d1}", 10)] + [InlineData("{0,4:d1}", 15)] + [InlineData("{0,4:d1}", 100)] + [InlineData("{0,4:d1}", 123)] + [InlineData("{0,4:d1}", 1024)] + [InlineData("{0,4:d1}", -5)] + [InlineData("{0,4:d1}", -10)] + [InlineData("{0,4:d1}", -15)] + [InlineData("{0,4:d1}", -100)] + [InlineData("{0,4:d1}", -123)] + [InlineData("{0,4:d1}", -1024)] + [InlineData("{0,4:d2}", 0)] + [InlineData("{0,4:d2}", 5)] + [InlineData("{0,4:d2}", 10)] + [InlineData("{0,4:d2}", 15)] + [InlineData("{0,4:d2}", 100)] + [InlineData("{0,4:d2}", 123)] + [InlineData("{0,4:d2}", 1024)] + [InlineData("{0,4:d2}", -5)] + [InlineData("{0,4:d2}", -10)] + [InlineData("{0,4:d2}", -15)] + [InlineData("{0,4:d2}", -100)] + [InlineData("{0,4:d2}", -123)] + [InlineData("{0,4:d2}", -1024)] + [InlineData("{0,4:d3}", 0)] + [InlineData("{0,4:d3}", 5)] + [InlineData("{0,4:d3}", 10)] + [InlineData("{0,4:d3}", 15)] + [InlineData("{0,4:d3}", 100)] + [InlineData("{0,4:d3}", 123)] + [InlineData("{0,4:d3}", 1024)] + [InlineData("{0,4:d3}", -5)] + [InlineData("{0,4:d3}", -10)] + [InlineData("{0,4:d3}", -15)] + [InlineData("{0,4:d3}", -100)] + [InlineData("{0,4:d3}", -123)] + [InlineData("{0,4:d3}", -1024)] + [InlineData("{0,4:d4}", 0)] + [InlineData("{0,4:d4}", 5)] + [InlineData("{0,4:d4}", 10)] + [InlineData("{0,4:d4}", 15)] + [InlineData("{0,4:d4}", 100)] + [InlineData("{0,4:d4}", 123)] + [InlineData("{0,4:d4}", 1024)] + [InlineData("{0,4:d4}", -5)] + [InlineData("{0,4:d4}", -10)] + [InlineData("{0,4:d4}", -15)] + [InlineData("{0,4:d4}", -100)] + [InlineData("{0,4:d4}", -123)] + [InlineData("{0,4:d4}", -1024)] + public void TestStringFormatD4(string format, int arg) + { + CheckFormat(format, arg); + } + + [Theory] + [InlineData("{0}", 1)] + [InlineData("{0}{1}", 2)] + [InlineData("{0,3}", 1)] + [InlineData("{0,3:d}", 1)] + [InlineData("{0,3:d}{0}", 1)] + [InlineData("{0,3:d}{1}", 2)] + [InlineData("{0,3:d}{9}", 10)] + public void TestNumArgsNeeded(string format, int argsExpected) + { + var cf = CompositeFormat.Parse(format); + Assert.Equal(argsExpected, cf.NumArgumentsNeeded); + } + + [Fact] + public void OverflowNoArgs() + { + var cf = CompositeFormat.Parse("0123"); + Assert.False(cf.TryFormat(new char[3], out var charsWritten, null, null)); + Assert.Equal(0, charsWritten); + + Assert.True(cf.TryFormat(new char[4], out charsWritten, null, null)); + Assert.Equal(4, charsWritten); + } + + [Fact] + public void TemplateFormat() + { + var cf = CompositeFormat.Parse("{one} {_two} {t_hree} {one} {f4}".AsSpan(), out var templates); + Assert.Equal(4, templates.Count); + Assert.Equal("one", templates[0]); + Assert.Equal("_two", templates[1]); + Assert.Equal("t_hree", templates[2]); + Assert.Equal("f4", templates[3]); + Assert.Equal(4, cf.NumArgumentsNeeded); + Assert.Equal("ONE TWO THREE ONE FOUR", cf.Format(null, "ONE", "TWO", "THREE", "FOUR")); + + var ex = Assert.Throws(() => CompositeFormat.Parse("{".AsSpan(), out templates)); + Assert.Contains("format string", ex.Message); + + ex = Assert.Throws(() => CompositeFormat.Parse("{@".AsSpan(), out templates)); + Assert.Contains("format string", ex.Message); + + ex = Assert.Throws(() => CompositeFormat.Parse("{a".AsSpan(), out templates)); + Assert.Contains("format string", ex.Message); + + ex = Assert.Throws(() => CompositeFormat.Parse("{_".AsSpan(), out templates)); + Assert.Contains("format string", ex.Message); + + ex = Assert.Throws(() => CompositeFormat.Parse("{0}".AsSpan(), out templates)); + Assert.Contains("format string", ex.Message); + } +} diff --git a/test/Shared/Text.Formatting/MakerTests.cs b/test/Shared/Text.Formatting/MakerTests.cs new file mode 100644 index 0000000000..c0a104456c --- /dev/null +++ b/test/Shared/Text.Formatting/MakerTests.cs @@ -0,0 +1,532 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +#pragma warning disable S4056 // Overloads with a "CultureInfo" or an "IFormatProvider" parameter should be used + +namespace Microsoft.Shared.Text.Formatting.Test; + +public class MakerTests +{ + [Fact] + public void TestExpansion_Int64() + { + StringMaker sm; + string expected; + string actual; + + long o = 123456; + for (int capacity = 0; capacity < 20; capacity++) + { + for (int width = -10; width < 10; width++) + { + sm = new StringMaker(capacity); + sm.Append(o, "", null, width); + actual = sm.ExtractString(); + expected = string.Format(string.Format("{{0,{0}}}", width), o); + Assert.Equal(expected, actual); + sm.Dispose(); + + sm = new StringMaker(capacity); + sm.Append(o, "", null, width); + actual = sm.ExtractSpan().ToString(); + Assert.Equal(expected, actual); + sm.Dispose(); + } + } + + sm = new StringMaker(Array.Empty(), true); + sm.Append(o, string.Empty, null, 0); + Assert.True(sm.Overflowed); + Assert.Equal(string.Empty, sm.ExtractSpan().ToString()); + Assert.Equal(string.Empty, sm.ExtractString()); + sm.Dispose(); + } + + [Fact] + public void TestExpansion_Uint64() + { + StringMaker sm; + string expected; + string actual; + + ulong o = 123456; + for (int capacity = 0; capacity < 20; capacity++) + { + for (int width = -10; width < 10; width++) + { + sm = new StringMaker(capacity); + sm.Append(o, "", null, width); + actual = sm.ExtractString(); + expected = string.Format(string.Format("{{0,{0}}}", width), o); + Assert.Equal(expected, actual); + sm.Dispose(); + } + } + + sm = new StringMaker(Array.Empty(), true); + sm.Append(o, string.Empty, null, 0); + Assert.True(sm.Overflowed); + Assert.Equal(string.Empty, sm.ExtractSpan().ToString()); + Assert.Equal(string.Empty, sm.ExtractString()); + sm.Dispose(); + + sm = new StringMaker(new char[1], true); + sm.Append(1, string.Empty, null, 2); + Assert.True(sm.Overflowed); + Assert.Equal("1", sm.ExtractString()); + sm.Dispose(); + } + + [Fact] + public void TestExpansion_Double() + { + StringMaker sm; + string expected; + string actual; + + double o = 123.456; + for (int capacity = 0; capacity < 20; capacity++) + { + for (int width = -10; width < 10; width++) + { + sm = new StringMaker(capacity); + sm.Append(o, "", null, width); + actual = sm.ExtractString(); + expected = string.Format(string.Format("{{0,{0}}}", width), o); + Assert.Equal(expected, actual); + sm.Dispose(); + } + } + + sm = new StringMaker(Array.Empty(), true); + sm.Append(o, string.Empty, null, 0); + Assert.True(sm.Overflowed); + Assert.Equal(string.Empty, sm.ExtractSpan().ToString()); + Assert.Equal(string.Empty, sm.ExtractString()); + sm.Dispose(); + } + + [Fact] + public void TestExpansion_Bool() + { + StringMaker sm; + string expected; + string actual; + + bool o = true; + for (int capacity = 0; capacity < 20; capacity++) + { + for (int width = -10; width < 10; width++) + { + sm = new StringMaker(capacity); + sm.Append(o, width); + actual = sm.ExtractString(); + expected = string.Format(string.Format("{{0,{0}}}", width), o); + Assert.Equal(expected, actual); + sm.Dispose(); + } + } + + sm = new StringMaker(Array.Empty(), true); + sm.Append(o, 0); + Assert.True(sm.Overflowed); + Assert.Equal(string.Empty, sm.ExtractSpan().ToString()); + Assert.Equal(string.Empty, sm.ExtractString()); + sm.Dispose(); + } + + [Fact] + public void TestExpansion_Decimal() + { + StringMaker sm; + string expected; + string actual; + + decimal o = new(123.456); + for (int capacity = 0; capacity < 20; capacity++) + { + for (int width = -10; width < 10; width++) + { + sm = new StringMaker(capacity); + sm.Append(o, "", null, width); + actual = sm.ExtractString(); + expected = string.Format(string.Format("{{0,{0}}}", width), o); + Assert.Equal(expected, actual); + sm.Dispose(); + } + } + + sm = new StringMaker(Array.Empty(), true); + sm.Append(o, string.Empty, null, 0); + Assert.True(sm.Overflowed); + Assert.Equal(string.Empty, sm.ExtractSpan().ToString()); + Assert.Equal(string.Empty, sm.ExtractString()); + sm.Dispose(); + } + + [Fact] + public void TestExpansion_DateTime() + { + StringMaker sm; + string expected; + string actual; + + var o = new DateTime(2000, 1, 1); + for (int capacity = 0; capacity < 20; capacity++) + { + for (int width = -10; width < 10; width++) + { + sm = new StringMaker(capacity); + sm.Append(o, "", null, width); + actual = sm.ExtractString(); + expected = string.Format(string.Format("{{0,{0}}}", width), o); + Assert.Equal(expected, actual); + sm.Dispose(); + } + } + + sm = new StringMaker(Array.Empty(), true); + sm.Append(o, string.Empty, null, 0); + Assert.True(sm.Overflowed); + Assert.Equal(string.Empty, sm.ExtractSpan().ToString()); + Assert.Equal(string.Empty, sm.ExtractString()); + sm.Dispose(); + } + + [Fact] + public void TestExpansion_TimeSpan() + { + StringMaker sm; + string expected; + string actual; + + var o = new TimeSpan(123456); + for (int capacity = 0; capacity < 20; capacity++) + { + for (int width = -10; width < 10; width++) + { + sm = new StringMaker(capacity); + sm.Append(o, "", null, width); + actual = sm.ExtractString(); + expected = string.Format(string.Format("{{0,{0}}}", width), o); + Assert.Equal(expected, actual); + sm.Dispose(); + } + } + + sm = new StringMaker(Array.Empty(), true); + sm.Append(o, string.Empty, null, 0); + Assert.True(sm.Overflowed); + Assert.Equal(string.Empty, sm.ExtractSpan().ToString()); + Assert.Equal(string.Empty, sm.ExtractString()); + sm.Dispose(); + } + + [Fact] + public void TestExpansion_Char() + { + StringMaker sm; + string expected; + string actual; + + char o = 'x'; + for (int capacity = 0; capacity < 20; capacity++) + { + sm = new StringMaker(capacity); + sm.Append(o); + actual = sm.ExtractString(); + expected = string.Format("{0}", o); + Assert.Equal(expected, actual); + sm.Dispose(); + + for (int width = -10; width < 10; width++) + { + sm = new StringMaker(capacity); + sm.Append(o, width); + actual = sm.ExtractString(); + expected = string.Format(string.Format("{{0,{0}}}", width), o); + Assert.Equal(expected, actual); + sm.Dispose(); + } + } + + sm = new StringMaker(Array.Empty(), true); + sm.Append(o); + Assert.True(sm.Overflowed); + Assert.Equal(string.Empty, sm.ExtractString()); + sm.Dispose(); + + sm = new StringMaker(Array.Empty(), true); + sm.Append(o, 0); + Assert.True(sm.Overflowed); + Assert.Equal(string.Empty, sm.ExtractSpan().ToString()); + Assert.Equal(string.Empty, sm.ExtractString()); + sm.Dispose(); + + sm = new StringMaker(Array.Empty(), true); + sm.Append(o, 2); + Assert.True(sm.Overflowed); + Assert.Equal(string.Empty, sm.ExtractString()); + sm.Dispose(); + + sm = new StringMaker(Array.Empty(), true); + sm.Append(o, -2); + Assert.True(sm.Overflowed); + Assert.Equal(string.Empty, sm.ExtractString()); + sm.Dispose(); + } + + [Fact] + public void TestExpansion_String() + { + StringMaker sm; + string expected; + string actual; + + var o = "123456"; + for (int capacity = 0; capacity < 20; capacity++) + { + for (int width = -10; width < 10; width++) + { + sm = new StringMaker(capacity); + sm.Append(o, width); + actual = sm.ExtractString(); + expected = string.Format(string.Format("{{0,{0}}}", width), o); + Assert.Equal(expected, actual); + sm.Dispose(); + } + } + + sm = new StringMaker(Array.Empty(), true); + sm.Append(o, 0); + Assert.True(sm.Overflowed); + Assert.Equal(string.Empty, sm.ExtractSpan().ToString()); + Assert.Equal(string.Empty, sm.ExtractString()); + sm.Dispose(); + } + + [Fact] + public void TestExpansion_Span() + { + StringMaker sm; + string expected; + string actual; + + var o = "123456".AsSpan(); + for (int capacity = 0; capacity < 20; capacity++) + { + sm = new StringMaker(capacity); + sm.Append(o); + actual = sm.ExtractString(); + expected = string.Format("{0}", "123456"); + Assert.Equal(expected, actual); + sm.Dispose(); + + for (int width = -10; width < 10; width++) + { + sm = new StringMaker(capacity); + sm.Append(o, width); + actual = sm.ExtractString(); + expected = string.Format(string.Format("{{0,{0}}}", width), "123456"); + Assert.Equal(expected, actual); + sm.Dispose(); + } + } + + sm = new StringMaker(Array.Empty(), true); + sm.Append(o); + Assert.True(sm.Overflowed); + Assert.Equal(string.Empty, sm.ExtractString()); + sm.Dispose(); + + sm = new StringMaker(Array.Empty(), true); + sm.Append(o, 0); + Assert.True(sm.Overflowed); + Assert.Equal(string.Empty, sm.ExtractSpan().ToString()); + Assert.Equal(string.Empty, sm.ExtractString()); + sm.Dispose(); + } + + [Fact] + public void TestExpansion_Object() + { + StringMaker sm; + string expected; + string actual; + + var o = (object)"123456"; + for (int capacity = 0; capacity < 20; capacity++) + { + for (int width = -10; width < 10; width++) + { + sm = new StringMaker(capacity); + sm.Append(o, width); + actual = sm.ExtractString(); + expected = string.Format(string.Format("{{0,{0}}}", width), o); + Assert.Equal(expected, actual); + sm.Dispose(); + } + } + + sm = new StringMaker(Array.Empty(), true); + sm.Append(o, 0); + Assert.True(sm.Overflowed); + Assert.Equal(string.Empty, sm.ExtractSpan().ToString()); + Assert.Equal(string.Empty, sm.ExtractString()); + sm.Dispose(); + } + +#if NET6_0_OR_GREATER + private struct LegacySpanFormattable : System.ISpanFormattable + { + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) + { + if (destination.Length < 6) + { + charsWritten = 0; + return false; + } + + "123456".AsSpan().CopyTo(destination); + charsWritten = 6; + return true; + } + + public override string ToString() => "123456"; + public string ToString(string? format, IFormatProvider? formatProvider) => "123456"; + } + + [Fact] + public void TestExpansion_T() + { + StringMaker sm; + string expected; + string actual; + + var o = default(LegacySpanFormattable); + for (int capacity = 0; capacity < 20; capacity++) + { + for (int width = -10; width < 10; width++) + { + sm = new StringMaker(capacity); + sm.Append(o, string.Empty, null, width); + actual = sm.ExtractString(); + expected = string.Format(string.Format("{{0,{0}}}", width), o); + Assert.Equal(expected, actual); + sm.Dispose(); + } + } + + sm = new StringMaker(Array.Empty(), true); + sm.Append(o, string.Empty, null, 0); + Assert.True(sm.Overflowed); + Assert.Equal(string.Empty, sm.ExtractSpan().ToString()); + Assert.Equal(string.Empty, sm.ExtractString()); + sm.Dispose(); + } +#endif + + private struct Formattable : IFormattable + { + public string ToString(string? format, IFormatProvider? formatProvider) + { + return "123456"; + } + } + + [Fact] + public void TestExpansion_Formattable() + { + StringMaker sm; + string expected; + string actual; + + Formattable o = default; + for (int capacity = 0; capacity < 20; capacity++) + { + for (int width = -10; width < 10; width++) + { + sm = new StringMaker(capacity); + sm.Append(o, string.Empty, null, width); + actual = sm.ExtractString(); + expected = string.Format(string.Format("{{0,{0}}}", width), o); + Assert.Equal(expected, actual); + sm.Dispose(); + } + } + + sm = new StringMaker(Array.Empty(), true); + sm.Append(o, string.Empty, null, 0); + Assert.True(sm.Overflowed); + Assert.Equal(string.Empty, sm.ExtractSpan().ToString()); + Assert.Equal(string.Empty, sm.ExtractString()); + sm.Dispose(); + } + + [Fact] + public void TestExpansion_Fill() + { + StringMaker sm; + + for (int capacity = 0; capacity < 20; capacity++) + { + sm = new StringMaker(capacity); + sm.Fill('X', 6); + var actual = sm.ExtractString(); + var expected = string.Format("{0}", "XXXXXX"); + Assert.Equal(expected, actual); + sm.Dispose(); + } + + sm = new StringMaker(Array.Empty(), true); + sm.Fill('X', 6); + Assert.True(sm.Overflowed); + Assert.Equal(string.Empty, sm.ExtractString()); + sm.Dispose(); + + sm = new StringMaker(Array.Empty(), true); + sm.Fill('X', 6); + Assert.Equal(string.Empty, sm.ExtractSpan().ToString()); + sm.Dispose(); + + sm = new StringMaker(Array.Empty(), true); + sm.Fill('X', 6); + Assert.True(sm.Overflowed); + Assert.Equal(string.Empty, sm.ExtractString()); + sm.Dispose(); + } + + [Fact] + public void TestNullArgs() + { + StringMaker sm; + + sm = default; +#if NETCOREAPP3_1_OR_GREATER + sm.Append(null, 12); +#else + sm.Append((string?)null, 12); +#endif + Assert.Equal(" ", sm.ExtractString()); + sm.Dispose(); + + sm = default; + sm.Append((object?)null, 12); + Assert.Equal(" ", sm.ExtractString()); + sm.Dispose(); + } + + [Fact] + public void TestCapacity() + { + Assert.Throws(() => new StringMaker(-1)); + + var sm = new StringMaker(0); + Assert.Equal(0, sm.Length); + Assert.False(sm.Overflowed); + sm.Dispose(); + } +} diff --git a/test/Shared/Throw/DoubleTests.cs b/test/Shared/Throw/DoubleTests.cs new file mode 100644 index 0000000000..f261efdb9d --- /dev/null +++ b/test/Shared/Throw/DoubleTests.cs @@ -0,0 +1,177 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.Shared.Diagnostics.Test; + +#pragma warning disable S3236 // Caller information arguments should not be provided explicitly + +public class DoubleTests +{ + #region For Double + + [Fact] + public void IfDoubleLessThan_ThrowWhenLessThan() + { + var exception = Assert.Throws(() => Throw.IfLessThan(0.0, 1.0, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument less than minimum value 1", exception.Message); + } + + [Fact] + public void IfDoubleLessThan_DoesntThrow_WhenEqual() + { + var exception = Record.Exception(() => Throw.IfLessThan(0.0, 0.0, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void IfDoubleGreaterThan_ThrowWhenGreaterThan() + { + var exception = Assert.Throws(() => Throw.IfGreaterThan(1.4, 0.0, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument greater than maximum value 0", exception.Message); + } + + [Fact] + public void IfDoubleGreaterThan_DoesntThrow_WhenEqual() + { + var exception = Record.Exception(() => Throw.IfGreaterThan(0.0, 0.0, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void IfDoubleLessThanOrEqual_ThrowWhenEqual() + { + var exception = Assert.Throws(() => Throw.IfLessThanOrEqual(1.2, 1.2, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument less or equal than minimum value 1.2", exception.Message); + } + + [Fact] + public void IfDoubleLessThanOrEqual_DoesntThrow_WhenGreaterThan() + { + var exception = Record.Exception(() => Throw.IfLessThanOrEqual(1.5, 0.0, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void IfDoubleGreaterThanOrEqual_ThrowWhenEqual() + { + var exception = Assert.Throws(() => Throw.IfGreaterThanOrEqual(1.22, 1.22, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument greater or equal than maximum value 1.22", exception.Message); + } + + [Fact] + public void IfDoubleGreaterThanOrEqual_DoesntThrow_WhenLessThan() + { + var exception = Record.Exception(() => Throw.IfGreaterThanOrEqual(0.0, 1.3, "paramName")); + Assert.Null(exception); + } + + [Theory] + [InlineData(-0.0)] + [InlineData(-0)] + public void IfDoubleZero_ThrowWhenZero(double zero) + { + var exception = Assert.Throws(() => Throw.IfZero(zero, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument is zero", exception.Message); + } + + [Theory] + [InlineData(0.001)] + [InlineData(-0.010)] + [InlineData(1.1)] + public void IfDoubleZero_DoesntThrow_WhenNotZero(double notZero) + { + var exception = Record.Exception(() => Throw.IfZero(notZero, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void Double_OUtOfRange() + { + var exception = Assert.Throws(() => Throw.IfOutOfRange(-1.0, 0, 1, "foo")); + Assert.Equal("foo", exception.ParamName); + Assert.StartsWith("Argument not in the range", exception.Message); + + exception = Assert.Throws(() => Throw.IfOutOfRange(2.0, 0, 1, "foo")); + Assert.Equal("foo", exception.ParamName); + Assert.StartsWith("Argument not in the range", exception.Message); + + Assert.Equal(0, Throw.IfOutOfRange(0.0, 0, 1, "foo")); + Assert.Equal(1, Throw.IfOutOfRange(1.0, 0, 1, "foo")); + } + + [Fact] + public void Shorter_Version_Of_GreaterThan_For_Double_Get_Correct_Argument_Name() + { + const double Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfGreaterThan(Zero, -1)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfGreaterThan(Zero, -1, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_GreaterThanOrEqual_For_Double_Get_Correct_Argument_Name() + { + const double Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfGreaterThanOrEqual(Zero, -1)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfGreaterThanOrEqual(Zero, -1, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_LessThan_For_Double_Get_Correct_Argument_Name() + { + const double Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfLessThan(Zero, 1)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfLessThan(Zero, 1, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_LessThanOrEqual_For_Double_Get_Correct_Argument_Name() + { + const double Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfLessThanOrEqual(Zero, 1)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfLessThanOrEqual(Zero, 1, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_Zero_For_Double_Get_Correct_Argument_Name() + { + const double Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfZero(Zero)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfZero(Zero, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_OutOfRange_For_Double_Get_Correct_Argument_Name() + { + const double Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfOutOfRange(Zero, 1, 2)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfOutOfRange(Zero, 1, 2, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + #endregion +} diff --git a/test/Shared/Throw/IntegerTests.cs b/test/Shared/Throw/IntegerTests.cs new file mode 100644 index 0000000000..206d5b39e6 --- /dev/null +++ b/test/Shared/Throw/IntegerTests.cs @@ -0,0 +1,332 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.Shared.Diagnostics.Test; + +#pragma warning disable S3236 // Caller information arguments should not be provided explicitly + +public class IntegerTests +{ + #region For Integer + + [Fact] + public void IfIntLessThan_ThrowWhenLessThan() + { + var exception = Assert.Throws(() => Throw.IfLessThan(0, 1, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument less than minimum value 1", exception.Message); + } + + [Fact] + public void IfIntLessThan_DoesntThrow_WhenEqual() + { + var exception = Record.Exception(() => Throw.IfLessThan(0, 0, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void IfIntGreaterThan_ThrowWhenGreaterThan() + { + var exception = Assert.Throws(() => Throw.IfGreaterThan(1, 0, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument greater than maximum value 0", exception.Message); + } + + [Fact] + public void IfIntGreaterThan_DoesntThrow_WhenEqual() + { + var exception = Record.Exception(() => Throw.IfGreaterThan(0, 0, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void IfIntLessThanOrEqual_ThrowWhenEqual() + { + var exception = Assert.Throws(() => Throw.IfLessThanOrEqual(1, 1, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument less or equal than minimum value 1", exception.Message); + } + + [Fact] + public void IfIntLessThanOrEqual_DoesntThrow_WhenGreaterThan() + { + var exception = Record.Exception(() => Throw.IfLessThanOrEqual(1, 0, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void IfIntGreaterThanOrEqual_ThrowWhenEqual() + { + var exception = Assert.Throws(() => Throw.IfGreaterThanOrEqual(1, 1, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument greater or equal than maximum value 1", exception.Message); + } + + [Fact] + public void IfIntGreaterThanOrEqual_DoesntThrow_WhenLessThan() + { + var exception = Record.Exception(() => Throw.IfGreaterThanOrEqual(0, 1, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void IfIntZero_ThrowWhenZero() + { + var exception = Assert.Throws(() => Throw.IfZero(0, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument is zero", exception.Message); + } + + [Fact] + public void IfIntZero_DoesntThrow_WhenNotZero() + { + var exception = Record.Exception(() => Throw.IfZero(1, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void Int_OUtOfRange() + { + var exception = Assert.Throws(() => Throw.IfOutOfRange(-1, 0, 1, "foo")); + Assert.Equal("foo", exception.ParamName); + Assert.StartsWith("Argument not in the range", exception.Message); + + exception = Assert.Throws(() => Throw.IfOutOfRange(2, 0, 1, "foo")); + Assert.Equal("foo", exception.ParamName); + Assert.StartsWith("Argument not in the range", exception.Message); + + Assert.Equal(0, Throw.IfOutOfRange(0, 0, 1, "foo")); + Assert.Equal(1, Throw.IfOutOfRange(1, 0, 1, "foo")); + } + + [Fact] + public void Shorter_Version_Of_GreaterThan_For_Int_Get_Correct_Argument_Name() + { + const int Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfGreaterThan(Zero, -1)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfGreaterThan(Zero, -1, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_GreaterThanOrEqual_For_Int_Get_Correct_Argument_Name() + { + const int Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfGreaterThanOrEqual(Zero, -1)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfGreaterThanOrEqual(Zero, -1, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_LessThan_For_Int_Get_Correct_Argument_Name() + { + const int Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfLessThan(Zero, 1)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfLessThan(Zero, 1, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_LessThanOrEqual_For_Int_Get_Correct_Argument_Name() + { + const int Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfLessThanOrEqual(Zero, 1)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfLessThanOrEqual(Zero, 1, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_Zero_For_Int_Get_Correct_Argument_Name() + { + const int Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfZero(Zero)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfZero(Zero, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_OutOfRange_For_Int_Get_Correct_Argument_Name() + { + const int Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfOutOfRange(Zero, 1, 2)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfOutOfRange(Zero, 1, 2, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + #endregion + + #region For Unsigned Integer + + [Fact] + public void IfUIntLessThan_ThrowWhenLessThan() + { + var exception = Assert.Throws(() => Throw.IfLessThan(0U, 1U, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument less than minimum value 1", exception.Message); + } + + [Fact] + public void IfUIntLessThan_DoesntThrow_WhenEqual() + { + var exception = Record.Exception(() => Throw.IfLessThan(0U, 0U, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void IfUIntGreaterThan_ThrowWhenGreaterThan() + { + var exception = Assert.Throws(() => Throw.IfGreaterThan(1U, 0U, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument greater than maximum value 0", exception.Message); + } + + [Fact] + public void IfUIntGreaterThan_DoesntThrow_WhenEqual() + { + var exception = Record.Exception(() => Throw.IfGreaterThan(0U, 0U, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void IfUIntLessThanOrEqual_ThrowWhenEqual() + { + var exception = Assert.Throws(() => Throw.IfLessThanOrEqual(1U, 1U, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument less or equal than minimum value 1", exception.Message); + } + + [Fact] + public void IfUIntLessThanOrEqual_DoesntThrow_WhenGreaterThan() + { + var exception = Record.Exception(() => Throw.IfLessThanOrEqual(1U, 0U, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void IfUIntGreaterThanOrEqual_ThrowWhenEqual() + { + var exception = Assert.Throws(() => Throw.IfGreaterThanOrEqual(1U, 1U, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument greater or equal than maximum value 1", exception.Message); + } + + [Fact] + public void IfUIntGreaterThanOrEqual_DoesntThrow_WhenLessThan() + { + var exception = Record.Exception(() => Throw.IfGreaterThanOrEqual(0U, 1U, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void IfUIntZero_ThrowWhenZero() + { + var exception = Assert.Throws(() => Throw.IfZero(0U, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument is zero", exception.Message); + } + + [Fact] + public void IfUIntZero_DoesntThrow_WhenNotZero() + { + var exception = Record.Exception(() => Throw.IfZero(1U, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void UInt_OutOfRange() + { + var exception = Assert.Throws(() => Throw.IfOutOfRange(0U, 1, 2, "foo")); + Assert.Equal("foo", exception.ParamName); + Assert.StartsWith("Argument not in the range", exception.Message); + + exception = Assert.Throws(() => Throw.IfOutOfRange(2U, 0U, 1U, "foo")); + Assert.Equal("foo", exception.ParamName); + Assert.StartsWith("Argument not in the range", exception.Message); + + Assert.Equal(0U, Throw.IfOutOfRange(0U, 0, 1, "foo")); + Assert.Equal(1U, Throw.IfOutOfRange(1U, 0, 1, "foo")); + } + + [Fact] + public void Shorter_Version_Of_GreaterThan_For_UInt_Get_Correct_Argument_Name() + { + const uint One = 1; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfGreaterThan(One, 0U)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfGreaterThan(One, 0U, nameof(One))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_GreaterThanOrEqual_For_UInt_Get_Correct_Argument_Name() + { + const uint One = 1; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfGreaterThanOrEqual(One, 0U)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfGreaterThanOrEqual(One, 0U, nameof(One))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_LessThan_For_UInt_Get_Correct_Argument_Name() + { + const uint Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfLessThan(Zero, 1U)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfLessThan(Zero, 1U, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_LessThanOrEqual_For_UInt_Get_Correct_Argument_Name() + { + const uint Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfLessThanOrEqual(Zero, 1U)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfLessThanOrEqual(Zero, 1U, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_Zero_For_UInt_Get_Correct_Argument_Name() + { + const uint Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfZero(Zero)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfZero(Zero, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_OutOfRange_For_UInt_Get_Correct_Argument_Name() + { + const uint Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfOutOfRange(Zero, 1U, 2U)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfOutOfRange(Zero, 1U, 2U, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + #endregion +} diff --git a/test/Shared/Throw/LongTests.cs b/test/Shared/Throw/LongTests.cs new file mode 100644 index 0000000000..553dc0ba43 --- /dev/null +++ b/test/Shared/Throw/LongTests.cs @@ -0,0 +1,332 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace Microsoft.Shared.Diagnostics.Test; + +#pragma warning disable S3236 // Caller information arguments should not be provided explicitly + +public class LongTests +{ + #region For Long + + [Fact] + public void IfLongLessThan_ThrowWhenLessThan() + { + var exception = Assert.Throws(() => Throw.IfLessThan(0L, 1L, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument less than minimum value 1", exception.Message); + } + + [Fact] + public void IfLongLessThan_DoesntThrow_WhenEqual() + { + var exception = Record.Exception(() => Throw.IfLessThan(0L, 0L, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void IfLongGreaterThan_ThrowWhenGreaterThan() + { + var exception = Assert.Throws(() => Throw.IfGreaterThan(1L, 0L, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument greater than maximum value 0", exception.Message); + } + + [Fact] + public void IfLongGreaterThan_DoesntThrow_WhenEqual() + { + var exception = Record.Exception(() => Throw.IfGreaterThan(0L, 0L, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void IfLongLessThanOrEqual_ThrowWhenEqual() + { + var exception = Assert.Throws(() => Throw.IfLessThanOrEqual(1L, 1L, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument less or equal than minimum value 1", exception.Message); + } + + [Fact] + public void IfLongLessThanOrEqual_DoesntThrow_WhenGreaterThan() + { + var exception = Record.Exception(() => Throw.IfLessThanOrEqual(1L, 0L, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void IfLongGreaterThanOrEqual_ThrowWhenEqual() + { + var exception = Assert.Throws(() => Throw.IfGreaterThanOrEqual(1L, 1L, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument greater or equal than maximum value 1", exception.Message); + } + + [Fact] + public void IfLongGreaterThanOrEqual_DoesntThrow_WhenLessThan() + { + var exception = Record.Exception(() => Throw.IfGreaterThanOrEqual(0L, 1L, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void IfLongZero_ThrowWhenZero() + { + var exception = Assert.Throws(() => Throw.IfZero(0L, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument is zero", exception.Message); + } + + [Fact] + public void IfLongZero_DoesntThrow_WhenNotZero() + { + var exception = Record.Exception(() => Throw.IfZero(1L, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void Long_OUtOfRange() + { + var exception = Assert.Throws(() => Throw.IfOutOfRange(-1L, 0, 1, "foo")); + Assert.Equal("foo", exception.ParamName); + Assert.StartsWith("Argument not in the range", exception.Message); + + exception = Assert.Throws(() => Throw.IfOutOfRange(2L, 0, 1, "foo")); + Assert.Equal("foo", exception.ParamName); + Assert.StartsWith("Argument not in the range", exception.Message); + + Assert.Equal(0, Throw.IfOutOfRange(0L, 0, 1, "foo")); + Assert.Equal(1, Throw.IfOutOfRange(1L, 0, 1, "foo")); + } + + [Fact] + public void Shorter_Version_Of_GreaterThan_For_Long_Get_Correct_Argument_Name() + { + const long Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfGreaterThan(Zero, -1)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfGreaterThan(Zero, -1, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_GreaterThanOrEqual_For_Long_Get_Correct_Argument_Name() + { + const long Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfGreaterThanOrEqual(Zero, -1)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfGreaterThanOrEqual(Zero, -1, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_LessThan_For_Long_Get_Correct_Argument_Name() + { + const long Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfLessThan(Zero, 1)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfLessThan(Zero, 1, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_LessThanOrEqual_For_Long_Get_Correct_Argument_Name() + { + const long Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfLessThanOrEqual(Zero, 1)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfLessThanOrEqual(Zero, 1, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_Zero_For_Long_Get_Correct_Argument_Name() + { + const long Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfZero(Zero)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfZero(Zero, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_OutOfRange_For_Long_Get_Correct_Argument_Name() + { + const long Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfOutOfRange(Zero, 1, 2)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfOutOfRange(Zero, 1, 2, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + #endregion + + #region For Unsigned Long + + [Fact] + public void IfULongLessThan_ThrowWhenLessThan() + { + var exception = Assert.Throws(() => Throw.IfLessThan(0UL, 1UL, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument less than minimum value 1", exception.Message); + } + + [Fact] + public void IfULongLessThan_DoesntThrow_WhenEqual() + { + var exception = Record.Exception(() => Throw.IfLessThan(0UL, 0UL, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void IfULongGreaterThan_ThrowWhenGreaterThan() + { + var exception = Assert.Throws(() => Throw.IfGreaterThan(1UL, 0UL, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument greater than maximum value 0", exception.Message); + } + + [Fact] + public void IfULongGreaterThan_DoesntThrow_WhenEqual() + { + var exception = Record.Exception(() => Throw.IfGreaterThan(0UL, 0UL, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void IfULongLessThanOrEqual_ThrowWhenEqual() + { + var exception = Assert.Throws(() => Throw.IfLessThanOrEqual(1UL, 1UL, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument less or equal than minimum value 1", exception.Message); + } + + [Fact] + public void IfULongLessThanOrEqual_DoesntThrow_WhenGreaterThan() + { + var exception = Record.Exception(() => Throw.IfLessThanOrEqual(1UL, 0UL, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void IfULongGreaterThanOrEqual_ThrowWhenEqual() + { + var exception = Assert.Throws(() => Throw.IfGreaterThanOrEqual(1UL, 1UL, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument greater or equal than maximum value 1", exception.Message); + } + + [Fact] + public void IfULongGreaterThanOrEqual_DoesntThrow_WhenLessThan() + { + var exception = Record.Exception(() => Throw.IfGreaterThanOrEqual(0UL, 1UL, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void IfULongZero_ThrowWhenZero() + { + var exception = Assert.Throws(() => Throw.IfZero(0UL, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument is zero", exception.Message); + } + + [Fact] + public void IfULongZero_DoesntThrow_WhenNotZero() + { + var exception = Record.Exception(() => Throw.IfZero(1UL, "paramName")); + Assert.Null(exception); + } + + [Fact] + public void ULong_OutOfRange() + { + var exception = Assert.Throws(() => Throw.IfOutOfRange(0UL, 1UL, 2UL, "foo")); + Assert.Equal("foo", exception.ParamName); + Assert.StartsWith("Argument not in the range", exception.Message); + + exception = Assert.Throws(() => Throw.IfOutOfRange(2L, 0UL, 1UL, "foo")); + Assert.Equal("foo", exception.ParamName); + Assert.StartsWith("Argument not in the range", exception.Message); + + Assert.Equal(0UL, Throw.IfOutOfRange(0UL, 0, 1, "foo")); + Assert.Equal(1UL, Throw.IfOutOfRange(1UL, 0, 1, "foo")); + } + + [Fact] + public void Shorter_Version_Of_GreaterThan_For_ULong_Get_Correct_Argument_Name() + { + const ulong One = 1; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfGreaterThan(One, 0UL)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfGreaterThan(One, 0UL, nameof(One))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_GreaterThanOrEqual_For_ULong_Get_Correct_Argument_Name() + { + const ulong One = 1; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfGreaterThanOrEqual(One, 0UL)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfGreaterThanOrEqual(One, 0UL, nameof(One))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_LessThan_For_ULong_Get_Correct_Argument_Name() + { + const ulong Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfLessThan(Zero, 1UL)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfLessThan(Zero, 1UL, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_LessThanOrEqual_For_ULong_Get_Correct_Argument_Name() + { + const ulong Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfLessThanOrEqual(Zero, 1UL)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfLessThanOrEqual(Zero, 1UL, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_Zero_For_ULong_Get_Correct_Argument_Name() + { + const ulong Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfZero(Zero)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfZero(Zero, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_OutOfRange_For_ULong_Get_Correct_Argument_Name() + { + const ulong Zero = 0; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfOutOfRange(Zero, 1UL, 2UL)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfOutOfRange(Zero, 1UL, 2UL, nameof(Zero))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + #endregion +} diff --git a/test/Shared/Throw/ThrowTest.cs b/test/Shared/Throw/ThrowTest.cs new file mode 100644 index 0000000000..691217d86c --- /dev/null +++ b/test/Shared/Throw/ThrowTest.cs @@ -0,0 +1,424 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Shared.Diagnostics.Test; + +#pragma warning disable S3236 // Caller information arguments should not be provided explicitly + +public class ThrowTest +{ + #region Exceptions + + [Fact] + public void ThrowInvalidOperationException_ThrowsException_WithMessage() + { + var message = "message"; + var exception = Assert.Throws(() => Throw.InvalidOperationException(message)); + Assert.Contains(message, exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void ThrowInvalidOperationException_ThrowsException_WithMessageAndInnerException() + { + var message = "message"; +#pragma warning disable CA2201 // Do not raise reserved exception types + var innerException = new Exception(); +#pragma warning restore CA2201 // Do not raise reserved exception types + var exception = Assert.Throws(() => Throw.InvalidOperationException(message, innerException)); + Assert.Contains(message, exception.Message, StringComparison.Ordinal); + Assert.Equal(innerException, exception.InnerException); + } + + [Fact] + public void ThrowArgumentException_ThrowsException_WithMessageAndParamName() + { + var message = "message"; + var paramName = "paramName"; + var exception = Assert.Throws(() => Throw.ArgumentException(paramName, message)); + Assert.Contains(message, exception.Message, StringComparison.Ordinal); + Assert.Equal(paramName, exception.ParamName); + } + + [Fact] + public void ThrowArgumentException_ThrowsException_WithMessageAndParamNameAndInnerException() + { + var message = "message"; + var paramName = "paramName"; +#pragma warning disable CA2201 // Do not raise reserved exception types + var innerException = new Exception(); +#pragma warning restore CA2201 // Do not raise reserved exception types + var exception = Assert.Throws(() => Throw.ArgumentException(paramName, message, innerException)); + Assert.Contains(message, exception.Message, StringComparison.Ordinal); + Assert.Equal(paramName, exception.ParamName); + Assert.Equal(innerException, exception.InnerException); + } + + [Fact] + public void ThrowArgumentNullException_ThrowsException_WithMessage() + { + var paramName = "paramName"; + var exception = Assert.Throws(() => Throw.ArgumentNullException(paramName)); + Assert.Equal(paramName, exception.ParamName); + } + + [Fact] + public void ThrowArgumentNullException_ThrowsException_WithMessageAndParamName() + { + var paramName = "paramName"; + var message = "message"; + var exception = Assert.Throws(() => Throw.ArgumentNullException(paramName, message)); + Assert.Contains(message, exception.Message, StringComparison.Ordinal); + Assert.Equal(paramName, exception.ParamName); + } + + [Fact] + public void ThrowArgumentOutOfRangeException_ThrowsException_WithParamName() + { + var paramName = "paramName"; + var exception = Assert.Throws(() => Throw.ArgumentOutOfRangeException(paramName)); + Assert.Equal(paramName, exception.ParamName); + } + + [Fact] + public void ThrowArgumentOutOfRangeException_ThrowsException_WithMessageAndParamName() + { + var paramName = "paramName"; + var message = "message"; + var exception = Assert.Throws(() => Throw.ArgumentOutOfRangeException(paramName, message)); + Assert.Contains(message, exception.Message, StringComparison.Ordinal); + Assert.Equal(paramName, exception.ParamName); + } + + [Fact] + public void ThrowArgumentOutOfRangeException_ThrowsException_WithMessageAndActualValue() + { + var paramName = "paramName"; + var message = "message"; + var actualValue = 10; + + var exception = Assert.Throws(() => Throw.ArgumentOutOfRangeException(paramName, actualValue, message)); + Assert.Contains(message, exception.Message, StringComparison.Ordinal); + Assert.Equal(paramName, exception.ParamName); + Assert.Equal(actualValue, exception.ActualValue); + } + + #endregion + + #region For Object + + [Fact] + public void ThrowIfNull_ThrowsWhenNullValue_ReferenceType() + { + var exception = Assert.Throws(() => Throw.IfNull(null!, "paramName")); + Assert.Equal("paramName", exception.ParamName); + } + + [Fact] + public void ThrowIfNull_ThrowsWhenNullValue_NullableValueType() + { + var exception = Assert.Throws(() => Throw.IfNull(null!, "paramName")); + Assert.Equal("paramName", exception.ParamName); + } + + [Fact] + public void ThrowIfNull_DoesntThrow_WhenNotNullReferenceType() + { + var value = string.Empty; + Assert.Equal(value, Throw.IfNull(value, nameof(value))); + } + + [Fact] + public void ThrowIfNull_DoesntThrow_WhenNullableValueType() + { + int? value = 0; + Assert.Equal(value, Throw.IfNull(value, nameof(value))); + } + + [Fact] + public void Shorter_Version_Of_Throws_Get_Correct_Argument_Name() + { + Random? somethingThatIsNull = null; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfNull(somethingThatIsNull)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfNull(somethingThatIsNull, nameof(somethingThatIsNull))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_Throws_Get_Correct_Argument_Name_For_Object_Checks() + { + Random? somethingThatIsNull = null; + Random? somethingNestedThatIsNull = null; + object somethingThatIsNotNull = new(); + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfNullOrMemberNull(somethingThatIsNull, somethingNestedThatIsNull)); + var exceptionExplicitArgumentName = Record.Exception( + () => Throw.IfNullOrMemberNull(somethingThatIsNull, somethingNestedThatIsNull, nameof(somethingThatIsNull), nameof(somethingNestedThatIsNull))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_Throws_Get_Correct_Argument_Name_For_Member_Checks() + { + Color red = Color.Red; + Random? somethingNestedThatIsNull = null; + object somethingThatIsNotNull = new(); + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfNullOrMemberNull(red, somethingNestedThatIsNull)); + var exceptionExplicitArgumentName = Record.Exception( + () => Throw.IfNullOrMemberNull(red, somethingNestedThatIsNull, nameof(red), nameof(somethingNestedThatIsNull))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + + var expectedMessage = $"Member {nameof(somethingNestedThatIsNull)} of {nameof(red)} is null"; +#if NETCOREAPP3_1_OR_GREATER + expectedMessage += $" (Parameter '{nameof(red)}')"; +#else + expectedMessage += $"\r\nParameter name: {nameof(red)}"; +#endif + Assert.Equal(expectedMessage, exceptionImplicitArgumentName.Message); + + exceptionImplicitArgumentName = Record.Exception(() => Throw.IfMemberNull(somethingThatIsNotNull, somethingNestedThatIsNull)); + exceptionExplicitArgumentName = Record.Exception( + () => Throw.IfMemberNull(somethingThatIsNotNull, somethingNestedThatIsNull, nameof(somethingThatIsNotNull), nameof(somethingNestedThatIsNull))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + + expectedMessage = $"Member {nameof(somethingNestedThatIsNull)} of {nameof(somethingThatIsNotNull)} is null"; +#if NETCOREAPP3_1_OR_GREATER + expectedMessage += $" (Parameter '{nameof(somethingThatIsNotNull)}')"; +#else + expectedMessage += $"\r\nParameter name: {nameof(somethingThatIsNotNull)}"; +#endif + Assert.Equal(expectedMessage, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_IfNull_Does_Not_Throw_When_Member_Is_Not_Null() + { + Color red = Color.Red; + Color blue = Color.Blue; + + var exception = Record.Exception(() => Throw.IfNullOrMemberNull(red, blue)); + Assert.Null(exception); + + var resultOfThrows = Throw.IfNullOrMemberNull(red, blue); + Assert.Equal(resultOfThrows, blue); + + resultOfThrows = Throw.IfMemberNull(red, blue); + Assert.Equal(resultOfThrows, blue); + } + + #endregion + + #region For String + [Fact] + public void ThrowIfNullOrWhitespace_ThrowsWhenNull() + { + var exception = Assert.Throws(() => Throw.IfNullOrWhitespace(null!, "paramName")); + Assert.Equal("paramName", exception.ParamName); + } + + [Fact] + public void ThrowIfNullOrWhitespace_ThrowsWhenWhitespace() + { + var exception = Assert.Throws(() => Throw.IfNullOrWhitespace(" ", "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument is whitespace", exception.Message); + } + + [Fact] + public void ThrowIfNullOrWhitespace_DoesntThrow_WhenNotNullOrWhitespace() + { + var exception = Record.Exception(() => Throw.IfNullOrWhitespace("param", "paramName")); + Assert.Null(exception); + } + + [Fact] + public void ThrowIfNullOrEmpty_ThrowsWhenNull() + { + var exception = Assert.Throws(() => Throw.IfNullOrEmpty(null, "paramName")); + Assert.Equal("paramName", exception.ParamName); + } + + [Fact] + public void ThrowIfNullOrEmpty_ThrowsWhenEmpty() + { + var exception = Assert.Throws(() => Throw.IfNullOrEmpty("", "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Argument is an empty string", exception.Message); + } + + [Fact] + public void ThrowIfNullOrWhitespace_DoesntThrow_WhenNotNullOrEmpty() + { + var exception = Record.Exception(() => Throw.IfNullOrEmpty("param", "paramName")); + Assert.Null(exception); + } + + [Fact] + public void Shorter_Version_Of_ThrowIfNullOrWhitespace_Get_Correct_Argument_Name() + { + string? somethingThatIsNull = null; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfNullOrWhitespace(somethingThatIsNull)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfNullOrWhitespace(somethingThatIsNull, nameof(somethingThatIsNull))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + [Fact] + public void Shorter_Version_Of_ThrowIfNullOrEmpty_Get_Correct_Argument_Name() + { + string? somethingThatIsNull = null; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfNullOrEmpty(somethingThatIsNull)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfNullOrEmpty(somethingThatIsNull, nameof(somethingThatIsNull))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + #endregion + + #region For Buffer + + [Fact] + public void IfBufferTooSmall_ThrowsWhenBufferTooSmall() + { + var exception = Assert.Throws(() => Throw.IfBufferTooSmall(23, 24, "paramName")); + Assert.Equal("paramName", exception.ParamName); + Assert.StartsWith("Buffer", exception.Message); + } + + [Fact] + public void IfBufferTooSmall_DoesntThrow_WhenBufferIsLargeEnough() + { + var exception = Record.Exception(() => Throw.IfBufferTooSmall(23, 23, "paramName")); + Assert.Null(exception); + } + #endregion + + #region For Collections + + private static IEnumerable GetEmptyEnumerable() + { + yield break; + } + + private static IEnumerable GetNonemptyEnumerable() + { + yield return 1; + } + + [Fact] + public void Collection_IfNullOrEmpty() + { + ArgumentException exception = Assert.Throws(() => Throw.IfNullOrEmpty((ICollection?)null, "foo")); + Assert.Equal("foo", exception.ParamName); + + exception = Assert.Throws(() => Throw.IfNullOrEmpty((IReadOnlyCollection?)null, "foo")); + Assert.Equal("foo", exception.ParamName); + + exception = Assert.Throws(() => Throw.IfNullOrEmpty((List?)null, "foo")); + Assert.Equal("foo", exception.ParamName); + + exception = Assert.Throws(() => Throw.IfNullOrEmpty((Queue?)null, "foo")); + Assert.Equal("foo", exception.ParamName); + + exception = Assert.Throws(() => Throw.IfNullOrEmpty((IEnumerable?)null, "foo")); + Assert.Equal("foo", exception.ParamName); + + var list = new List(); + + exception = Assert.Throws(() => Throw.IfNullOrEmpty((ICollection?)list, "foo")); + Assert.Equal("foo", exception.ParamName); + Assert.StartsWith("Collection is empty", exception.Message); + + exception = Assert.Throws(() => Throw.IfNullOrEmpty((IReadOnlyCollection?)list, "foo")); + Assert.Equal("foo", exception.ParamName); + Assert.StartsWith("Collection is empty", exception.Message); + + exception = Assert.Throws(() => Throw.IfNullOrEmpty(list, "foo")); + Assert.Equal("foo", exception.ParamName); + Assert.StartsWith("Collection is empty", exception.Message); + + var queue = new Queue(); + + exception = Assert.Throws(() => Throw.IfNullOrEmpty(queue, "foo")); + Assert.Equal("foo", exception.ParamName); + Assert.StartsWith("Collection is empty", exception.Message); + + var enumerable = GetEmptyEnumerable(); + + exception = Assert.Throws(() => Throw.IfNullOrEmpty(enumerable, "foo")); + Assert.Equal("foo", exception.ParamName); + Assert.StartsWith("Collection is empty", exception.Message); + + list.Add(42); + Assert.Equal(list, Throw.IfNullOrEmpty((ICollection?)list, "foo")); + Assert.Equal(list, Throw.IfNullOrEmpty((IReadOnlyCollection?)list, "foo")); + Assert.Equal(list, Throw.IfNullOrEmpty(list, "foo")); + + queue.Enqueue(42); + Assert.Equal(queue, Throw.IfNullOrEmpty(queue, "foo")); + + enumerable = GetNonemptyEnumerable(); + Assert.Equal(enumerable, Throw.IfNullOrEmpty(enumerable, "foo")); + } + + [Fact] + public void Shorter_Version_Of_NullOrEmpty_Get_Correct_Argument_Name() + { + List? listButActuallyNull = null; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfNullOrEmpty(listButActuallyNull!)); + + Assert.Contains(nameof(listButActuallyNull), exceptionImplicitArgumentName.Message); + } + + #endregion + + #region For Enums + + internal enum Color + { + Red, + Green, + Blue, + } + + [Fact] + public void Enum_OutOfRange() + { + Assert.Equal(Color.Red, Throw.IfOutOfRange(Color.Red, "foo")); + Assert.Equal(Color.Green, Throw.IfOutOfRange(Color.Green, "foo")); + Assert.Equal(Color.Blue, Throw.IfOutOfRange(Color.Blue, "foo")); + + var exception = Assert.Throws(() => Throw.IfOutOfRange((Color)(-1), "foo")); + Assert.Equal("foo", exception.ParamName); + Assert.Contains("is an invalid value for enum type", exception.Message); + + exception = Assert.Throws(() => Throw.IfOutOfRange((Color)3, "foo")); + Assert.Equal("foo", exception.ParamName); + Assert.Contains("is an invalid value for enum type", exception.Message); + } + + [Fact] + public void Shorter_Version_Of_OutOfRange_Get_Correct_Argument_Name() + { + Color? colorButNull = null; + + var exceptionImplicitArgumentName = Record.Exception(() => Throw.IfOutOfRange((Color)colorButNull!)); + var exceptionExplicitArgumentName = Record.Exception(() => Throw.IfOutOfRange((Color)colorButNull!, nameof(colorButNull))); + + Assert.Equal(exceptionExplicitArgumentName.Message, exceptionImplicitArgumentName.Message); + } + + #endregion +} diff --git a/test/TestUtilities/TestUtilities.csproj b/test/TestUtilities/TestUtilities.csproj new file mode 100644 index 0000000000..c46cf509ed --- /dev/null +++ b/test/TestUtilities/TestUtilities.csproj @@ -0,0 +1,22 @@ + + + + Microsoft.TestUtilities + Microsoft.TestUtilities + $(NetCoreTargetFrameworks)$(ConditionalNet462) + + + + + + + + + diff --git a/test/TestUtilities/XUnit/ConditionalFactAttribute.cs b/test/TestUtilities/XUnit/ConditionalFactAttribute.cs new file mode 100644 index 0000000000..92077e27db --- /dev/null +++ b/test/TestUtilities/XUnit/ConditionalFactAttribute.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Borrowed from https://github.com/dotnet/aspnetcore/blob/95ed45c67/src/Testing/src/xunit/ + +using System; +using Xunit; +using Xunit.Sdk; + +namespace Microsoft.TestUtilities; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +[XunitTestCaseDiscoverer("Microsoft.TestUtilities." + nameof(ConditionalFactDiscoverer), "Microsoft.TestUtilities")] +public class ConditionalFactAttribute : FactAttribute +{ +} diff --git a/test/TestUtilities/XUnit/ConditionalFactDiscoverer.cs b/test/TestUtilities/XUnit/ConditionalFactDiscoverer.cs new file mode 100644 index 0000000000..a27876703e --- /dev/null +++ b/test/TestUtilities/XUnit/ConditionalFactDiscoverer.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Borrowed from https://github.com/dotnet/aspnetcore/blob/95ed45c67/src/Testing/src/xunit/ + +using Xunit.Abstractions; +using Xunit.Sdk; + +// Do not change this namespace without changing the usage in ConditionalFactAttribute +namespace Microsoft.TestUtilities; + +internal sealed class ConditionalFactDiscoverer : FactDiscoverer +{ + private readonly IMessageSink _diagnosticMessageSink; + + public ConditionalFactDiscoverer(IMessageSink diagnosticMessageSink) + : base(diagnosticMessageSink) + { + _diagnosticMessageSink = diagnosticMessageSink; + } + + protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) + { + var skipReason = testMethod.EvaluateSkipConditions(); + return skipReason != null + ? new SkippedTestCase(skipReason, _diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), TestMethodDisplayOptions.None, testMethod) + : base.CreateTestCase(discoveryOptions, testMethod, factAttribute); + } +} diff --git a/test/TestUtilities/XUnit/ConditionalTheoryAttribute.cs b/test/TestUtilities/XUnit/ConditionalTheoryAttribute.cs new file mode 100644 index 0000000000..d5f23068dd --- /dev/null +++ b/test/TestUtilities/XUnit/ConditionalTheoryAttribute.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Borrowed from https://github.com/dotnet/aspnetcore/blob/95ed45c67/src/Testing/src/xunit/ + +using System; +using Xunit; +using Xunit.Sdk; + +namespace Microsoft.TestUtilities; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +[XunitTestCaseDiscoverer("Microsoft.TestUtilities." + nameof(ConditionalTheoryDiscoverer), "Microsoft.TestUtilities")] +public class ConditionalTheoryAttribute : TheoryAttribute +{ +} diff --git a/test/TestUtilities/XUnit/ConditionalTheoryDiscoverer.cs b/test/TestUtilities/XUnit/ConditionalTheoryDiscoverer.cs new file mode 100644 index 0000000000..3142df749a --- /dev/null +++ b/test/TestUtilities/XUnit/ConditionalTheoryDiscoverer.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Borrowed from https://github.com/dotnet/aspnetcore/blob/95ed45c67/src/Testing/src/xunit/ + +using System; +using System.Collections.Generic; +using Xunit.Abstractions; +using Xunit.Sdk; + +// Do not change this namespace without changing the usage in ConditionalTheoryAttribute +namespace Microsoft.TestUtilities; + +internal sealed class ConditionalTheoryDiscoverer : TheoryDiscoverer +{ + public ConditionalTheoryDiscoverer(IMessageSink diagnosticMessageSink) + : base(diagnosticMessageSink) + { + } + + private sealed class OptionsWithPreEnumerationEnabled : ITestFrameworkDiscoveryOptions + { + private const string PreEnumerateTheories = "xunit.discovery.PreEnumerateTheories"; + + private readonly ITestFrameworkDiscoveryOptions _original; + + public OptionsWithPreEnumerationEnabled(ITestFrameworkDiscoveryOptions original) + { + _original = original; + } + + public TValue GetValue(string name) + => (name == PreEnumerateTheories) ? (TValue)(object)true : _original.GetValue(name); + + public void SetValue(string name, TValue value) + => _original.SetValue(name, value); + } + + public override IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute) + => base.Discover(new OptionsWithPreEnumerationEnabled(discoveryOptions), testMethod, theoryAttribute); + + protected override IEnumerable CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute) + { + var skipReason = testMethod.EvaluateSkipConditions(); + return skipReason != null + ? new[] { new SkippedTestCase(skipReason, DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), TestMethodDisplayOptions.None, testMethod) } + : base.CreateTestCasesForTheory(discoveryOptions, testMethod, theoryAttribute); + } + + protected override IEnumerable CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow) + { + var skipReason = testMethod.EvaluateSkipConditions(); + if (skipReason == null && dataRow?.Length > 0) + { + var obj = dataRow[0]; + if (obj != null) + { + var type = obj.GetType(); + var property = type.GetProperty("Skip"); + if (property != null && property.PropertyType.Equals(typeof(string))) + { + skipReason = property.GetValue(obj) as string; + } + } + } + + return skipReason != null ? + base.CreateTestCasesForSkippedDataRow(discoveryOptions, testMethod, theoryAttribute, dataRow, skipReason) + : base.CreateTestCasesForDataRow(discoveryOptions, testMethod, theoryAttribute, dataRow); + } + + protected override IEnumerable CreateTestCasesForSkippedDataRow( + ITestFrameworkDiscoveryOptions discoveryOptions, + ITestMethod testMethod, + IAttributeInfo theoryAttribute, + object[] dataRow, + string skipReason) + { + return new[] + { + new WORKAROUND_SkippedDataRowTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, skipReason, dataRow), + }; + } +} diff --git a/test/TestUtilities/XUnit/ITestCondition.cs b/test/TestUtilities/XUnit/ITestCondition.cs new file mode 100644 index 0000000000..347f3c6900 --- /dev/null +++ b/test/TestUtilities/XUnit/ITestCondition.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Borrowed from https://github.com/dotnet/aspnetcore/blob/95ed45c67/src/Testing/src/xunit/ + +namespace Microsoft.TestUtilities; + +public interface ITestCondition +{ + bool IsMet { get; } + + string SkipReason { get; } +} diff --git a/test/TestUtilities/XUnit/OSSkipConditionAttribute.cs b/test/TestUtilities/XUnit/OSSkipConditionAttribute.cs new file mode 100644 index 0000000000..143cd7005a --- /dev/null +++ b/test/TestUtilities/XUnit/OSSkipConditionAttribute.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Borrowed from https://github.com/dotnet/aspnetcore/blob/95ed45c67/src/Testing/src/xunit/ + +using System; +#if NETCOREAPP || NET471_OR_GREATER +using System.Runtime.InteropServices; +#endif + +namespace Microsoft.TestUtilities; + +#pragma warning disable CA1019 // Define accessors for attribute arguments +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = true)] +public class OSSkipConditionAttribute : Attribute, ITestCondition +{ + private readonly OperatingSystems _excludedOperatingSystem; + private readonly OperatingSystems _osPlatform; + + public OSSkipConditionAttribute(OperatingSystems operatingSystem) + : this(operatingSystem, GetCurrentOS()) + { + } + + // to enable unit testing + internal OSSkipConditionAttribute(OperatingSystems operatingSystem, OperatingSystems osPlatform) + { + _excludedOperatingSystem = operatingSystem; + _osPlatform = osPlatform; + } + + public bool IsMet + { + get + { + var skip = (_excludedOperatingSystem & _osPlatform) == _osPlatform; + + // Since a test would be executed only if 'IsMet' is true, return false if we want to skip + return !skip; + } + } + + public string SkipReason { get; set; } = "Test cannot run on this operating system."; + + private static OperatingSystems GetCurrentOS() + { +#if NETCOREAPP || NET471_OR_GREATER + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return OperatingSystems.Windows; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return OperatingSystems.Linux; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return OperatingSystems.MacOSX; + } + + throw new PlatformNotSupportedException(); +#else + // RuntimeInformation API is only avaialble in .NET Framework 4.7.1+ + // .NET Framework 4.7 and below can only run on Windows. + return OperatingSystems.Windows; +#endif + } +} +#pragma warning restore CA1019 // Define accessors for attribute arguments diff --git a/test/TestUtilities/XUnit/OperatingSystems.cs b/test/TestUtilities/XUnit/OperatingSystems.cs new file mode 100644 index 0000000000..3bee3bac96 --- /dev/null +++ b/test/TestUtilities/XUnit/OperatingSystems.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Borrowed from https://github.com/dotnet/aspnetcore/blob/95ed45c67/src/Testing/src/xunit/ + +using System; + +namespace Microsoft.TestUtilities; + +[Flags] +public enum OperatingSystems +{ + Linux = 1, + MacOSX = 2, + Windows = 4, +} diff --git a/test/TestUtilities/XUnit/SkippedTestCase.cs b/test/TestUtilities/XUnit/SkippedTestCase.cs new file mode 100644 index 0000000000..7b59125ffb --- /dev/null +++ b/test/TestUtilities/XUnit/SkippedTestCase.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Borrowed from https://github.com/dotnet/aspnetcore/blob/95ed45c67/src/Testing/src/xunit/ + +#nullable disable + +using System; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.TestUtilities; + +public class SkippedTestCase : XunitTestCase +{ + private string _skipReason; + + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] + public SkippedTestCase() + { + } + + public SkippedTestCase( + string skipReason, + IMessageSink diagnosticMessageSink, + TestMethodDisplay defaultMethodDisplay, + TestMethodDisplayOptions defaultMethodDisplayOptions, + ITestMethod testMethod, + object[] testMethodArguments = null) + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) + { + _skipReason = skipReason; + } + + protected override string GetSkipReason(IAttributeInfo factAttribute) + => _skipReason ?? base.GetSkipReason(factAttribute); + + public override void Deserialize(IXunitSerializationInfo data) + { + _skipReason = data.GetValue(nameof(_skipReason)); + + // We need to call base after reading our value, because Deserialize will call + // into GetSkipReason. + base.Deserialize(data); + } + + public override void Serialize(IXunitSerializationInfo data) + { + base.Serialize(data); + data.AddValue(nameof(_skipReason), _skipReason); + } +} diff --git a/test/TestUtilities/XUnit/TestMethodExtensions.cs b/test/TestUtilities/XUnit/TestMethodExtensions.cs new file mode 100644 index 0000000000..88356330da --- /dev/null +++ b/test/TestUtilities/XUnit/TestMethodExtensions.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Borrowed from https://github.com/dotnet/aspnetcore/blob/95ed45c67/src/Testing/src/xunit/ + +using System.Linq; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.TestUtilities; + +public static class TestMethodExtensions +{ + public static string? EvaluateSkipConditions(this ITestMethod testMethod) + { + var testClass = testMethod.TestClass.Class; + var assembly = testMethod.TestClass.TestCollection.TestAssembly.Assembly; + var conditionAttributes = testMethod.Method + .GetCustomAttributes(typeof(ITestCondition)) + .Concat(testClass.GetCustomAttributes(typeof(ITestCondition))) + .Concat(assembly.GetCustomAttributes(typeof(ITestCondition))) + .OfType() + .Select(attributeInfo => attributeInfo.Attribute); + + foreach (ITestCondition condition in conditionAttributes.OfType()) + { + if (!condition.IsMet) + { + return condition.SkipReason; + } + } + + return null; + } +} diff --git a/test/TestUtilities/XUnit/WORKAROUND_SkippedDataRowTestCase.cs b/test/TestUtilities/XUnit/WORKAROUND_SkippedDataRowTestCase.cs new file mode 100644 index 0000000000..123dba2fa4 --- /dev/null +++ b/test/TestUtilities/XUnit/WORKAROUND_SkippedDataRowTestCase.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Borrowed from https://github.com/dotnet/aspnetcore/blob/95ed45c67/src/Testing/src/xunit/ + +using System; +using System.ComponentModel; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.TestUtilities; + +// This is a workaround for https://github.com/xunit/xunit/issues/1782 - as such, this code is a copy-paste +// from xUnit with the exception of fixing the bug. +// +// This will only work with [ConditionalTheory]. +internal sealed class WORKAROUND_SkippedDataRowTestCase : XunitTestCase +{ + private string? _skipReason; + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] + public WORKAROUND_SkippedDataRowTestCase() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message sink used to send diagnostic messages. + /// Default method display to use (when not customized). + /// The test method this test case belongs to. + /// The reason that this test case will be skipped. + /// The arguments for the test method. + [Obsolete("Please call the constructor which takes TestMethodDisplayOptions")] + public WORKAROUND_SkippedDataRowTestCase(IMessageSink diagnosticMessageSink, + TestMethodDisplay defaultMethodDisplay, + ITestMethod testMethod, + string skipReason, + object[]? testMethodArguments = null) + : this(diagnosticMessageSink, defaultMethodDisplay, TestMethodDisplayOptions.None, testMethod, skipReason, testMethodArguments) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message sink used to send diagnostic messages. + /// Default method display to use (when not customized). + /// Default method display options to use (when not customized). + /// The test method this test case belongs to. + /// The reason that this test case will be skipped. + /// The arguments for the test method. + public WORKAROUND_SkippedDataRowTestCase(IMessageSink diagnosticMessageSink, + TestMethodDisplay defaultMethodDisplay, + TestMethodDisplayOptions defaultMethodDisplayOptions, + ITestMethod testMethod, + string skipReason, + object[]? testMethodArguments = null) + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) + { + _skipReason = skipReason; + } + + /// + public override void Deserialize(IXunitSerializationInfo data) + { + // SkipReason has to be read before we call base.Deserialize, this is the workaround. + _skipReason = data.GetValue("SkipReason"); + + base.Deserialize(data); + } + + /// + protected override string? GetSkipReason(IAttributeInfo factAttribute) + { + return _skipReason; + } + + /// + public override void Serialize(IXunitSerializationInfo data) + { + base.Serialize(data); + + data.AddValue("SkipReason", _skipReason); + } +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/AcceptanceTest.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/AcceptanceTest.cs new file mode 100644 index 0000000000..150a26e676 --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/AcceptanceTest.cs @@ -0,0 +1,380 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection.Test.Fakes; +using Microsoft.Extensions.DependencyInjection.Test.Helpers; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Xunit; + +namespace Microsoft.Extensions.DependencyInjection.Test; + +public class AcceptanceTest +{ + [Fact] + public async Task CanAddAndActivateSingletonAsync() + { + var instanceCount = new InstanceCreatingCounter(); + Assert.Equal(0, instanceCount.Counter); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(instanceCount) + .AddActivatedSingleton()) + .StartAsync(); + + var service = host.Services.GetService(); + await host.StopAsync(); + + Assert.NotNull(service); + Assert.Equal(1, instanceCount.Counter); + } + + [Fact] + public async Task SouldIgnoreComponent_WhenNoAutoStartAsync() + { + var instanceCount = new InstanceCreatingCounter(); + Assert.Equal(0, instanceCount.Counter); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(instanceCount) + .AddSingleton()) + .StartAsync(); + + Assert.Equal(0, instanceCount.Counter); + + var service = host.Services.GetService(); + await host.StopAsync(); + + Assert.NotNull(service); + Assert.Equal(1, instanceCount.Counter); + } + + [Fact] + public async Task ShouldAddAndActivateOnlyOnce_WhenHasChildAsync() + { + var parentCount = new InstanceCreatingCounter(); + var childCount = new InstanceCreatingCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services.AddSingleton(childCount) + .AddSingleton(parentCount) + .AddActivatedSingleton(typeof(IFakeService), typeof(FakeService)) + .AddActivatedSingleton(_ => + { + return new FactoryService(_.GetService()!, _.GetService()!); + })) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, childCount.Counter); + Assert.Equal(1, parentCount.Counter); + } + + [Fact] + public async Task ShouldResolveComponentsAutomaticallyAsync() + { + var parentCount = new InstanceCreatingCounter(); + var childCount = new InstanceCreatingCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(childCount) + .AddSingleton(parentCount) + .AddSingleton() + .AddActivatedSingleton()) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, childCount.Counter); + Assert.Equal(1, parentCount.Counter); + } + + [Fact] + public async Task CanActivateEnumerableAsync() + { + var fakeServiceCount = new InstanceCreatingCounter(); + var fakeFactoryCount = new InstanceCreatingCounter(); + var anotherFakeServiceCount = new AnotherFakeServiceCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(fakeServiceCount) + .AddSingleton(fakeFactoryCount) + .AddSingleton(anotherFakeServiceCount) + .AddActivatedSingleton() + .AddActivatedSingleton() + .AddActivatedSingleton()) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, fakeServiceCount.Counter); + Assert.Equal(1, fakeFactoryCount.Counter); + Assert.Equal(1, anotherFakeServiceCount.Counter); + + await host.StopAsync(); + } + + [Fact] + public async Task CanActivateOneServiceAsync() + { + var fakeServiceCount = new InstanceCreatingCounter(); + var anotherFakeServiceCount = new AnotherFakeServiceCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(fakeServiceCount) + .AddSingleton(anotherFakeServiceCount) + .AddSingleton() + .AddActivatedSingleton()) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(0, fakeServiceCount.Counter); + Assert.Equal(1, anotherFakeServiceCount.Counter); + } + + [Fact] + public async Task ShouldActivateService_WhenTypeIsSpecifiedInTypeParameterTService() + { + var counter = new InstanceCreatingCounter(); + var anotherFakeServiceCount = new AnotherFakeServiceCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(counter) + .AddSingleton(anotherFakeServiceCount) + .AddActivatedSingleton() + .AddActivatedSingleton(_ => new AnotherFakeService(_.GetService()!))) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, counter.Counter); + Assert.Equal(1, anotherFakeServiceCount.Counter); + } + + [Fact] + public async Task ShouldActivateService_WhenTypeIsSpecifiedInParameter() + { + var counter = new InstanceCreatingCounter(); + var anotherFakeServiceCount = new AnotherFakeServiceCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(counter) + .AddSingleton(anotherFakeServiceCount) + .AddActivatedSingleton(typeof(FakeService)) + .AddActivatedSingleton(typeof(AnotherFakeService), _ => new AnotherFakeService(_.GetService()!))) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, counter.Counter); + Assert.Equal(1, anotherFakeServiceCount.Counter); + } + + [Fact] + public async Task TestStopHostAsync() + { + var counter = new InstanceCreatingCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(counter) + .AddActivatedSingleton()) + .StartAsync(); + + Assert.Equal(1, counter.Counter); + await host.StopAsync(); + } + + [Fact] + public async Task ShouldNotActivate_WhenServiceOfTypeSpecifiedInTypeParameter_WasAlreadyAdded() + { + var counter = new InstanceCreatingCounter(); + var anotherFakeServiceCount = new AnotherFakeServiceCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => + { + services + .AddSingleton(counter) + .AddSingleton(anotherFakeServiceCount) + .AddSingleton() + .AddSingleton(); + services.TryAddActivatedSingleton(typeof(FakeService)); + services.TryAddActivatedSingleton(typeof(AnotherFakeService), _ => new AnotherFakeService(_.GetService()!)); + }) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(0, counter.Counter); + Assert.Equal(0, anotherFakeServiceCount.Counter); + } + + [Fact] + public async Task ShouldNotActivate_WhenServiceOfTypeSpecifiedInParameter_WasAlreadyAdded() + { + var counter = new InstanceCreatingCounter(); + var anotherFakeServiceCount = new AnotherFakeServiceCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => + { + services + .AddSingleton(counter) + .AddSingleton(anotherFakeServiceCount) + .AddSingleton() + .AddSingleton(); + services.TryAddActivatedSingleton(); + services.TryAddActivatedSingleton(_ => new AnotherFakeService(_.GetService()!)); + }) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(0, counter.Counter); + Assert.Equal(0, anotherFakeServiceCount.Counter); + } + + [Fact] + public async Task ShouldActivateOneSingleton_WhenTryAddIsCalled_WithTypeSpecifiedImplementation() + { + var counter = new InstanceCreatingCounter(); + var anotherFakeServiceCount = new AnotherFakeServiceCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => + { + services + .AddSingleton(counter) + .AddSingleton(anotherFakeServiceCount); + services.TryAddActivatedSingleton(); + services.TryAddActivatedSingleton(typeof(IFakeService), typeof(AnotherFakeService)); + }) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, counter.Counter); + Assert.Equal(0, anotherFakeServiceCount.Counter); + } + + // ------------------------------------------------------------------------------ + [Fact] + public async Task CanActivateSingletonAsync() + { + var instanceCount = new InstanceCreatingCounter(); + Assert.Equal(0, instanceCount.Counter); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(instanceCount) + .AddSingleton() + .Activate()) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, instanceCount.Counter); + + var service = host.Services.GetService(); + + Assert.NotNull(service); + Assert.Equal(1, instanceCount.Counter); + } + + [Fact] + public async Task ActivationOfNotRegisteredType_ThrowsExceptionAsync() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services.Activate()) + .Build(); + + var exception = await Assert.ThrowsAsync(() => host.StartAsync()); + + Assert.Contains(typeof(IFakeService).FullName!, exception.Message); + } + + [Fact] + public async Task CanActivateEnumerableImplicitlyAddedAsync() + { + var fakeServiceCount = new InstanceCreatingCounter(); + var fakeFactoryCount = new InstanceCreatingCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(fakeServiceCount) + .AddSingleton(fakeFactoryCount) + .AddSingleton().Activate() + .AddSingleton().Activate()) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, fakeServiceCount.Counter); + Assert.Equal(1, fakeFactoryCount.Counter); + } + + [Fact] + public async Task CanActivateEnumerableExplicitlyAddedAsync() + { + var fakeServiceCount = new InstanceCreatingCounter(); + var fakeFactoryCount = new InstanceCreatingCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(fakeServiceCount) + .AddSingleton(fakeFactoryCount) + .AddSingleton() + .AddSingleton() + .Activate>()) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, fakeServiceCount.Counter); + Assert.Equal(1, fakeFactoryCount.Counter); + } + + [Fact] + public async Task CanAutoActivateOpenGenericsAsEnumerableAsync() + { + var fakeServiceCount = new InstanceCreatingCounter(); + var fakeOpenGenericCount = new InstanceCreatingCounter(); + + using var host = await new HostBuilder() + .ConfigureServices(services => services + .AddSingleton(fakeServiceCount) + .AddSingleton(fakeOpenGenericCount) + .AddTransient() + .AddSingleton(typeof(IFakeOpenGenericService), typeof(FakeService)) + .AddSingleton(typeof(IFakeOpenGenericService<>), typeof(FakeOpenGenericService<>)) + .Activate>>() + .Activate>()) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, fakeServiceCount.Counter); + Assert.Equal(2, fakeOpenGenericCount.Counter); + } + + [Fact] + public async Task CanAutoActivateClosedGenericsAsEnumerableAsync() + { + var fakeServiceCount = new InstanceCreatingCounter(); + var fakeOpenGenericCount = new InstanceCreatingCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(fakeServiceCount) + .AddSingleton(fakeOpenGenericCount) + .AddTransient() + .AddSingleton(typeof(IFakeOpenGenericService), typeof(FakeService)) + .AddSingleton, FakeOpenGenericService>() + .Activate>>()) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, fakeServiceCount.Counter); + Assert.Equal(1, fakeOpenGenericCount.Counter); + } +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/AutoActivationExtensionsTests.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/AutoActivationExtensionsTests.cs new file mode 100644 index 0000000000..38079da9ff --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/AutoActivationExtensionsTests.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Test.Fakes; +using Microsoft.Extensions.DependencyInjection.Test.Helpers; +using Xunit; + +namespace Microsoft.Extensions.DependencyInjection.Test; + +public class AutoActivationExtensionsTests +{ + [Fact] + public void Activate_Throws_WhenArgumentsAreNull() + { + Assert.Throws(() => AutoActivationExtensions.Activate(null!)); + } + + [Fact] + public void AddActivatedSingleton_Throws_WhenArgumentsAreNull() + { + var serviceCollection = new ServiceCollection(); + Assert.Throws(() => AutoActivationExtensions.AddActivatedSingleton(null!, _ => null!)); + Assert.Throws(() => AutoActivationExtensions.AddActivatedSingleton(serviceCollection, null!)); + + Assert.Throws(() => AutoActivationExtensions.AddActivatedSingleton(null!, _ => null!)); + Assert.Throws(() => AutoActivationExtensions.AddActivatedSingleton(serviceCollection, null!)); + + Assert.Throws(() => AutoActivationExtensions.AddActivatedSingleton(null!)); + + Assert.Throws(() => AutoActivationExtensions.AddActivatedSingleton(null!, typeof(FakeService))); + Assert.Throws(() => AutoActivationExtensions.AddActivatedSingleton(serviceCollection, null!)); + + Assert.Throws(() => AutoActivationExtensions.AddActivatedSingleton(null!)); + + Assert.Throws(() => AutoActivationExtensions.AddActivatedSingleton(null!, typeof(FakeService), _ => null!)); + Assert.Throws(() => AutoActivationExtensions.AddActivatedSingleton(serviceCollection, null!, _ => null!)); + Assert.Throws(() => AutoActivationExtensions.AddActivatedSingleton(serviceCollection, typeof(FakeService), implementationFactory: null!)); + + Assert.Throws(() => AutoActivationExtensions.AddActivatedSingleton(null!, typeof(IFakeService), typeof(FakeService))); + Assert.Throws(() => AutoActivationExtensions.AddActivatedSingleton(serviceCollection, null!, typeof(FakeService))); + Assert.Throws(() => AutoActivationExtensions.AddActivatedSingleton(serviceCollection, typeof(IFakeService), implementationType: null!)); + } + + [Fact] + public void TryAddActivatedSingleton_Throws_WhenArgumentsAreNull() + { + var serviceCollection = new ServiceCollection(); + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedSingleton(null!, typeof(FakeService))); + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedSingleton(serviceCollection, null!)); + + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedSingleton(null!, typeof(IFakeService), typeof(FakeService))); + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedSingleton(serviceCollection, null!, typeof(FakeService))); + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedSingleton(serviceCollection, typeof(IFakeService), implementationType: null!)); + + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedSingleton(null!, typeof(FakeService), _ => null!)); + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedSingleton(serviceCollection, null!, _ => null!)); + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedSingleton(serviceCollection, typeof(FakeService), implementationFactory: null!)); + + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedSingleton(null!)); + + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedSingleton(null!)); + + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedSingleton(null!, _ => null!)); + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedSingleton(serviceCollection, null!)); + } + + [Fact] + public void AutoActivate_Adds_OneHostedService() + { + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddSingleton(new InstanceCreatingCounter()); + serviceCollection.AddActivatedSingleton(); + Assert.Equal(1, serviceCollection.Count(d => d.ImplementationType == typeof(AutoActivationHostedService))); + + serviceCollection.AddActivatedSingleton(); + Assert.Equal(1, serviceCollection.Count(d => d.ImplementationType == typeof(AutoActivationHostedService))); + } +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/AutoActivationHostedServiceTests.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/AutoActivationHostedServiceTests.cs new file mode 100644 index 0000000000..64ac3d66a3 --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/AutoActivationHostedServiceTests.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.DependencyInjection.Test; + +public class AutoActivationHostedServiceTests +{ + [Fact] + public void Ctor_Throws_WhenOptionsValueIsNull() + { + var options = Microsoft.Extensions.Options.Options.Create(null!); + Assert.Throws(() => new AutoActivationHostedService(Mock.Of(), options)); + } +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/DependencyInjection.AutoActivation.Tests.csproj b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/DependencyInjection.AutoActivation.Tests.csproj new file mode 100644 index 0000000000..c8c8e4220a --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/DependencyInjection.AutoActivation.Tests.csproj @@ -0,0 +1,13 @@ + + + Microsoft.Extensions.DependencyInjection.AutoActivation.Tests + Microsoft.Extensions.DependencyInjection.Test + Tests for Microsoft.Extensions.DependencyInjection.AutoActivation + + + + + + + + diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/AnotherFakeService.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/AnotherFakeService.cs new file mode 100644 index 0000000000..60325eaab4 --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/AnotherFakeService.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection.Test.Helpers; + +namespace Microsoft.Extensions.DependencyInjection.Test.Fakes; + +public class AnotherFakeService : IFakeService +{ + public AnotherFakeService(IAnotherFakeServiceCounter count) + { + count.Counter += 1; + } +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/DifferentPocoClass.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/DifferentPocoClass.cs new file mode 100644 index 0000000000..d8eea16e54 --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/DifferentPocoClass.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.DependencyInjection.Test.Fakes; + +public class DifferentPocoClass +{ +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/FactoryService.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/FactoryService.cs new file mode 100644 index 0000000000..cb1d9701ab --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/FactoryService.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection.Test.Helpers; + +namespace Microsoft.Extensions.DependencyInjection.Test.Fakes; + +public class FactoryService : IFactoryService +{ + public FactoryService(IFakeService fakeService, IFactoryServiceCounter count) + { + FakeService = fakeService; + count.Counter += 1; + } + + public IFakeService FakeService { get; } +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/FakeOneMultipleService.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/FakeOneMultipleService.cs new file mode 100644 index 0000000000..7016632bff --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/FakeOneMultipleService.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection.Test.Helpers; + +namespace Microsoft.Extensions.DependencyInjection.Test.Fakes; + +public class FakeOneMultipleService : IFakeMultipleService +{ + public FakeOneMultipleService(IFakeMultipleCounter count) + { + count.Counter += 1; + } +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/FakeOpenGenericService.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/FakeOpenGenericService.cs new file mode 100644 index 0000000000..7a5656fd47 --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/FakeOpenGenericService.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection.Test.Helpers; + +namespace Microsoft.Extensions.DependencyInjection.Test.Fakes; + +public class FakeOpenGenericService : IFakeOpenGenericService +{ + public FakeOpenGenericService(IFakeOpenGenericCounter count) + { + count.Counter += 1; + } +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/FakeService.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/FakeService.cs new file mode 100644 index 0000000000..ad72b4b99f --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/FakeService.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection.Test.Helpers; + +namespace Microsoft.Extensions.DependencyInjection.Test.Fakes; + +public class FakeService : IFakeService +{ + public FakeService(IFakeServiceCounter count) + { + count.Counter += 1; + } +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/IFactoryService.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/IFactoryService.cs new file mode 100644 index 0000000000..4e57c19c38 --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/IFactoryService.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.DependencyInjection.Test.Fakes; + +public interface IFactoryService +{ + IFakeService FakeService { get; } +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/IFakeMultipleService.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/IFakeMultipleService.cs new file mode 100644 index 0000000000..82dc706fb6 --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/IFakeMultipleService.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.DependencyInjection.Test.Fakes; + +public interface IFakeMultipleService : IFakeService +{ +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/IFakeOpenGenericService.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/IFakeOpenGenericService.cs new file mode 100644 index 0000000000..03a071af8f --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/IFakeOpenGenericService.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.DependencyInjection.Test.Fakes; + +public interface IFakeOpenGenericService +{ +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/IFakeService.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/IFakeService.cs new file mode 100644 index 0000000000..780e609cc8 --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/IFakeService.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.DependencyInjection.Test.Fakes; + +public interface IFakeService : IFakeOpenGenericService +{ +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/PocoClass.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/PocoClass.cs new file mode 100644 index 0000000000..cd626593db --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Fakes/PocoClass.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.DependencyInjection.Test.Fakes; + +public class PocoClass +{ +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/AnotherFakeServiceCounter.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/AnotherFakeServiceCounter.cs new file mode 100644 index 0000000000..9d68c1e7b4 --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/AnotherFakeServiceCounter.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.DependencyInjection.Test.Helpers; + +public class AnotherFakeServiceCounter : IAnotherFakeServiceCounter +{ + public int Counter { get; set; } +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IAnotherFakeServiceCounter.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IAnotherFakeServiceCounter.cs new file mode 100644 index 0000000000..61fcf23246 --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IAnotherFakeServiceCounter.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.DependencyInjection.Test.Helpers; + +public interface IAnotherFakeServiceCounter +{ + public int Counter { get; set; } +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IFactoryServiceCounter.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IFactoryServiceCounter.cs new file mode 100644 index 0000000000..592a617d32 --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IFactoryServiceCounter.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.DependencyInjection.Test.Helpers; + +public interface IFactoryServiceCounter +{ + public int Counter { get; set; } +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IFakeMultipleCounter.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IFakeMultipleCounter.cs new file mode 100644 index 0000000000..772b0be54d --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IFakeMultipleCounter.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.DependencyInjection.Test.Helpers; + +public interface IFakeMultipleCounter +{ + public int Counter { get; set; } +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IFakeOpenGenericCounter.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IFakeOpenGenericCounter.cs new file mode 100644 index 0000000000..e7bc46ec66 --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IFakeOpenGenericCounter.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.DependencyInjection.Test.Helpers; + +public interface IFakeOpenGenericCounter +{ + public int Counter { get; set; } +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IFakeServiceCounter.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IFakeServiceCounter.cs new file mode 100644 index 0000000000..d7708a196d --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/IFakeServiceCounter.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.DependencyInjection.Test.Helpers; + +public interface IFakeServiceCounter +{ + public int Counter { get; set; } +} diff --git a/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/InstanceCreatingCounter.cs b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/InstanceCreatingCounter.cs new file mode 100644 index 0000000000..8ef13bc8c9 --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.AutoActivation.Tests/Helpers/InstanceCreatingCounter.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.DependencyInjection.Test.Helpers; + +public class InstanceCreatingCounter : IFactoryServiceCounter, IFakeServiceCounter, IFakeMultipleCounter, IFakeOpenGenericCounter +{ + public int Counter { get; set; } +} diff --git a/test/ToBeMoved/DependencyInjection.NamedService.Tests/DependencyInjection.NamedService.Tests.csproj b/test/ToBeMoved/DependencyInjection.NamedService.Tests/DependencyInjection.NamedService.Tests.csproj new file mode 100644 index 0000000000..7efb9bb610 --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.NamedService.Tests/DependencyInjection.NamedService.Tests.csproj @@ -0,0 +1,15 @@ + + + Microsoft.Extensions.DependencyInjection.NamedService.Tests + Microsoft.Extensions.DependencyInjection.NamedService.Test + Tests for Microsoft.Extensions.DependencyInjection.NamedService + + + + + + + + + + diff --git a/test/ToBeMoved/DependencyInjection.NamedService.Tests/ResolutionTests.cs b/test/ToBeMoved/DependencyInjection.NamedService.Tests/ResolutionTests.cs new file mode 100644 index 0000000000..0326a1470c --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.NamedService.Tests/ResolutionTests.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.DependencyInjection.NamedService.Test; + +public class ResolutionTests : IDisposable +{ + private const string NamedSingleton = nameof(NamedSingleton); + private const string NamedTransient = nameof(NamedTransient); + private readonly ServiceProvider _provider; + + public ResolutionTests() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddNamedSingleton(NamedSingleton); + serviceCollection.AddNamedSingleton(NamedSingleton); + serviceCollection.AddNamedTransient(NamedTransient); + _provider = serviceCollection.BuildServiceProvider(); + } + + [Fact] + public void GetRequiredService_WhenTypeIsRegistered_ShouldReturnNewObject() + { + var namedProvider = _provider.GetRequiredService>(); + var service = namedProvider.GetRequiredService(NamedSingleton); + service.Should().BeOfType(); + } + + [Fact] + public void GetRequiredService_WhenResolveSingletonSecondTime_ShouldReturnSameObject() + { + var namedProvider = _provider.GetRequiredService>(); + var service = namedProvider.GetRequiredService(NamedSingleton); + var service2 = namedProvider.GetRequiredService(NamedSingleton); + service2.Should().BeOfType(); + service2.Should().BeSameAs(service); + } + + [Fact] + public void GetRequiredService_WhenResolveTransientSecondTime_ShouldReturnNewObject() + { + var namedProvider = _provider.GetRequiredService>(); + var service = namedProvider.GetRequiredService(NamedTransient); + var service2 = namedProvider.GetRequiredService(NamedTransient); + service2.Should().NotBeSameAs(service); + } + + [Fact] + public void GetService_WhenNotRegistered_ShouldReturnNull() + { + var namedProvider = _provider.GetRequiredService>(); + var service = namedProvider.GetService("bogus"); + service.Should().BeNull(); + } + + [Fact] + public void GetRequiredService_WhenNotRegistered_ShouldThrow() + { + var namedProvider = _provider.GetRequiredService>(); + Assert.Throws(() => namedProvider.GetRequiredService("bogus")); + } + + [Fact] + public void GetServices_WhenMultipleTypes_ReturnsCollection() + { + var namedProvider = _provider.GetRequiredService>(); + var collection = namedProvider.GetServices(NamedSingleton).ToList(); + collection.Should().HaveCount(2); + collection.First().Should().BeOfType(); + collection.Skip(1).First().Should().BeOfType(); + } + + private class TestClass + { + } + + private sealed class DuplicateTestClass : TestClass + { + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _provider.Dispose(); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/test/ToBeMoved/DependencyInjection.Pools.Tests/DependencyInjection.Pools.Tests.csproj b/test/ToBeMoved/DependencyInjection.Pools.Tests/DependencyInjection.Pools.Tests.csproj new file mode 100644 index 0000000000..2db128ad8b --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.Pools.Tests/DependencyInjection.Pools.Tests.csproj @@ -0,0 +1,16 @@ + + + Microsoft.Extensions.DependencyInjection.Pools.Tests + Microsoft.Extensions.DependencyInjection.Pools.Test + Tests for Microsoft.Extensions.DependencyInjection.Pools + + + + + + + + + + + diff --git a/test/ToBeMoved/DependencyInjection.Pools.Tests/DependencyInjectionExtensionsTest.cs b/test/ToBeMoved/DependencyInjection.Pools.Tests/DependencyInjectionExtensionsTest.cs new file mode 100644 index 0000000000..0a862ff63d --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.Pools.Tests/DependencyInjectionExtensionsTest.cs @@ -0,0 +1,117 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection.Pools.Test.TestResources; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.DependencyInjection.Pools.Test; + +public class DependencyInjectionExtensionsTest +{ + [Fact] + public void ConfigurePools_ConfiguresPoolOptions() + { + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(new[] + { + new KeyValuePair($"R9:Pools:{typeof(TestClass).FullName!}", "2048"), + new KeyValuePair($"R9:Pools:{typeof(TestDependency).FullName!}", "4096"), + }); + + var services = new ServiceCollection().ConfigurePools(builder.Build().GetSection("R9:Pools")); + using var provider = services.BuildServiceProvider(); + + var sut = provider.GetRequiredService>(); + + Assert.Equal(2048, sut.Get(typeof(TestClass).FullName!).Capacity); + Assert.Equal(4096, sut.Get(typeof(TestDependency).FullName!).Capacity); + } + + [Fact] + public void ConfigurePools_ThrowsOnUnparsableMaximumCapacity() + { + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(new[] + { + new KeyValuePair($"R9:Pools:{typeof(TestClass).FullName!}", "twenty!"), + new KeyValuePair($"R9:Pools:{typeof(TestDependency).FullName!}", "4096"), + }); + + var exception = Assert.Throws( + () => new ServiceCollection().ConfigurePools(builder.Build().GetSection("R9:Pools"))); + + Assert.StartsWith( + "Can't parse 'Microsoft.Extensions.DependencyInjection.Pools.Test.TestResources.TestClass' value 'twenty!' to integer.", + exception.Message); + } + + [Fact] + public void ConfigurePools_ThrowsOnNullSection() + { + var exception = Assert.Throws(() => new ServiceCollection().ConfigurePools(null!)); + Assert.StartsWith("Value cannot be null.", exception.Message); + } + + [Fact] + public void AddPool_ServiceTypeOnly_AddsPool() + { + var services = new ServiceCollection().AddPool(); + + var sut = services.BuildServiceProvider().GetService>(); + using var provider = services.BuildServiceProvider(); + var optionsMonitor = provider.GetRequiredService>(); + + Assert.NotNull(sut); + Assert.Equal(1024, optionsMonitor.Get(typeof(TestDependency).FullName).Capacity); + } + + [Fact] + public void AddPool_ServiceTypeOnlyWithCapacity_AddsPoolAndSetsCapacity() + { + var services = new ServiceCollection().AddPool(options => options.Capacity = 64); + + var sut = services.BuildServiceProvider().GetService>(); + using var provider = services.BuildServiceProvider(); + var optionsMonitor = provider.GetRequiredService>(); + + Assert.NotNull(sut); + Assert.Equal(64, optionsMonitor.Get(typeof(TestDependency).FullName).Capacity); + } + + [Fact] + public void AddPool_ServiceAndImplementationType_AddsPool() + { + var services = new ServiceCollection() + .AddSingleton() + .AddPool(); + + using var provider = services.BuildServiceProvider(); + var sut = provider.GetService>(); + var optionsMonitor = provider.GetRequiredService>(); + + Assert.NotNull(sut); + Assert.Equal(TestDependency.Message, sut!.Get().ReadMessage()); + Assert.Equal(1024, optionsMonitor.Get(typeof(ITestClass).FullName).Capacity); + } + + [Fact] + public void AddPool_ServiceAndImplementationTypeWithCapacity_AddsPoolAndSetsCapacity() + { + var services = new ServiceCollection() + .AddSingleton() + .AddPool(options => options.Capacity = 64); + + using var provider = services.BuildServiceProvider(); + var sut = provider.GetService>(); + var optionsMonitor = provider.GetRequiredService>(); + + Assert.NotNull(sut); + Assert.Equal(TestDependency.Message, sut!.Get().ReadMessage()); + Assert.Equal(64, optionsMonitor.Get(typeof(ITestClass).FullName).Capacity); + } +} diff --git a/test/ToBeMoved/DependencyInjection.Pools.Tests/TestResources/ITestClass.cs b/test/ToBeMoved/DependencyInjection.Pools.Tests/TestResources/ITestClass.cs new file mode 100644 index 0000000000..20ff2561f3 --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.Pools.Tests/TestResources/ITestClass.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.DependencyInjection.Pools.Test.TestResources; + +public interface ITestClass +{ + int ResetCalled { get; } + string ReadMessage(); +} diff --git a/test/ToBeMoved/DependencyInjection.Pools.Tests/TestResources/TestClass.cs b/test/ToBeMoved/DependencyInjection.Pools.Tests/TestResources/TestClass.cs new file mode 100644 index 0000000000..413f8740b3 --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.Pools.Tests/TestResources/TestClass.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.Extensions.DependencyInjection.Pools.Test.TestResources; + +public class TestClass : IResettable, ITestClass +{ + public int ResetCalled { get; private set; } + private readonly TestDependency _testClass; + + public TestClass(TestDependency testClass) + { + _testClass = testClass; + } + + public string ReadMessage() => _testClass.ReadMessage(); + + public bool TryReset() + { + ResetCalled++; + return true; + } +} diff --git a/test/ToBeMoved/DependencyInjection.Pools.Tests/TestResources/TestDependency.cs b/test/ToBeMoved/DependencyInjection.Pools.Tests/TestResources/TestDependency.cs new file mode 100644 index 0000000000..99a31b7dbd --- /dev/null +++ b/test/ToBeMoved/DependencyInjection.Pools.Tests/TestResources/TestDependency.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.DependencyInjection.Pools.Test.TestResources; + +public class TestDependency +{ + public const string Message = "I'm here!"; + +#pragma warning disable CA1822 + public string ReadMessage() => Message; +#pragma warning restore CA1822 +} diff --git a/test/ToBeMoved/Directory.Build.props b/test/ToBeMoved/Directory.Build.props new file mode 100644 index 0000000000..ef415739cb --- /dev/null +++ b/test/ToBeMoved/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + $(NetCoreTargetFrameworks) + $(NetCoreTargetFrameworks)$(ConditionalNet462) + + diff --git a/test/ToBeMoved/Hosting.StartupInitialization.Tests/Hosting.StartupInitialization.Tests.csproj b/test/ToBeMoved/Hosting.StartupInitialization.Tests/Hosting.StartupInitialization.Tests.csproj new file mode 100644 index 0000000000..f513cc76a8 --- /dev/null +++ b/test/ToBeMoved/Hosting.StartupInitialization.Tests/Hosting.StartupInitialization.Tests.csproj @@ -0,0 +1,20 @@ + + + + Microsoft.Extensions.Hosting.Testing.StartupInitialization.Tests + Microsoft.Extensions.Hosting.Testing.StartupInitialization.Test + Tests for Microsoft.Extensions.Hosting.Testing.StartupInitialization + + + + + + + + + + + + + + diff --git a/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/Database.cs b/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/Database.cs new file mode 100644 index 0000000000..51446a5eb8 --- /dev/null +++ b/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/Database.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Hosting.Testing.StartupInitialization.Test; + +public class Database +{ + private readonly ILogger _logger; + + public const string LogMessage = "HEY I WAS INITIALIZED AT STARTUP IN ASYNC WAY HEHE"; + + public Database(ILogger logger) + { + _logger = logger; + } + + public Task Initialize() + { + _logger.LogInformation(LogMessage); + + return Task.CompletedTask; + } +} diff --git a/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/DatabaseInitializer.cs b/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/DatabaseInitializer.cs new file mode 100644 index 0000000000..3e349866ac --- /dev/null +++ b/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/DatabaseInitializer.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Hosting.Testing.StartupInitialization.Test; + +public class DatabaseInitializer : IStartupInitializer +{ + private readonly ILogger _logger; + public const string LogMessage = "HEY I WAS INITIALIZED AT STARTUP IN ASYNC WAY HEHE"; + + public DatabaseInitializer(ILogger logger) + { + _logger = logger; + } + + public Task InitializeAsync(CancellationToken token) + { + _logger.LogInformation(LogMessage); + + return Task.CompletedTask; + } +} diff --git a/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/DummyHostedService.cs b/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/DummyHostedService.cs new file mode 100644 index 0000000000..0ca01e74f2 --- /dev/null +++ b/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/DummyHostedService.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.Extensions.Hosting.Testing.StartupInitialization.Test; + +#pragma warning disable SA1402 // File may only contain a single type + +[SuppressMessage("Minor Code Smell", "S3717:Track use of \"NotImplementedException\"", Justification = "Not applicable.")] +public class DummyHostedService : IHostedService +{ + public Task StartAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task StopAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); +} + +[SuppressMessage("Minor Code Smell", "S3717:Track use of \"NotImplementedException\"", Justification = "Not applicable.")] +public class DummyHostedService2 : IHostedService +{ + public Task StartAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task StopAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); +} + +[SuppressMessage("Minor Code Smell", "S3717:Track use of \"NotImplementedException\"", Justification = "Not applicable.")] +public class DummyHostedService3 : IHostedService +{ + public Task StartAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task StopAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); +} diff --git a/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/TestResources.cs b/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/TestResources.cs new file mode 100644 index 0000000000..aeca5012f5 --- /dev/null +++ b/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/TestResources.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Extensions.Hosting.Testing.StartupInitialization.Test; + +public static class TestResources +{ + public static IConfigurationSection GetSection(TimeSpan timeout) + { + StartupInitializationOptions options; + + return new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { $"{nameof(StartupInitializationOptions)}:{nameof(options.Timeout)}", timeout.ToString() }, + }) + .Build() + .GetSection($"{nameof(StartupInitializationOptions)}"); + } +} diff --git a/test/ToBeMoved/Hosting.StartupInitialization.Tests/StartupInitializationAcceptanceTest.cs b/test/ToBeMoved/Hosting.StartupInitialization.Tests/StartupInitializationAcceptanceTest.cs new file mode 100644 index 0000000000..3380bbe6c2 --- /dev/null +++ b/test/ToBeMoved/Hosting.StartupInitialization.Tests/StartupInitializationAcceptanceTest.cs @@ -0,0 +1,263 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting.Testing.Internal; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Time.Testing; +using Microsoft.Shared.Diagnostics; +using Xunit; + +namespace Microsoft.Extensions.Hosting.Testing.StartupInitialization.Test; + +public class StartupInitializationAcceptanceTest +{ + [Fact] + public async Task Initialization_Functions_Are_Executed_On_Startup_In_Async_Manner_And_Logs_Message() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices((_, services) => services + .AddSingleton() + .AddStartupInitialization() + .AddInitializer(async static (sp, _) => + { + var db = sp.GetService(); + + Assert.NotNull(db); + await db!.Initialize(); + })) + .Build(); + + await host.StartAsync(); + await host.StopAsync(); + + var logMessages = host.GetFakeLogCollector().GetSnapshot().Select(x => x.Message); + + Assert.Contains(Database.LogMessage, logMessages); + } + + [Fact] + public async Task Initialization_Functions_Are_Executed_On_Startup_In_Async_Manner_And_Logs_Message_When_Using_Interface_Registration() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices((_, services) => services + .AddStartupInitialization() + .AddInitializer()) + .Build(); + + await host.StartAsync(); + await host.StopAsync(); + + var logMessages = host.GetFakeLogCollector().GetSnapshot().Select(x => x.Message); + + Assert.Contains(Database.LogMessage, logMessages); + } + + [Fact] + public void Initializers_Are_Indempotent_When_Provided_As_Interface() + { + using var sp = new ServiceCollection() + .AddLogging() + .AddStartupInitialization() + .AddInitializer() + .AddInitializer() + .AddInitializer() + .AddInitializer() + .Services + .BuildServiceProvider(); + + var i = sp.GetRequiredService>(); + + Assert.Single(i); + } + + [Fact] + public void Initializers_Are_Not_Indempotent_When_Provided_As_Anonymous_Function() + { + using var sp = new ServiceCollection() + .AddLogging() + .AddStartupInitialization() + .AddInitializer((_, _) => Task.CompletedTask) + .AddInitializer((_, _) => Task.CompletedTask) + .AddInitializer((_, _) => Task.CompletedTask) + .AddInitializer((_, _) => Task.CompletedTask) + .AddInitializer((_, _) => Task.CompletedTask) + .Services + .BuildServiceProvider(); + + var i = sp.GetRequiredService>().ToArray(); + + Assert.Equal(5, i.Length); + } + + [Fact] + public async Task Initialization_Functions_Are_Not_Executed_On_Startup_So_There_Is_No_Log_Message() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices((_, services) => services + .AddSingleton()) + .Build(); + + await host.StartAsync(); + await host.StopAsync(); + + var logMessages = host.GetFakeLogCollector().GetSnapshot().Select(x => x.Message); + + Assert.DoesNotContain(Database.LogMessage, logMessages); + } + + [Fact] + public void When_Registering_Multiple_Hosted_Services_StartupService_Is_First() + { + const int RegisteredHostedServices = 4; + + using var host = FakeHost.CreateBuilder() + .ConfigureServices((_, services) => services + .AddSingleton() + .AddHostedService() + .AddHostedService() + .AddHostedService() + .AddStartupInitialization() + .AddInitializer(async static (sp, _) => + { + var db = sp.GetService(); + + Assert.NotNull(db); + await db!.Initialize(); + })) + .Build(); + + var jobs = host.Services + .GetRequiredService>() + ?.ToArray(); + + Assert.NotNull(jobs); + + // In case FakeHost is adding some stuff. + Assert.True(jobs!.Length >= RegisteredHostedServices); + Assert.IsAssignableFrom(jobs[0]); + } + + [Fact] + public async Task Initialization_Function_Times_Out_When_It_Takes_Longer_Than_Options() + { + var fiveSeconds = TimeSpan.FromSeconds(5); + var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow); + + using var host = FakeHost.CreateBuilder() + .ConfigureServices(s => s + .AddSingleton(timeProvider) + .AddStartupInitialization(x => x.Timeout = fiveSeconds) + .AddInitializer((_, ct) => + { + timeProvider.Advance(fiveSeconds); + return Task.Delay(-1, ct); + })) + .Build(); + + var e = await Assert.ThrowsAsync(() => host.StartAsync()); + + Assert.Contains(fiveSeconds.ToString(), e.Message); + Assert.Contains(nameof(StartupInitializationOptions), e.Message); + } + + [Fact] + public async Task Initialization_Function_Is_Cancelled_Without_Message_When_HostBuilder_Is_Canceled() + { + var fiveSeconds = TimeSpan.FromSeconds(5); + var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow); + + var twoSeconds = TimeSpan.FromSeconds(2); + using var cts = timeProvider.CreateCancellationTokenSource(twoSeconds); + using var host = FakeHost.CreateBuilder() + .ConfigureServices(s => s + .AddSingleton(timeProvider) + .AddStartupInitialization(x => x.Timeout = fiveSeconds) + .AddInitializer((_, ct) => + { + timeProvider.Advance(twoSeconds); + return Task.Delay(-1, ct); + })) + .Build(); + + var e = await Assert.ThrowsAsync(() => host.StartAsync(cts.Token)); + + Assert.DoesNotContain(fiveSeconds.ToString(), e.Message); + Assert.DoesNotContain(nameof(StartupInitializationOptions), e.Message); + } + + [Fact] + public void Can_Use_Configuration_Section_To_Configure_StartupInitializationOptions() + { + var timeout = TimeSpan.FromSeconds(29); + + var o = new ServiceCollection() + .AddStartupInitialization(TestResources.GetSection(timeout)) + .Services + .BuildServiceProvider() + .GetRequiredService>(); + + Assert.NotNull(o?.Value); + Assert.Equal(timeout, o!.Value.Timeout); + } + + [Theory] + [InlineData(60000)] + [InlineData(4)] + public void When_Setting_Initialization_Timeout_Out_Of_Boundary_Validator_Throws(int seconds) + { + var o = new ServiceCollection() + .AddStartupInitialization(x => x.Timeout = TimeSpan.FromSeconds(seconds)) + .Services + .BuildServiceProvider() + .GetRequiredService>(); + + Assert.Throws(() => o?.Value); + } + + [Fact] + public void StartupHostedService_Gets_Registered_Only_Once_In_DI_And_It_Is_First() + { + using var sp = new ServiceCollection() + .AddStartupInitialization() + .Services + .AddStartupInitialization() + .Services + .AddStartupInitialization() + .Services + .AddStartupInitialization() + .Services + .AddStartupInitialization(_ => { }) + .Services + .AddStartupInitialization(_ => { }) + .Services + .BuildServiceProvider(); + + var s = sp.GetRequiredService>().ToArray(); + + Assert.IsAssignableFrom(s[0]); + Assert.Equal(1, s.Count(x => x is StartupHostedService)); + } + + [Fact] + public void When_Debugger_Is_Attached_Hosted_Service_Timeout_Is_Set_To_Infinite() + { + using var provider = new ServiceCollection() + .AddAttachedDebuggerState() + .AddStartupInitialization() + .AddInitializer((_, _) => Task.CompletedTask) + .Services + .BuildServiceProvider(); + + var service = provider + .GetRequiredService>() + .FirstOrDefault(x => x is StartupHostedService); + + Assert.IsAssignableFrom(service); + Assert.Equal(((StartupHostedService)service!).Timeout, System.Threading.Timeout.InfiniteTimeSpan); + } +} diff --git a/test/ToBeMoved/Hosting.StartupInitialization.Tests/StartupInitializationExtensionsTest.cs b/test/ToBeMoved/Hosting.StartupInitialization.Tests/StartupInitializationExtensionsTest.cs new file mode 100644 index 0000000000..93cc96b31d --- /dev/null +++ b/test/ToBeMoved/Hosting.StartupInitialization.Tests/StartupInitializationExtensionsTest.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Hosting.Testing.StartupInitialization.Test; +public class StartupInitializationExtensionsTest +{ + [Fact] + public void Public_API_Throws_On_Nulls() + { + var s = new ServiceCollection(); + + Assert.Throws(() => s.AddStartupInitialization((Action)null!)); + Assert.Throws(() => s.AddStartupInitialization((IConfigurationSection)null!)); + Assert.Throws(() => s.AddStartupInitialization().AddInitializer(null!)); + } + + [Fact] + public void Startup_Initializers_Are_Registered_As_Transient_So_They_Do_Not_Waste_Memory_After_They_Are_Used() + { + var s = new ServiceCollection() + .AddLogging() + .AddStartupInitialization() + .AddInitializer() + .Services; + + using var sp = s.BuildServiceProvider(); + + var first = sp.GetRequiredService(); + var second = sp.GetRequiredService(); + + Assert.IsAssignableFrom(first); + Assert.IsAssignableFrom(second); + Assert.NotEqual(first, second); + } +} diff --git a/test/ToBeMoved/HttpClient.SocketHandling.Tests/HttpClient.SocketHandling.Tests.csproj b/test/ToBeMoved/HttpClient.SocketHandling.Tests/HttpClient.SocketHandling.Tests.csproj new file mode 100644 index 0000000000..9616a32f61 --- /dev/null +++ b/test/ToBeMoved/HttpClient.SocketHandling.Tests/HttpClient.SocketHandling.Tests.csproj @@ -0,0 +1,19 @@ + + + Microsoft.Extensions.HttpClient.SocketHandling.Tests + Microsoft.Extensions.HttpClient.SocketHandling.Test + Tests for Microsoft.Extensions.HttpClient.SocketHandling + $(NetCoreTargetFrameworks) + + + + + + + + + + + + + diff --git a/test/ToBeMoved/HttpClient.SocketHandling.Tests/HttpClientSocketHandlingExtensionsTest.cs b/test/ToBeMoved/HttpClient.SocketHandling.Tests/HttpClientSocketHandlingExtensionsTest.cs new file mode 100644 index 0000000000..7d0c0485b1 --- /dev/null +++ b/test/ToBeMoved/HttpClient.SocketHandling.Tests/HttpClientSocketHandlingExtensionsTest.cs @@ -0,0 +1,268 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using AutoFixture; +using AutoFixture.Kernel; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.HttpClient.SocketHandling.Test.Utils; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.HttpClient.SocketHandling.Test; + +public class HttpClientSocketHandlingExtensionsTest +{ + [Fact] + public void AddSocketsHandler_NotUsingBuilder_ReturnsOriginalBuilderInstance() + { + var services = new ServiceCollection(); + + var originalBuilder = services.AddHttpClient(); + var returnedBuilder = originalBuilder.AddSocketsHttpHandler(); + + returnedBuilder.Should().Be(originalBuilder); + } + + [Fact] + public void AddSocketsHandler_UsingBuilder_ReturnsOriginalBuilderInstance() + { + var services = new ServiceCollection(); + + var originalBuilder = services.AddHttpClient(); + var returnedBuilder = originalBuilder.AddSocketsHttpHandler(builder => builder.DisableRemoteCertificateValidation()); + + returnedBuilder.Should().Be(originalBuilder); + } + + [Fact] + public void AddSocketsHandler_WithDefaultOptions_ShouldUpdateHandler() + { + var services = new ServiceCollection(); + + _ = services + .AddHttpClient() + .AddSocketsHttpHandler(); + + using var provider = services.BuildServiceProvider(); + var primaryHandler = provider.ResolveHttpPrimaryHandler(); + + primaryHandler.ExtractOptions().Should().BeEquivalentTo(new SocketsHttpHandlerOptions()); + } + + [Fact] + public void AddSocketsHandler_WithExplicitOptions_ShouldUpdateHandler() + { + SocketsHttpHandlerOptions? socketOptions = null; + var services = new ServiceCollection(); + + _ = services + .AddHttpClient() + .AddSocketsHttpHandler(); + + services.Configure(nameof(HttpClientSocketHandlingExtensionsTest), o => + { + new AutoPropertiesCommand().Execute(o, new SpecimenContext(CreateFixture())); + socketOptions = o; // capture + }); + + using var provider = services.BuildServiceProvider(); + var primaryHandler = provider.ResolveHttpPrimaryHandler(); + + primaryHandler.ExtractOptions().Should().BeEquivalentTo(socketOptions); + } + + [Fact] + public void AddSocketsHandler_WithInPlaceOptions_ShouldUpdateHandler() + { + SocketsHttpHandlerOptions? socketOptions = null; + var services = new ServiceCollection(); + + _ = services + .AddHttpClient() + .AddSocketsHttpHandler(builder => + { + builder.ConfigureOptions(options => + { + new AutoPropertiesCommand().Execute(options, new SpecimenContext(CreateFixture())); + socketOptions = options; // capture + }); + }); + + using var provider = services.BuildServiceProvider(); + var primaryHandler = provider.ResolveHttpPrimaryHandler(); + + primaryHandler.ExtractOptions().Should().BeEquivalentTo(socketOptions); + } + + [Fact] + public void AddSocketsHandler_WithConfigSection_ShouldUpdateHandler() + { + const string ConnectTimeout = "00:04:56"; + const string ConfigSection = "sockets"; + + using var host = FakeHost.CreateBuilder() + .ConfigureHostConfiguration($"{ConfigSection}:ConnectTimeout", ConnectTimeout) + .ConfigureServices((context, services) => services + .AddHttpClient() + .AddSocketsHttpHandler(builder => builder.ConfigureOptions(context.Configuration.GetSection(ConfigSection)))) + .Build(); + + var primaryHandler = host.Services.ResolveHttpPrimaryHandler(); + var socketsOptions = host.Services.GetRequiredService>() + .Get(nameof(HttpClientSocketHandlingExtensionsTest)); + + socketsOptions.ConnectTimeout.Should().Be(TimeSpan.Parse(ConnectTimeout, CultureInfo.InvariantCulture)); + primaryHandler.ExtractOptions().Should().BeEquivalentTo(socketsOptions); + } + + [Fact] + public void AddSocketsHandler_WithDefaultOptions_ChangesAutomaticDecompression() + { + var services = new ServiceCollection(); + + _ = services + .AddHttpClient() + .AddSocketsHttpHandler(); + + using var provider = services.BuildServiceProvider(); + var primaryHandler = provider.ResolveHttpPrimaryHandler(); + + ((SocketsHttpHandler)primaryHandler).AutomaticDecompression.Should().Be(DecompressionMethods.All); + } + + [Fact] + public void DisableRemoteCertificateValidation_WithBogusArguments_ReturnsTrue() + { + var services = new ServiceCollection(); + + _ = services + .AddHttpClient() + .AddSocketsHttpHandler(builder => builder.DisableRemoteCertificateValidation()); + + using var provider = services.BuildServiceProvider(); + var primaryHandler = provider.ResolveHttpPrimaryHandler(); + var options = ((SocketsHttpHandler)primaryHandler).SslOptions; + + options.RemoteCertificateValidationCallback!(null!, null!, null!, SslPolicyErrors.RemoteCertificateNameMismatch) + .Should().BeTrue(); + } + + [Fact] + public void ConfigureClientCertificate_WithCertificate_AppliesIt() + { + using var certificate = new X509Certificate2(Array.Empty()); + + var services = new ServiceCollection(); + + _ = services + .AddHttpClient() + .AddSocketsHttpHandler(builder => builder.ConfigureClientCertificate(_ => certificate)); + + using var provider = services.BuildServiceProvider(); + var primaryHandler = provider.ResolveHttpPrimaryHandler(); + var callback = ((SocketsHttpHandler)primaryHandler).SslOptions.LocalCertificateSelectionCallback; + + callback!(null!, null!, new X509CertificateCollection(), null!, null!).Should().Be(certificate); + } + + [Fact] + public void ConfigureClientCertificate_NullCertificate_ThrowsOnBuild() + { + var services = new ServiceCollection(); + + _ = services + .AddHttpClient() + .AddSocketsHttpHandler(builder => builder.ConfigureClientCertificate(_ => null!)); + + using var provider = services.BuildServiceProvider(); + var executingBuilder = () => provider.ResolveHttpPrimaryHandler(); + + executingBuilder.Should().Throw() + .WithMessage("The parameter clientCertificate returned null when called."); + } + + [Fact] + public void AddSocketsHandler_WithConfigureAction_ShouldUpdateHandler() + { + var sslClientAuthenticationOptions = new SslClientAuthenticationOptions(); + var services = new ServiceCollection(); + + _ = services + .AddHttpClient() + .AddSocketsHttpHandler(builder => + { + builder.ConfigureHandler(handler => handler.SslOptions = sslClientAuthenticationOptions); + }); + + using var provider = services.BuildServiceProvider(); + var primaryHandler = provider.ResolveHttpPrimaryHandler(); + + ((SocketsHttpHandler)primaryHandler).SslOptions.Should().Be(sslClientAuthenticationOptions); + } + + [Fact] + public void AddSocketsHandler_WithConfigureActionWithServiceProvider_ShouldUseServiceProvider() + { + IServiceProvider? capturedServices = null; + var services = new ServiceCollection(); + + _ = services + .AddHttpClient() + .AddSocketsHttpHandler(builder => + { + builder.ConfigureHandler((provider, _) => + { + capturedServices = provider; + }); + }); + + using var provider = services.BuildServiceProvider(); + _ = provider.ResolveHttpPrimaryHandler(); + + capturedServices.Should().Be(provider.GetRequiredService()); + } + + [Fact] + public void AddSocketsHandler_WithConfigureActionWithServiceProvider_ShouldUpdateHandler() + { + var sslClientAuthenticationOptions = new SslClientAuthenticationOptions(); + var services = new ServiceCollection(); + + _ = services + .AddHttpClient() + .AddSocketsHttpHandler(builder => + { + builder.ConfigureHandler((_, handler) => + { + handler.SslOptions = sslClientAuthenticationOptions; + }); + }); + + using var provider = services.BuildServiceProvider(); + var primaryHandler = provider.ResolveHttpPrimaryHandler(); + + ((SocketsHttpHandler)primaryHandler).SslOptions.Should().Be(sslClientAuthenticationOptions); + } + + private static Fixture CreateFixture() + { + var customizedFixture = new Fixture(); + + customizedFixture.Customize(c => + { + // SocketsHttpHandler setters enforce some values to be bigger than 1 seconds therefore this customization. + return c.FromFactory(() => TimeSpan.FromSeconds(1) + TimeSpan.FromTicks(customizedFixture.Create())); + }); + + return customizedFixture; + } +} diff --git a/test/ToBeMoved/HttpClient.SocketHandling.Tests/Utils/HttpMessageHandlerBuilderHelpers.cs b/test/ToBeMoved/HttpClient.SocketHandling.Tests/Utils/HttpMessageHandlerBuilderHelpers.cs new file mode 100644 index 0000000000..963a44fa70 --- /dev/null +++ b/test/ToBeMoved/HttpClient.SocketHandling.Tests/Utils/HttpMessageHandlerBuilderHelpers.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.HttpClient.SocketHandling.Test.Utils; + +public static class HttpMessageHandlerBuilderHelpers +{ + public static HttpMessageHandler ResolveHttpPrimaryHandler(this IServiceProvider services) + { + var name = typeof(T).Name; + var optionsMonitor = services.GetRequiredService>(); + var options = optionsMonitor.Get(name); + + var builder = services.GetRequiredService(); + builder.Name = name; + + foreach (var action in options.HttpMessageHandlerBuilderActions) + { + action(builder); + } + + return builder.PrimaryHandler; + } + + public static SocketsHttpHandlerOptions ExtractOptions(this HttpMessageHandler messageHandler) + { + var handler = (SocketsHttpHandler)messageHandler; + + return new SocketsHttpHandlerOptions + { + AllowAutoRedirect = handler.AllowAutoRedirect, + UseCookies = handler.UseCookies, + MaxConnectionsPerServer = handler.MaxConnectionsPerServer, + AutomaticDecompression = handler.AutomaticDecompression, + ConnectTimeout = handler.ConnectTimeout, + PooledConnectionLifetime = handler.PooledConnectionLifetime, + PooledConnectionIdleTimeout = handler.PooledConnectionIdleTimeout, +#if NET5_0_OR_GREATER + KeepAlivePingDelay = handler.KeepAlivePingDelay, + KeepAlivePingTimeout = handler.KeepAlivePingTimeout, +#endif + }; + } +} diff --git a/test/ToBeRemoved/Directory.Build.props b/test/ToBeRemoved/Directory.Build.props new file mode 100644 index 0000000000..ef415739cb --- /dev/null +++ b/test/ToBeRemoved/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + $(NetCoreTargetFrameworks) + $(NetCoreTargetFrameworks)$(ConditionalNet462) + + diff --git a/test/ToBeRemoved/Options.ValidateOnStart.Tests/AcceptanceTest.cs b/test/ToBeRemoved/Options.ValidateOnStart.Tests/AcceptanceTest.cs new file mode 100644 index 0000000000..9af12ca45a --- /dev/null +++ b/test/ToBeRemoved/Options.ValidateOnStart.Tests/AcceptanceTest.cs @@ -0,0 +1,480 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Options.Validation.Test; + +public class AcceptanceTest +{ + [Fact] + public async Task CanValidateOptionsEagerlyWithDefaultError() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => o.Boolean = false) + .Validate(o => o.Boolean)) + .Build(); + + var error = await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + + ValidateFailure(error); + } + + [Fact] + public async Task CanValidateOptionsEagerlyWithDefaultError_WithName() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions("bad_configuration") + .Configure(o => o.Boolean = false) + .Validate(o => o.Boolean)) + .Build(); + + var error = await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + + ValidateFailure(error); + } + + [Fact] + public async Task CanValidateOptionsEagerlyWithDefaultError_WithNullName() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions(null!) + .Configure(o => o.Boolean = false) + .Validate(o => o.Boolean)) + .Build(); + + var error = await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + + ValidateFailure(error); + } + + [Fact] + public async Task CanValidateOptionsEagerThanLazySameType() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => o.Boolean = false) + .Validate(o => o.Boolean, "first Boolean must be true.") + .Services + .AddOptions() + .Configure(o => o.Boolean = true) + .Validate(o => !o.Boolean, "second Boolean must be false.")) + .Build(); + + var error = await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + + ValidateFailure(error, 1, "second Boolean must be false."); + } + + [Fact] + public async Task CanValidateOptionsLazyThanEagerSameType() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOptions() + .Configure(o => o.Boolean = false) + .Validate(o => o.Boolean, "first Boolean must be true.") + .Services + .AddValidatedOptions() + .Configure(o => o.Boolean = true) + .Validate(o => !o.Boolean, "second Boolean must be false.")) + .Build(); + + var error = await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + + ValidateFailure(error, 1, "second Boolean must be false."); + } + + [Fact] + public async Task CanValidateOptionsLazyThanEagerDifferentTypes() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOptions() + .Configure(o => o.Integer = 11) + .Validate(o => o.Integer > 12, "Integer") + .Services + .AddValidatedOptions() + .Configure(o => o.Boolean = false) + .Validate(o => o.Boolean, "first Boolean must be true.")) + .Build(); + + var error = await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + + ValidateFailure(error, 1, "first Boolean must be true."); + } + + [Fact] + public async Task CanValidateMultipleOptionsSameEagerly() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => o.Integer = 11) + .Validate(o => o.Integer > 12, "Integer") + .Services + .AddValidatedOptions() + .Configure(o => o.Boolean = false) + .Validate(o => o.Boolean, "first Boolean must be true.")) + .Build(); + + var error = await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + + ValidateFailure(error, 2, "Integer", "first Boolean must be true."); + } + + [Fact(Skip = "Flaky, see https://github.com/dotnet/r9/issues/171")] + public async Task CanValidateMultipleOptionsEagerly() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => o.Integer = 11) + .Validate(o => o.Integer > 12, "Integer") + .Services + .AddValidatedOptions() + .Configure(o => o.Boolean = false) + .Validate(o => o.Boolean, "first Boolean must be true.")) + .Build(); + + var error = await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + + Assert.Equal(2, error.InnerExceptions.Count); + + var errors = error.InnerExceptions; + + // order is not guaranteed, so deal with both possible orderings + if (errors[0].ToString().Contains("Integer")) + { + ValidateFailure((OptionsValidationException)errors[0], 1, "Integer"); + ValidateFailure((OptionsValidationException)errors[1], 1, "first Boolean must be true."); + } + else + { + ValidateFailure((OptionsValidationException)errors[0], 1, "first Boolean must be true."); + ValidateFailure((OptionsValidationException)errors[1], 1, "Integer"); + } + } + + [Fact] + public async Task CanValidateOptionsEagerThanLazyDifferentTypes() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => o.Integer = 11) + .Validate(o => o.Integer > 12, "Integer") + .Services + .AddOptions() + .Configure(o => o.Boolean = false) + .Validate(o => o.Boolean, "first Boolean must be true.")) + .Build(); + + var error = await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + + ValidateFailure(error, 1, "Integer"); + } + + [Fact] + public async Task CanValidateOptionsEagerlyWithMultipleDefaultErrors() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => + { + o.Boolean = false; + o.Integer = 11; + }) + .Validate(o => o.Boolean) + .Validate(o => o.Integer > 12)) + .Build(); + + var error = await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + + ValidateFailure(error, 2); + } + + [Fact] + public async Task CanValidateOptionEagerlysWithMixedOverloads() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => + { + o.Boolean = false; + o.Integer = 11; + o.Virtual = "wut"; + }) + .Validate(o => o.Boolean) + .Validate(o => o.Virtual == null, "Virtual") + .Validate(o => o.Integer > 12, "Integer")) + .Build(); + + var error = await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + + ValidateFailure(error, 3, "Virtual", "Integer"); + } + + [Fact] + public async Task CanValidateEagerlyDataAnnotations() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => + { + o.StringLength = "111111"; + o.IntRange = 10; + o.Custom = "nowhere"; + o.Dep1 = "Not dep2"; + }) + .ValidateDataAnnotations()) + .Build(); + + var error = await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + + var optionsName = "'" + nameof(AnnotatedOptions) + "' "; + ValidateFailure(error, 5, + $"DataAnnotation validation failed for {optionsName}members: 'Required' with the error: 'The Required field is required.'.", + $"DataAnnotation validation failed for {optionsName}members: 'StringLength' with the error: 'Too long.'.", + $"DataAnnotation validation failed for {optionsName}members: 'IntRange' with the error: 'Out of range.'.", + $"DataAnnotation validation failed for {optionsName}members: 'Custom' with the error: 'The field Custom is invalid.'.", + $"DataAnnotation validation failed for {optionsName}members: 'Dep1,Dep2' with the error: 'Dep1 != Dep2'."); + } + + [Fact] + public async Task CanValidateEagerlyMixDataAnnotations() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure( + o => + { + o.StringLength = "111111"; + o.IntRange = 10; + o.Custom = "nowhere"; + o.Dep1 = "Not dep2"; + }) + .ValidateDataAnnotations() + .Validate(o => o.Custom != "nowhere", "I don't want to go to nowhere!")) + .Build(); + + var error = await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + + var optionsName = "'" + nameof(AnnotatedOptions) + "' "; + ValidateFailure(error, 6, + $"DataAnnotation validation failed for {optionsName}members: 'Required' with the error: 'The Required field is required.'.", + $"DataAnnotation validation failed for {optionsName}members: 'StringLength' with the error: 'Too long.'.", + $"DataAnnotation validation failed for {optionsName}members: 'IntRange' with the error: 'Out of range.'.", + $"DataAnnotation validation failed for {optionsName}members: 'Custom' with the error: 'The field Custom is invalid.'.", + $"DataAnnotation validation failed for {optionsName}members: 'Dep1,Dep2' with the error: 'Dep1 != Dep2'.", + "I don't want to go to nowhere!"); + } + + [Fact] + public async Task Test_IValidationSuccessful() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => + { + o.Required = "required"; + o.StringLength = "1111"; + o.IntRange = 0; + o.Custom = "CZ"; + o.Dep1 = "dep"; + o.Dep2 = "dep"; + }) + .Validate(o => o.Custom != "nowhere", "I don't want to go to nowhere!")) + .Build(); + + var exception = await Record.ExceptionAsync(() => host.StartAndStopAsync()); + + Assert.Null(exception); + } + + [Fact(Skip = "Flaky, see https://github.com/dotnet/r9/issues/171")] + public async Task ValidateOnStart_AddOptionsMultipleTimesForSameType_AllGetTriggered() + { + bool firstOptionsBuilderTriggered = false; + bool secondOptionsBuilderTriggered = false; + + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions("bad_configuration1") + .Configure(o => o.Boolean = false) + .Validate(o => + { + firstOptionsBuilderTriggered = true; + return o.Boolean; + }, "Boolean1") + .Services + .AddValidatedOptions("bad_configuration2") + .Configure(o => + { + o.Boolean = false; + o.Integer = 11; + }) + .Validate(o => + { + secondOptionsBuilderTriggered = true; + return o.Boolean; + }, "Boolean2") + .Validate(o => o.Integer > 12, "Integer")) + .Build(); + + var error = await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + + // order is not guaranteed, so handle both possible orderings + if (error.InnerExceptions[0].ToString().Contains("Boolean1")) + { + ValidateFailure((error.InnerExceptions[0] as OptionsValidationException)!, 1, "Boolean1"); + ValidateFailure((error.InnerExceptions[1] as OptionsValidationException)!, 2, "Boolean2", "Integer"); + } + else + { + ValidateFailure((error.InnerExceptions[0] as OptionsValidationException)!, 2, "Boolean2", "Integer"); + ValidateFailure((error.InnerExceptions[1] as OptionsValidationException)!, 1, "Boolean1"); + } + + Assert.True(firstOptionsBuilderTriggered); + Assert.True(secondOptionsBuilderTriggered); + } + + [Fact] + public async Task ValidateOnStart_AddEagerValidation_DoesValidationWhenHostStartsWithNoFailure() + { + bool validateCalled = false; + + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions("correct_configuration") + .Configure(o => o.Boolean = true) + .Validate(o => + { + validateCalled = true; + return o.Boolean; + }, "correct_configuration")) + .Build(); + + await host.StartAndStopAsync(); + + Assert.True(validateCalled); + } + + [Fact] + public async Task ValidateOnStart_AddLazyValidation_SkipsValidationWhenHostStarts() + { + bool validateCalled = false; + + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions("correct_configuration") + .Configure(o => o.Boolean = true) + .Validate(o => o.Boolean, "correct_configuration") + .Services + .AddOptions("bad_configuration") + .Configure(o => o.Boolean = false) + .Validate(o => + { + validateCalled = true; + return o.Boolean; + }, "bad_configuration")) + .Build(); + + // For the lazily added "bad_configuration", validation failure does not occur when host starts + await host.StartAndStopAsync(); + + Assert.False(validateCalled); + } + + [Fact] + public async Task ValidateOnStart_AddBothLazyAndEagerValidationOnDifferentTypes_ValidatesWhenHostStartsOnlyForEagerValidations() + { + bool validateCalledForNested = false; + bool validateCalledForComplexOptions = false; + + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddOptions() + .Configure(o => o.Integer = 11) + .Validate(o => + { + validateCalledForNested = true; + return o.Integer > 12; + }, "Integer") + .Services + .AddValidatedOptions() + .Configure(o => o.Boolean = false) + .Validate(o => + { + validateCalledForComplexOptions = true; + return o.Boolean; + }, "first Boolean must be true.")) + .Build(); + + var error = await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + + ValidateFailure(error, 1, "first Boolean must be true."); + + Assert.False(validateCalledForNested); + Assert.True(validateCalledForComplexOptions); + } + + [Fact] + public async Task ValidationMechanism_Provide_Friendly_Message_To_Debug_When_ValidationException_Is_Thrown() + { + var optionsName = "SomeName"; + + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions(optionsName) + .Configure(x => + x.DeeplyComplexVal = new ComplexModel + { + ComplexVal = new Model + { + Val = 4 + } + })) + .Build(); + + var error = await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + + var failureReasons = error.Failures.ToArray(); + + Assert.Single(failureReasons); + } + + private static void ValidateFailure(OptionsValidationException e, int count = 1, params string[] errorsToMatch) + { + Assert.Equal(typeof(TOptions), e.OptionsType); + Assert.Equal(count, e.Failures.ToList().Count); + + // Check for the error in any of the failures + foreach (var error in errorsToMatch) + { +#if NETCOREAPP3_1_OR_GREATER + Assert.True(e.Failures.FirstOrDefault(predicate: f => f.Contains(error, StringComparison.Ordinal)) != null, "Did not find: " + error); +#else + Assert.True(e.Failures.FirstOrDefault(predicate: f => f.IndexOf(error, StringComparison.Ordinal) >= 0) != null, "Did not find: " + error + " " + e.Failures.First()); +#endif + } + } +} diff --git a/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/AnnotatedOptions.cs b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/AnnotatedOptions.cs new file mode 100644 index 0000000000..0582058eb4 --- /dev/null +++ b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/AnnotatedOptions.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Extensions.Options.Validation.Test; + +public class AnnotatedOptions +{ + [Required] + public string? Required { get; set; } + + [StringLength(5, ErrorMessage = "Too long.")] + public string? StringLength { get; set; } + + [Range(-5, 5, ErrorMessage = "Out of range.")] + public int IntRange { get; set; } + + [From(Accepted = "CZ")] + public string? Custom { get; set; } + + [DepValidator(Target = "Dep2")] + public string? Dep1 { get; set; } + public string? Dep2 { get; set; } +} diff --git a/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/AnotherNestedOptionsValidator.cs b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/AnotherNestedOptionsValidator.cs new file mode 100644 index 0000000000..85c7840d89 --- /dev/null +++ b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/AnotherNestedOptionsValidator.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Options.Validation.Test; + +internal class AnotherNestedOptionsValidator : IValidateOptions +{ + public const string LogMethod = "Executed Validator."; + private readonly ILogger _logger; + + public AnotherNestedOptionsValidator(ILogger logger) + { + _logger = logger; + } + + public ValidateOptionsResult Validate(string? name, NestedOptions options) + { + _logger.Log(LogLevel.Information, 0, LogMethod); + + return ValidateOptionsResult.Success; + } +} diff --git a/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/ComplexOptions.cs b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/ComplexOptions.cs new file mode 100644 index 0000000000..bd34242e3b --- /dev/null +++ b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/ComplexOptions.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Options.Validation.Test; + +public class ComplexOptions +{ +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public ComplexOptions() +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + { + Nested = new NestedOptions(); + Virtual = "complex"; + } + + public static string? StaticProperty { get; set; } + public static string? ReadOnly => null; + + public NestedOptions Nested { get; set; } + public int Integer { get; set; } + public bool Boolean { get; set; } + public virtual string Virtual { get; set; } + public object Object { get; set; } + + public string PrivateSetter { get; private set; } + public string ProtectedSetter { get; protected set; } + public string InternalSetter { get; internal set; } + internal string InternalProperty { get; set; } + internal string InternalReadOnly { get; } + + protected string ProtectedProperty { get; set; } + protected string ProtectedPrivateSet { get; private set; } + protected string ProtectedReadOnly { get; } + + [SuppressMessage("Major Code Smell", "S1144:Unused private types or members should be removed", Justification = "For testing")] + private string PrivateProperty { get; set; } + + [SuppressMessage("Major Code Smell", "S1144:Unused private types or members should be removed", Justification = "For testing")] + private string PrivateReadOnly { get; } +} diff --git a/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/DepValidatorAttribute.cs b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/DepValidatorAttribute.cs new file mode 100644 index 0000000000..3d9b6f027d --- /dev/null +++ b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/DepValidatorAttribute.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Extensions.Options.Validation.Test; + +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public sealed class DepValidatorAttribute + : ValidationAttribute +{ + public string? Target { get; set; } + + protected override ValidationResult IsValid(object? value, ValidationContext validationContext) + { + var instance = validationContext.ObjectInstance; + var type = instance.GetType(); + var dep1 = type.GetProperty("Dep1")?.GetValue(instance); + var dep2 = type.GetProperty(name: Target!)?.GetValue(instance); + + if (dep1 == dep2) + { + return ValidationResult.Success!; + } + + return new ValidationResult("Dep1 != " + Target, new[] { "Dep1", Target ?? "" }); + } +} diff --git a/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/FailingNestedOptionsValidator.cs b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/FailingNestedOptionsValidator.cs new file mode 100644 index 0000000000..7e00d6112d --- /dev/null +++ b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/FailingNestedOptionsValidator.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Options.Validation.Test; + +internal class FailingNestedOptionsValidator : IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, NestedOptions options) + => options.Integer > 0 + ? ValidateOptionsResult.Success + : ValidateOptionsResult.Fail("Validation failed for options with name: " + name); +} diff --git a/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/FromAttribute.cs b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/FromAttribute.cs new file mode 100644 index 0000000000..afbd66c3ac --- /dev/null +++ b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/FromAttribute.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Extensions.Options.Validation.Test; + +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public sealed class FromAttribute : ValidationAttribute +{ + public string? Accepted { get; set; } + + public override bool IsValid(object? value) => value?.ToString() == Accepted; +} diff --git a/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/HostStartStopExtension.cs b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/HostStartStopExtension.cs new file mode 100644 index 0000000000..81d46cfbc6 --- /dev/null +++ b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/HostStartStopExtension.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.Extensions.Options.Validation.Test; + +internal static class HostStartStopExtension +{ + public static async Task StartAndStopAsync(this IHost host) + { + try + { + await host.StartAsync(); + } + finally + { + await host.StopAsync(); + } + } +} diff --git a/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/NestedOptions.cs b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/NestedOptions.cs new file mode 100644 index 0000000000..bffd4fe447 --- /dev/null +++ b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/NestedOptions.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Options.Validation.Test; + +public class NestedOptions +{ + public int Integer { get; set; } +} diff --git a/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/NestedOptionsValidator.cs b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/NestedOptionsValidator.cs new file mode 100644 index 0000000000..4a9c2f369f --- /dev/null +++ b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/NestedOptionsValidator.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Options.Validation.Test; + +internal class NestedOptionsValidator : IValidateOptions +{ + public const string LogMethod = "Executed Validator."; + private readonly ILogger _logger; + + public NestedOptionsValidator(ILogger logger) + { + _logger = logger; + } + + public ValidateOptionsResult Validate(string? name, NestedOptions options) + { + _logger.Log(LogLevel.Information, 0, LogMethod); + + return ValidateOptionsResult.Success; + } +} diff --git a/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/OptionsValidationModels.cs b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/OptionsValidationModels.cs new file mode 100644 index 0000000000..3d2e0ebbfd --- /dev/null +++ b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/OptionsValidationModels.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Options.Validation.Test; + +#pragma warning disable SA1402 // File may only contain a single type + +[OptionsValidator] +public partial class ModelValidator : IValidateOptions +{ +} + +[OptionsValidator] +public partial class Model2Validator : IValidateOptions +{ +} + +[OptionsValidator] +public partial class ComplexModelValidator : IValidateOptions +{ +} + +[OptionsValidator] +public partial class InceptionComplexModelValidator : IValidateOptions +{ +} + +public class Model +{ + [Range(1, 3)] + public int Val { get; set; } +} + +public class Model2 +{ + [Range(1, 3)] + public int Val1 { get; set; } + + [MinLength(1)] + [MaxLength(5)] + public string? Val2 { get; set; } +} + +public class ModelWithoutOptionsValidator +{ + [Range(5, 10)] + public int Val { get; set; } +} + +public class ComplexModel +{ + [ValidateObjectMembers] + public Model? ComplexVal { get; set; } + + [ValidateObjectMembers] + public Model? ComplexValWithSameType { get; set; } + + [ValidateObjectMembers] + public ModelWithoutOptionsValidator? ValWithoutOptionsValidator { get; set; } + +#pragma warning disable R9G113 + public Model? ValWithoutRecursiveValidation { get; set; } +#pragma warning restore R9G113 +} + +public class InceptionComplexModel +{ + [Required] + [ValidateObjectMembers] + public ComplexModel? DeeplyComplexVal { get; set; } +} diff --git a/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/ThreeFailuresMultiErrorValidator.cs b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/ThreeFailuresMultiErrorValidator.cs new file mode 100644 index 0000000000..fb2ad1024d --- /dev/null +++ b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/ThreeFailuresMultiErrorValidator.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Validation = Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Options.Validation.Test.Helpers; + +public class ThreeFailuresMultiErrorValidator : IValidateOptions +{ + internal const string FirstErrorMessage = "First error message."; + internal const string SecondErrorMessage = "Second error message."; + internal const string ThirdErrorMessage = "Third error message."; + internal const string ThirdPropertyName = "ThirdProperty"; + + public ValidateOptionsResult Validate(string? name, NestedOptions options) + { + var builder = new ValidateOptionsResultBuilder(); + + builder.AddError(FirstErrorMessage); + builder.AddError(SecondErrorMessage); + builder.AddError(ThirdErrorMessage, ThirdPropertyName); + + return builder.Build(); + } +} diff --git a/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/ZeroFailuresMultiErrorValidator.cs b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/ZeroFailuresMultiErrorValidator.cs new file mode 100644 index 0000000000..16a89a54f7 --- /dev/null +++ b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Helpers/ZeroFailuresMultiErrorValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Validation = Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Options.Validation.Test.Helpers; + +public class ZeroFailuresMultiErrorValidator : IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, NestedOptions options) => new ValidateOptionsResultBuilder().Build(); +} diff --git a/test/ToBeRemoved/Options.ValidateOnStart.Tests/MultipleMessageValidatorTest.cs b/test/ToBeRemoved/Options.ValidateOnStart.Tests/MultipleMessageValidatorTest.cs new file mode 100644 index 0000000000..90773697ff --- /dev/null +++ b/test/ToBeRemoved/Options.ValidateOnStart.Tests/MultipleMessageValidatorTest.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETCOREAPP3_1_OR_GREATER +using System.Linq; +#endif +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Validation.Test.Helpers; +using Xunit; +using Validation = Microsoft.Extensions.Options.Validation; + +namespace Microsoft.Extensions.Options.Validation.Test; + +public class MultipleMessageValidatorTest +{ +#if NETCOREAPP3_1_OR_GREATER + [Fact] + public void Validator_That_Inherits_From_BaseValidator_Outputs_List_Of_Failures() + { + var validator = new ThreeFailuresMultiErrorValidator(); + + var validationResult = validator.Validate(string.Empty, new NestedOptions()); + var failures = validationResult.Failures!.ToArray(); + + Assert.True(validationResult.Failed); + Assert.Equal(3, failures.Length); + + Assert.Contains(ThreeFailuresMultiErrorValidator.FirstErrorMessage, failures[0]); + Assert.Contains(ThreeFailuresMultiErrorValidator.SecondErrorMessage, failures[1]); + Assert.Contains(ThreeFailuresMultiErrorValidator.ThirdErrorMessage, failures[2]); + Assert.Contains(ThreeFailuresMultiErrorValidator.ThirdPropertyName, failures[2]); + } + + [Fact] + public void When_Validator_Is_Not_Adding_Any_Failure_Messages_Output_Validation_Succeeds() + { + var validator = new ZeroFailuresMultiErrorValidator(); + + var validationResult = validator.Validate(string.Empty, new NestedOptions()); + + Assert.Null(validationResult.Failures); + Assert.False(validationResult.Failed); + Assert.True(validationResult.Succeeded); + } +#else + [Fact] + public void Validator_That_Inherits_From_BaseValidator_Outputs_All_Error_Messages_For_Older_Frameworks() + { + var validator = new ThreeFailuresMultiErrorValidator(); + + var validationResult = validator.Validate(string.Empty, new NestedOptions()); + + Assert.True(validationResult.Failed); + Assert.Contains(ThreeFailuresMultiErrorValidator.FirstErrorMessage, validationResult.FailureMessage); + Assert.Contains(ThreeFailuresMultiErrorValidator.SecondErrorMessage, validationResult.FailureMessage); + Assert.Contains(ThreeFailuresMultiErrorValidator.ThirdErrorMessage, validationResult.FailureMessage); + } + + [Fact] + public void When_Validator_Is_Not_Adding_Any_Failure_Messages_Output_Message_Validation_Succeeds() + { + var validator = new ZeroFailuresMultiErrorValidator(); + + var validationResult = validator.Validate(string.Empty, new NestedOptions()); + + Assert.False(validationResult.Failed); + Assert.True(validationResult.Succeeded); + + Assert.DoesNotContain(ThreeFailuresMultiErrorValidator.FirstErrorMessage, validationResult.FailureMessage); + Assert.DoesNotContain(ThreeFailuresMultiErrorValidator.SecondErrorMessage, validationResult.FailureMessage); + Assert.DoesNotContain(ThreeFailuresMultiErrorValidator.ThirdErrorMessage, validationResult.FailureMessage); + } +#endif + + [Fact] + public void Success() + { + var builder = new ValidateOptionsResultBuilder(); + var vr = builder.Build(); + Assert.True(vr.Succeeded); + } + + [Fact] + public void AddErrors_VaidationResult() + { + var builder = new ValidateOptionsResultBuilder(); + builder.AddResults(new[] + { + new ValidationResult("FAIL1"), + new ValidationResult("FAIL2"), + }); + + var vr = builder.Build(); + var failures = vr.FailureMessage!.Split(';'); + + Assert.True(vr.Failed); + Assert.Equal(2, failures.Length); + Assert.Contains("FAIL1", failures[0]); + Assert.Contains("FAIL2", failures[1]); + } + +#if NETCOREAPP3_1_OR_GREATER + [Fact] + public void AddErrors_VaidateOptionsResult() + { + var builder = new ValidateOptionsResultBuilder(); + builder.AddResult(ValidateOptionsResult.Fail(new[] { "FAIL1", "FAIL2" })); + + var vr = builder.Build(); + var failures = vr.FailureMessage!.Split(';'); + + Assert.True(vr.Failed); + Assert.Equal(2, failures.Length); + Assert.Contains("FAIL1", failures[0]); + Assert.Contains("FAIL2", failures[1]); + } +#endif + + [Fact] + public void NonDefaultContructor() + { + var builder = new ValidateOptionsResultBuilder(); + builder.AddError("A"); + builder.AddError("B"); + builder.AddError("C"); + var vr = builder.Build(); + var failures = vr.FailureMessage!.Split(';'); + + Assert.True(vr.Failed); + Assert.Equal(3, failures.Length); + Assert.Contains("A", failures[0]); + Assert.Contains("B", failures[1]); + Assert.Contains("C", failures[2]); + } +} diff --git a/test/ToBeRemoved/Options.ValidateOnStart.Tests/Options.ValidateOnStart.Tests.csproj b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Options.ValidateOnStart.Tests.csproj new file mode 100644 index 0000000000..aadeb05c93 --- /dev/null +++ b/test/ToBeRemoved/Options.ValidateOnStart.Tests/Options.ValidateOnStart.Tests.csproj @@ -0,0 +1,20 @@ + + + Microsoft.Extensions.Options.ValidateOnStart.Tests + Microsoft.Extensions.Options.ValidateOnStart.Test + Tests for Microsoft.Extensions.Options.ValidateOnStart + true + + + + + + + + + + + + + + diff --git a/test/ToBeRemoved/Options.ValidateOnStart.Tests/OptionsBuilderExtensionsTests.cs b/test/ToBeRemoved/Options.ValidateOnStart.Tests/OptionsBuilderExtensionsTests.cs new file mode 100644 index 0000000000..9bcb156d86 --- /dev/null +++ b/test/ToBeRemoved/Options.ValidateOnStart.Tests/OptionsBuilderExtensionsTests.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Options.Validation.Test; + +public class OptionsBuilderExtensionsTests +{ + [Fact] + public void AddValidatedOptions_Throws_WhenNullServices() + { + Assert.Throws(() => OptionsBuilderExtensions.AddValidatedOptions(null!)); + } + + [Theory] + [InlineData(1, 0, false, true)] + [InlineData(0, 1, true, false)] + [InlineData(2, 2, false, false)] + [InlineData(0, 0, true, true)] + public async Task OptionsBuilderExtensions_HandlesNamedOptionsProperly(int namedValue, int unnamedValue, bool namedFails, bool unnamedFails) + { + const string OptionsName = "Named options"; + + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddLogging() + .AddValidatedOptions(OptionsName) + .Configure(c => c.Integer = namedValue) + .Services + .AddValidatedOptions() + .Configure(c => c.Integer = unnamedValue)) + .Build(); + + var ex = await Record.ExceptionAsync(() => host.StartAndStopAsync()); + + if (!namedFails && !unnamedFails) + { + Assert.Null(ex); + return; + } + + if (namedFails && unnamedFails) + { + Assert.IsType(ex); + Assert.Equal(2, ((AggregateException)ex).InnerExceptions.Count); + } + else + { + if (namedFails) + { + Assert.IsType(ex); + Assert.Single(((OptionsValidationException)ex).Failures); + Assert.EndsWith(OptionsName, ((OptionsValidationException)ex).Failures.First()); + Assert.Equal(OptionsName, ((OptionsValidationException)ex).OptionsName); + } + else + { + Assert.IsType(ex); + Assert.Single(((OptionsValidationException)ex).Failures); + Assert.EndsWith(Microsoft.Extensions.Options.Options.DefaultName, ((OptionsValidationException)ex).Failures.First()); + Assert.Equal(Microsoft.Extensions.Options.Options.DefaultName, ((OptionsValidationException)ex).OptionsName); + } + } + } +} diff --git a/test/ToBeRemoved/Options.ValidateOnStart.Tests/OptionsValidatorExtensionsTest.cs b/test/ToBeRemoved/Options.ValidateOnStart.Tests/OptionsValidatorExtensionsTest.cs new file mode 100644 index 0000000000..e3758ea9e0 --- /dev/null +++ b/test/ToBeRemoved/Options.ValidateOnStart.Tests/OptionsValidatorExtensionsTest.cs @@ -0,0 +1,298 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Xunit; + +#if NETCOREAPP3_1_OR_GREATER +// On newer frameworks use .NET's version of options validation exception. +using OptionsValidationException = Microsoft.Extensions.Options.OptionsValidationException; +#else +using Microsoft.Extensions.Options; +#endif + +namespace Microsoft.Extensions.Options.Validation.Test; + +public class OptionsValidatorExtensionsTest +{ + [Fact] + public async Task ShouldValidateOnStartSuccess() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => o.Val = 2)) + .Build(); + + var ex = await Record.ExceptionAsync(() => host.StartAndStopAsync()); + Assert.Null(ex); + } + + [Fact] + public async Task ShouldValidateOnStartFailure() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => o.Val = 0)) + .Build(); + + await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + } + + [Fact] + public async Task ValidationHostedService_GivenDataAnnotatedOptionsFailure_ThrowsOptionsValidationException() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => o.Dep1 = "") + .ValidateDataAnnotations()) + .Build(); + + await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + } + + [Fact] + public async Task ShouldValidateOnStartMultipleModelsSuccess() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => + { + o.Val1 = 2; + o.Val2 = "ab"; + }) + .Services + .AddValidatedOptions() + .Configure(o => o.Val = 2)) + .Build(); + + var ex = await Record.ExceptionAsync(() => host.StartAndStopAsync()); + + Assert.Null(ex); + } + + [Fact] + public async Task ShouldValidateOnStartOneOfModelsFailure() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => + { + o.Val1 = 2; + o.Val2 = "abcdef"; + }) + .Services + .AddValidatedOptions() + .Configure(o => o.Val = 2)) + .Build(); + + await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + } + + [Fact] + public async Task ShouldValidateOnStartOneOfModelsFailure_WithName() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions("bad_configuration") + .Configure(o => + { + o.Val1 = 2; + o.Val2 = "abcdef"; + }) + .Services + .AddValidatedOptions() + .Configure(o => o.Val = 2)) + .Build(); + + await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + } + + [Fact] + public async Task ShouldValidateOnStartOneOfModelsFailure_WithNullName() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions(null!) + .Configure(o => + { + o.Val1 = 2; + o.Val2 = "abcdef"; + }) + .Services + .AddValidatedOptions() + .Configure(o => o.Val = 2)) + .Build(); + + await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + } + + [Fact] + public async Task ShouldValidateOnStartAllModelsFailure() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => + { + o.Val1 = 2; + o.Val2 = "abcdef"; + }) + .Services + .AddValidatedOptions() + .Configure(o => o.Val = 4)) + .Build(); + + var aggregateException = await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + + Assert.Equal(2, aggregateException.InnerExceptions.Count); + Assert.IsAssignableFrom(aggregateException.InnerExceptions[0]); + Assert.IsAssignableFrom(aggregateException.InnerExceptions[1]); + } + + [Fact] + public async Task ShouldValidateTransitivelyOnStartSuccessfully() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => + { + o.ComplexVal = new Model + { + Val = 2 + }; + + o.ValWithoutOptionsValidator = new ModelWithoutOptionsValidator + { + Val = 6 + }; + + o.ValWithoutRecursiveValidation = new Model + { + Val = -1 + }; + })) + .Build(); + + var ex = await Record.ExceptionAsync(async () => await host.StartAndStopAsync()); + Assert.Null(ex); + } + + [Fact] + public async Task ShouldValidateInnerNullModelRecursivelyOnStartSuccessfully() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => + { + o.ComplexVal = null; + o.ValWithoutOptionsValidator = null; + o.ValWithoutRecursiveValidation = null; + })) + .Build(); + + var ex = await Record.ExceptionAsync(() => host.StartAndStopAsync()); + + Assert.Null(ex); + } + + [Fact] + public async Task ShouldValidateTransitivelyOnStartWithFailure() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => o.ComplexVal = new Model + { + Val = 0 + })) + .Build(); + + await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + } + + [Fact] + public async Task ShouldValidateTransitivelyWithoutOptionsValidatorWithFailure() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => o.ValWithoutOptionsValidator = new ModelWithoutOptionsValidator + { + Val = 0 + })) + .Build(); + + await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + } + + [Fact] + public async Task ShouldValidateDeepRecursionOnStartSuccessfully() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => o.DeeplyComplexVal = new ComplexModel + { + ComplexVal = new Model + { + Val = 2 + }, + + ComplexValWithSameType = new Model + { + Val = 1 + }, + + ValWithoutOptionsValidator = new ModelWithoutOptionsValidator + { + Val = 9 + } + })) + .Build(); + + var ex = await Record.ExceptionAsync(() => host.StartAndStopAsync()); + + Assert.Null(ex); + } + + [Fact] + public async Task ShouldValidateDeepRecursionOnStartWithFailure() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => o.DeeplyComplexVal = new ComplexModel + { + ComplexVal = new Model + { + Val = 0 + } + })) + .Build(); + + await Assert.ThrowsAsync(async () => await host.StartAndStopAsync()); + } + + [Fact] + public async Task ShouldValidateInnerDeepNullModelRecursionOnStartWithFailure() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddValidatedOptions() + .Configure(o => o.DeeplyComplexVal = null)) + .Build(); + + await Assert.ThrowsAsync(() => host.StartAndStopAsync()); + } +} diff --git a/test/ToBeRemoved/Options.ValidateOnStart.Tests/ValidationHostedServiceTests.cs b/test/ToBeRemoved/Options.ValidateOnStart.Tests/ValidationHostedServiceTests.cs new file mode 100644 index 0000000000..8ab33fd539 --- /dev/null +++ b/test/ToBeRemoved/Options.ValidateOnStart.Tests/ValidationHostedServiceTests.cs @@ -0,0 +1,126 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !NET6_0_OR_GREATER + +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Xunit; +using Opt = Microsoft.Extensions.Options.Options; + +namespace Microsoft.Extensions.Options.Validation.Test; + +public class ValidationHostedServiceTests +{ + [Fact] + public void ValidationHostedService_Throws_Exception_When_Provided_Options_Are_Invalid() + { + var options = Opt.Create(new ValidatorOptions()); + + Assert.Throws(() => new ValidationHostedService(options)); + Assert.Throws(() => new ValidationHostedService(Opt.Create(null!))); + } + + [Fact] + public async Task Validation_Throws_WhenValidatorThrows() + { + var options = Opt.Create(new ValidatorOptions()); + options.Value.Validators[(typeof(object), string.Empty)] = () => throw new ValidationException(); + var service = new ValidationHostedService(options); + var ex = await Assert.ThrowsAsync(() => service.StartAsync(CancellationToken.None)); + Assert.NotEmpty(ex.Failures); + + await service.StopAsync(CancellationToken.None); + } + + [Fact] + public async Task Validation_Throws_AggregateExc_WhenMultipleValidators() + { + var options = Opt.Create(new ValidatorOptions()); + options.Value.Validators[(typeof(object), string.Empty)] = () => throw new ValidationException(); + options.Value.Validators[(typeof(string), string.Empty)] = () => throw new ValidationException(); + var service = new ValidationHostedService(options); + await Assert.ThrowsAsync(() => service.StartAsync(CancellationToken.None)); + + await service.StopAsync(CancellationToken.None); + } + + [Fact] + public async Task Validation_Throws_WhenTokenIsCancelled() + { + var options = Opt.Create(new ValidatorOptions()); + options.Value.Validators[(typeof(object), string.Empty)] = () => throw new ValidationException(); + options.Value.Validators[(typeof(string), string.Empty)] = () => throw new ValidationException(); + var service = new ValidationHostedService(options); + + using var tokenSource = new CancellationTokenSource(); + tokenSource.Cancel(); + + await Assert.ThrowsAsync(() => service.StartAsync(tokenSource.Token)); + + await service.StopAsync(CancellationToken.None); + } + + [Fact] + public async Task ValidatorHostedService_Fills_Invalid_Members_In_Friendly_Message() + { + const string OptionsName = "Jack Sparrow"; + + const string FailureMember1 = "Camembert1"; + const string FailureMember2 = "Camembert2"; + const string FailureMember3 = "Camembert3"; + + const string ErrorMessage = "I will fail forever :(."; + + var options = Opt.Create(new ValidatorOptions()); + options.Value.Validators[(typeof(object), OptionsName)] = () => throw new ValidationException( + validationResult: new ValidationResult(ErrorMessage, new[] { FailureMember1, FailureMember2, FailureMember3 }), new RangeAttribute(0, 10), new object()); + + var service = new ValidationHostedService(options); + + var error = await Assert.ThrowsAsync(() => service.StartAsync(CancellationToken.None)); + var failureReasons = error.Failures.ToArray(); + + Assert.Single(failureReasons); + + Assert.Contains(typeof(object).FullName!, failureReasons[0]); + Assert.Contains(OptionsName, failureReasons[0]); + Assert.Contains(ErrorMessage, failureReasons[0]); + Assert.Contains(FailureMember1, failureReasons[0]); + Assert.Contains(FailureMember2, failureReasons[0]); + Assert.Contains(FailureMember3, failureReasons[0]); + + await service.StopAsync(CancellationToken.None); + } + + [Fact] + public async Task ValidatorHostedService_Fills_Failed_Members_To_Be_Unknown_When_Not_Provided() + { + const string OptionsName = "Jack Sparrow"; + + const string ErrorMessage = "I will fail forever :(."; + + var options = Opt.Create(new ValidatorOptions()); + options.Value.Validators[(typeof(object), OptionsName)] = () => throw new ValidationException( + validationResult: new ValidationResult(ErrorMessage, Array.Empty()), new RangeAttribute(0, 10), new object()); + + var service = new ValidationHostedService(options); + + var error = await Assert.ThrowsAsync(() => service.StartAsync(CancellationToken.None)); + var failureReasons = error.Failures.ToArray(); + + Assert.Single(failureReasons); + + Assert.Contains(typeof(object).FullName!, failureReasons[0]); + Assert.Contains(OptionsName, failureReasons[0]); + Assert.Contains(ErrorMessage, failureReasons[0]); + Assert.Contains(ValidationHostedService.Unknown, failureReasons[0]); + + await service.StopAsync(CancellationToken.None); + } +} +#endif diff --git a/testEnvironments.json b/testEnvironments.json new file mode 100644 index 0000000000..66b97ba577 --- /dev/null +++ b/testEnvironments.json @@ -0,0 +1,10 @@ +{ + "version": "1", + "environments": [ + { + "name": "WSL Ubuntu", + "type": "wsl", + "wslDistribution": "Ubuntu" + } + ] +}