diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 0000000..6eff044
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,24 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "powershell": {
+ "version": "7.4.1",
+ "commands": [
+ "pwsh"
+ ]
+ },
+ "dotnet-coverage": {
+ "version": "17.10.2",
+ "commands": [
+ "dotnet-coverage"
+ ]
+ },
+ "nbgv": {
+ "version": "3.6.133",
+ "commands": [
+ "nbgv"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 0000000..b680b1d
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,14 @@
+# Refer to https://hub.docker.com/_/microsoft-dotnet-sdk for available versions
+FROM mcr.microsoft.com/dotnet/sdk:8.0.100-jammy
+
+# Installing mono makes `dotnet test` work without errors even for net472.
+# But installing it takes a long time, so it's excluded by default.
+#RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF
+#RUN echo "deb https://download.mono-project.com/repo/ubuntu stable-bionic main" | tee /etc/apt/sources.list.d/mono-official-stable.list
+#RUN apt-get update
+#RUN DEBIAN_FRONTEND=noninteractive apt-get install -y mono-devel
+
+# Clear the NUGET_XMLDOC_MODE env var so xml api doc files get unpacked, allowing a rich experience in Intellisense.
+# See https://github.com/dotnet/dotnet-docker/issues/2790 for a discussion on this, where the prioritized use case
+# was *not* devcontainers, sadly.
+ENV NUGET_XMLDOC_MODE=
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..f4e3b31
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,20 @@
+{
+ "name": "Dev space",
+ "dockerFile": "Dockerfile",
+ "settings": {
+ "terminal.integrated.shell.linux": "/usr/bin/pwsh"
+ },
+ "postCreateCommand": "./init.ps1 -InstallLocality machine",
+ "extensions": [
+ "ms-azure-devops.azure-pipelines",
+ "ms-dotnettools.csharp",
+ "k--kato.docomment",
+ "editorconfig.editorconfig",
+ "pflannery.vscode-versionlens",
+ "davidanson.vscode-markdownlint",
+ "dotjoshjohnson.xml",
+ "ms-vscode-remote.remote-containers",
+ "ms-azuretools.vscode-docker",
+ "ms-vscode.powershell"
+ ]
+}
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..ffae180
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,188 @@
+# EditorConfig is awesome:http://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Don't use tabs for indentation.
+[*]
+indent_style = space
+
+# (Please don't specify an indent_size here; that has too many unintended consequences.)
+
+[*.yml]
+indent_size = 2
+indent_style = space
+
+# Code files
+[*.{cs,csx,vb,vbx,h,cpp,idl}]
+indent_size = 4
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+# MSBuild project files
+[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,msbuildproj,props,targets}]
+indent_size = 2
+
+# Xml config files
+[*.{ruleset,config,nuspec,resx,vsixmanifest,vsct,runsettings}]
+indent_size = 2
+
+# JSON files
+[*.json]
+indent_size = 2
+indent_style = space
+
+[*.ps1]
+indent_style = space
+indent_size = 4
+
+# Dotnet code style settings:
+[*.{cs,vb}]
+# Sort using and Import directives with System.* appearing first
+dotnet_sort_system_directives_first = true
+dotnet_separate_import_directive_groups = false
+dotnet_style_qualification_for_field = true:warning
+dotnet_style_qualification_for_property = true:warning
+dotnet_style_qualification_for_method = true:warning
+dotnet_style_qualification_for_event = true:warning
+
+# Use language keywords instead of framework type names for type references
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+
+# Suggest more modern language features when available
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+
+# Non-private static fields are PascalCase
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
+
+dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
+dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected internal, private protected
+dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
+
+dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
+
+# Constants are PascalCase
+dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants
+dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style
+
+dotnet_naming_symbols.constants.applicable_kinds = field, local
+dotnet_naming_symbols.constants.required_modifiers = const
+
+dotnet_naming_style.constant_style.capitalization = pascal_case
+
+# Static fields are camelCase
+dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion
+dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields
+dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style
+
+dotnet_naming_symbols.static_fields.applicable_kinds = field
+dotnet_naming_symbols.static_fields.required_modifiers = static
+
+dotnet_naming_style.static_field_style.capitalization = camel_case
+
+# Instance fields are camelCase
+dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion
+dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields
+dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style
+
+dotnet_naming_symbols.instance_fields.applicable_kinds = field
+
+dotnet_naming_style.instance_field_style.capitalization = camel_case
+
+# Locals and parameters are camelCase
+dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion
+dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters
+dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style
+
+dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local
+
+dotnet_naming_style.camel_case_style.capitalization = camel_case
+
+# Local functions are PascalCase
+dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions
+dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style
+
+dotnet_naming_symbols.local_functions.applicable_kinds = local_function
+
+dotnet_naming_style.local_function_style.capitalization = pascal_case
+
+# By default, name items with PascalCase
+dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members
+dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style
+
+dotnet_naming_symbols.all_members.applicable_kinds = *
+
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+
+# CSharp code style settings:
+[*.cs]
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = flush_left
+
+# Prefer "var" everywhere
+csharp_style_var_for_built_in_types = false
+csharp_style_var_when_type_is_apparent = true:suggestion
+csharp_style_var_elsewhere = false:warning
+
+# Prefer method-like constructs to have a block body
+csharp_style_expression_bodied_methods = false:none
+csharp_style_expression_bodied_constructors = false:none
+csharp_style_expression_bodied_operators = false:none
+
+# Prefer property-like constructs to have an expression-body
+csharp_style_expression_bodied_properties = true:none
+csharp_style_expression_bodied_indexers = true:none
+csharp_style_expression_bodied_accessors = true:none
+
+# Suggest more modern language features when available
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Newline settings
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+
+# Blocks are allowed
+csharp_prefer_braces = true:silent
+
+# SA1130: Use lambda syntax
+dotnet_diagnostic.SA1130.severity = silent
+
+# IDE1006: Naming Styles - StyleCop handles these for us
+dotnet_diagnostic.IDE1006.severity = none
+
+dotnet_diagnostic.DOC100.severity = silent
+dotnet_diagnostic.DOC104.severity = warning
+dotnet_diagnostic.DOC105.severity = warning
+dotnet_diagnostic.DOC106.severity = warning
+dotnet_diagnostic.DOC107.severity = warning
+dotnet_diagnostic.DOC108.severity = warning
+dotnet_diagnostic.DOC200.severity = warning
+dotnet_diagnostic.DOC202.severity = warning
+
+# CA1062: Validate arguments of public methods
+dotnet_diagnostic.CA1062.severity = warning
+
+[*.sln]
+indent_style = tab
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..1f35e68
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,70 @@
+###############################################################################
+# Set default behavior to automatically normalize line endings.
+###############################################################################
+* text=auto
+
+# Ensure shell scripts use LF line endings (linux only accepts LF)
+*.sh eol=lf
+*.ps1 eol=lf
+
+# The macOS codesign tool is extremely picky, and requires LF line endings.
+*.plist eol=lf
+
+###############################################################################
+# Set default behavior for command prompt diff.
+#
+# This is need for earlier builds of msysgit that does not have it on by
+# default for csharp files.
+# Note: This is only used by command line
+###############################################################################
+#*.cs diff=csharp
+
+###############################################################################
+# Set the merge driver for project and solution files
+#
+# Merging from the command prompt will add diff markers to the files if there
+# are conflicts (Merging from VS is not affected by the settings below, in VS
+# the diff markers are never inserted). Diff markers may cause the following
+# file extensions to fail to load in VS. An alternative would be to treat
+# these files as binary and thus will always conflict and require user
+# intervention with every merge. To do so, just uncomment the entries below
+###############################################################################
+#*.sln merge=binary
+#*.csproj merge=binary
+#*.vbproj merge=binary
+#*.vcxproj merge=binary
+#*.vcproj merge=binary
+#*.dbproj merge=binary
+#*.fsproj merge=binary
+#*.lsproj merge=binary
+#*.wixproj merge=binary
+#*.modelproj merge=binary
+#*.sqlproj merge=binary
+#*.wwaproj merge=binary
+
+###############################################################################
+# behavior for image files
+#
+# image files are treated as binary by default.
+###############################################################################
+#*.jpg binary
+#*.png binary
+#*.gif binary
+
+###############################################################################
+# diff behavior for common document formats
+#
+# Convert binary document formats to text before diffing them. This feature
+# is only available from the command line. Turn it on by uncommenting the
+# entries below.
+###############################################################################
+#*.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
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..63e3e89
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,9 @@
+# Please see the documentation for all configuration options:
+# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
+
+version: 2
+updates:
+- package-ecosystem: nuget
+ directory: /
+ schedule:
+ interval: weekly
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..3350282
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,115 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - main
+ - validate/*
+ pull_request:
+
+env:
+ DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
+ BUILDCONFIGURATION: Release
+ # codecov_token: 4dc9e7e2-6b01-4932-a180-847b52b43d35 # Get a new one from https://codecov.io/
+ NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages/
+
+jobs:
+ build:
+
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-20.04
+ - macos-latest
+ - windows-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
+ - name: โ Install prerequisites
+ run: |
+ ./init.ps1 -UpgradePrerequisites
+ dotnet --info
+
+ # Print mono version if it is present.
+ if (Get-Command mono -ErrorAction SilentlyContinue) {
+ mono --version
+ }
+ shell: pwsh
+ - name: โ๏ธ Set pipeline variables based on source
+ run: azure-pipelines/variables/_pipelines.ps1
+ shell: pwsh
+ - name: ๐ build
+ run: dotnet build -t:build,pack --no-restore -c ${{ env.BUILDCONFIGURATION }} -warnaserror /bl:"${{ runner.temp }}/_artifacts/build_logs/build.binlog"
+ - name: ๐งช test
+ run: azure-pipelines/dotnet-test-cloud.ps1 -Configuration ${{ env.BUILDCONFIGURATION }} -Agent ${{ runner.os }}
+ shell: pwsh
+ - name: โ Update pipeline variables based on build outputs
+ run: azure-pipelines/variables/_pipelines.ps1
+ shell: pwsh
+ - name: ๐ฅ Collect artifacts
+ run: azure-pipelines/artifacts/_stage_all.ps1
+ shell: pwsh
+ if: always()
+ - name: ๐ข Upload project.assets.json files
+ if: always()
+ uses: actions/upload-artifact@v1
+ with:
+ name: projectAssetsJson-${{ runner.os }}
+ path: ${{ runner.temp }}/_artifacts/projectAssetsJson
+ continue-on-error: true
+ - name: ๐ข Upload variables
+ uses: actions/upload-artifact@v1
+ with:
+ name: variables-${{ runner.os }}
+ path: ${{ runner.temp }}/_artifacts/Variables
+ continue-on-error: true
+ - name: ๐ข Upload build_logs
+ if: always()
+ uses: actions/upload-artifact@v1
+ with:
+ name: build_logs-${{ runner.os }}
+ path: ${{ runner.temp }}/_artifacts/build_logs
+ continue-on-error: true
+ - name: ๐ข Upload test_logs
+ if: always()
+ uses: actions/upload-artifact@v1
+ with:
+ name: test_logs-${{ runner.os }}
+ path: ${{ runner.temp }}/_artifacts/test_logs
+ continue-on-error: true
+ - name: ๐ข Upload testResults
+ if: always()
+ uses: actions/upload-artifact@v1
+ with:
+ name: testResults-${{ runner.os }}
+ path: ${{ runner.temp }}/_artifacts/testResults
+ continue-on-error: true
+ - name: ๐ข Upload coverageResults
+ if: always()
+ uses: actions/upload-artifact@v1
+ with:
+ name: coverageResults-${{ runner.os }}
+ path: ${{ runner.temp }}/_artifacts/coverageResults
+ continue-on-error: true
+ - name: ๐ข Upload symbols
+ uses: actions/upload-artifact@v1
+ with:
+ name: symbols-${{ runner.os }}
+ path: ${{ runner.temp }}/_artifacts/symbols
+ continue-on-error: true
+ - name: ๐ข Upload deployables
+ uses: actions/upload-artifact@v1
+ with:
+ name: deployables-${{ runner.os }}
+ path: ${{ runner.temp }}/_artifacts/deployables
+ if: always()
+ - name: ๐ข Publish code coverage results to codecov.io
+ run: ./azure-pipelines/publish-CodeCov.ps1 -CodeCovToken "${{ env.codecov_token }}" -PathToCodeCoverage "${{ runner.temp }}/_artifacts/coverageResults" -Name "${{ runner.os }} Coverage Results" -Flags "${{ runner.os }}Host,${{ env.BUILDCONFIGURATION }}"
+ shell: pwsh
+ timeout-minutes: 3
+ continue-on-error: true
+ if: always()
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..69599b8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,354 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+*.lutconfig
+launchSettings.json
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+!Directory.Build.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# 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
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+/coveragereport/
+
+# 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
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# 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
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# 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
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# dotnet tool local install directory
+.store/
+
+# mac-created file to track user view preferences for a directory
+.DS_Store
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..ca3a2aa
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,19 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
+ // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
+ // List of extensions which should be recommended for users of this workspace.
+ "recommendations": [
+ "ms-azure-devops.azure-pipelines",
+ "ms-dotnettools.csharp",
+ "k--kato.docomment",
+ "editorconfig.editorconfig",
+ "pflannery.vscode-versionlens",
+ "davidanson.vscode-markdownlint",
+ "dotjoshjohnson.xml",
+ "ms-vscode-remote.remote-containers",
+ "ms-azuretools.vscode-docker",
+ "tintoy.msbuild-project-tools"
+ ],
+ // List of extensions recommended by VS Code that should not be recommended for users of this workspace.
+ "unwantedRecommendations": []
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..a6e4859
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,14 @@
+{
+ // Use IntelliSense to find out which attributes exist for C# debugging
+ // Use hover for the description of the existing attributes
+ // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": ".NET Core Attach",
+ "type": "coreclr",
+ "request": "attach",
+ "processId": "${command:pickProcess}"
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..ce72437
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,12 @@
+{
+ "files.trimTrailingWhitespace": true,
+ "files.insertFinalNewline": true,
+ "files.trimFinalNewlines": true,
+ "omnisharp.enableEditorConfigSupport": true,
+ "omnisharp.enableRoslynAnalyzers": true,
+ "dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true,
+ "editor.formatOnSave": true,
+ "[xml]": {
+ "editor.wordWrap": "off"
+ }
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..67b0618
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,17 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "${workspaceFolder}",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "problemMatcher": "$msCompile"
+ }
+ ]
+}
diff --git a/Apply-Template.ps1 b/Apply-Template.ps1
new file mode 100644
index 0000000..42ed336
--- /dev/null
+++ b/Apply-Template.ps1
@@ -0,0 +1,42 @@
+#!/usr/bin/env pwsh
+
+<#
+.SYNOPSIS
+Applies the template to another repo in a semi-destructive way.
+Always apply to a clean working copy so that undesired updates can be easily reverted.
+.PARAMETER Path
+The path to the root of the repo to be updated with the latest version of this template.
+#>
+
+[CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium')]
+Param(
+ [Parameter(Mandatory=$true)]
+ [ValidateScript({Test-Path $_})]
+ [string]$Path
+)
+
+Push-Location $Path
+try {
+ # Look for our own initial commit in the target repo's history.
+ # If it's there, they've already switched to using git merge to freshen up.
+ # Using Apply-Template would just complicate future merges, so block it.
+ git merge-base --is-ancestor 05f49ce799c1f9cc696d53eea89699d80f59f833 HEAD | Out-Null
+ if ($LASTEXITCODE -eq 0) {
+ Write-Error 'The target repo already has Library.Template history merged into it. Use `git merge` instead of this script to freshen your repo. See the README.md file for details.'
+ exit 1
+ }
+} finally {
+ Pop-Location
+}
+
+Write-Host "Updating $Path"
+robocopy /mir $PSScriptRoot/azure-pipelines $Path/azure-pipelines
+robocopy /mir $PSScriptRoot/.config $Path/.config
+robocopy /mir $PSScriptRoot/.devcontainer $Path/.devcontainer
+robocopy /mir $PSScriptRoot/.github $Path/.github
+robocopy /mir $PSScriptRoot/.vscode $Path/.vscode
+robocopy /mir $PSScriptRoot/tools $Path/tools
+robocopy $PSScriptRoot $Path Directory.Build.* Directory.Packages.props global.json init.* azure-pipelines.yml .gitignore .gitattributes .editorconfig
+robocopy $PSScriptRoot/src $Path/src Directory.Build.* .editorconfig AssemblyInfo.cs
+robocopy $PSScriptRoot/test $Path/test Directory.Build.* .editorconfig
+Remove-Item $Path/azure-pipelines/expand-template.yml
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..e56631d
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,42 @@
+# Contributing
+
+This project has adopted the [Microsoft Open Source Code of
+Conduct](https://opensource.microsoft.com/codeofconduct/).
+For more information see the [Code of Conduct
+FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
+contact [opencode@microsoft.com](mailto:opencode@microsoft.com)
+with any additional questions or comments.
+
+## Best practices
+
+* Use Windows PowerShell or [PowerShell Core][pwsh] (including on Linux/OSX) to run .ps1 scripts.
+ Some scripts set environment variables to help you, but they are only retained if you use PowerShell as your shell.
+
+## Prerequisites
+
+All dependencies can be installed by running the `init.ps1` script at the root of the repository
+using Windows PowerShell or [PowerShell Core][pwsh] (on any OS).
+Some dependencies installed by `init.ps1` may only be discoverable from the same command line environment the init script was run from due to environment variables, so be sure to launch Visual Studio or build the repo from that same environment.
+Alternatively, run `init.ps1 -InstallLocality Machine` (which may require elevation) in order to install dependencies at machine-wide locations so Visual Studio and builds work everywhere.
+
+The only prerequisite for building, testing, and deploying from this repository
+is the [.NET SDK](https://get.dot.net/).
+You should install the version specified in `global.json` or a later version within
+the same major.minor.Bxx "hundreds" band.
+For example if 2.2.300 is specified, you may install 2.2.300, 2.2.301, or 2.2.310
+while the 2.2.400 version would not be considered compatible by .NET SDK.
+See [.NET Core Versioning](https://docs.microsoft.com/dotnet/core/versions/) for more information.
+
+## Package restore
+
+The easiest way to restore packages may be to run `init.ps1` which automatically authenticates
+to the feeds that packages for this repo come from, if any.
+`dotnet restore` or `nuget restore` also work but may require extra steps to authenticate to any applicable feeds.
+
+## Building
+
+This repository can be built on Windows, Linux, and OSX.
+
+Building, testing, and packing this repository can be done by using the standard dotnet CLI commands (e.g. `dotnet build`, `dotnet test`, `dotnet pack`, etc.).
+
+[pwsh]: https://docs.microsoft.com/powershell/scripting/install/installing-powershell?view=powershell-6
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..e42ac60
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,77 @@
+
+
+
+ Debug
+ $(MSBuildThisFileDirectory)
+ $(RepoRootPath)obj\$([MSBuild]::MakeRelative($(RepoRootPath), $(MSBuildProjectDirectory)))\
+ $(RepoRootPath)bin\$(MSBuildProjectName)\
+ $(RepoRootPath)bin\Packages\$(Configuration)\
+ 12
+ enable
+ enable
+ latest
+ true
+ true
+ true
+
+
+ true
+
+
+
+ false
+
+
+ $(MSBuildThisFileDirectory)
+
+
+ embedded
+
+ true
+ $(MSBuildThisFileDirectory)strongname.snk
+
+
+ COMPANY-PLACEHOLDER
+ COMPANY-PLACEHOLDER
+ ยฉ COMPANY-PLACEHOLDER. All rights reserved.
+ MIT
+ true
+ true
+ true
+ snupkg
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(PackageProjectUrl)/releases/tag/v$(Version)
+
+
+
+
+ false
+ true
+
+
+
+
+ false
+ false
+ false
+ false
+
+
diff --git a/Directory.Build.rsp b/Directory.Build.rsp
new file mode 100644
index 0000000..9a833a0
--- /dev/null
+++ b/Directory.Build.rsp
@@ -0,0 +1,16 @@
+#------------------------------------------------------------------------------
+# This file contains command-line options that MSBuild will process as part of
+# every build, unless the "/noautoresponse" switch is specified.
+#
+# MSBuild processes the options in this file first, before processing the
+# options on the command line. As a result, options on the command line can
+# override the options in this file. However, depending on the options being
+# set, the overriding can also result in conflicts.
+#
+# NOTE: The "/noautoresponse" switch cannot be specified in this file, nor in
+# any response file that is referenced by this file.
+#------------------------------------------------------------------------------
+/nr:false
+/m
+/verbosity:minimal
+/clp:Summary;ForceNoAlign
diff --git a/Directory.Build.targets b/Directory.Build.targets
new file mode 100644
index 0000000..cc8184a
--- /dev/null
+++ b/Directory.Build.targets
@@ -0,0 +1,12 @@
+
+
+
+
+ false
+
+
+
+
+
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..1438548
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,23 @@
+
+
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Expand-Template.cmd b/Expand-Template.cmd
new file mode 100644
index 0000000..970285c
--- /dev/null
+++ b/Expand-Template.cmd
@@ -0,0 +1,4 @@
+@echo off
+SETLOCAL
+set PS1UnderCmd=1
+powershell.exe -NoProfile -NoLogo -ExecutionPolicy bypass -Command "try { & '%~dpn0.ps1' %*; exit $LASTEXITCODE } catch { write-host $_; exit 1 }"
diff --git a/Expand-Template.ps1 b/Expand-Template.ps1
new file mode 100755
index 0000000..d203d85
--- /dev/null
+++ b/Expand-Template.ps1
@@ -0,0 +1,217 @@
+#!/usr/bin/env pwsh
+
+<#
+.SYNOPSIS
+Expands this template into an actual project, taking values for placeholders
+.PARAMETER LibraryName
+The name of the library. Should consist only of alphanumeric characters and periods.
+.PARAMETER Author
+The name to use in copyright and owner notices.
+.PARAMETER CodeCovToken
+A token obtained from codecov.io for your repo. If not specified, code coverage results will not be published to codecov.io,
+but can be added later by editing the Azure Pipelines YAML file.
+.PARAMETER CIFeed
+The `/{guid}` path to the Azure Pipelines artifact feed to push your nuget package to as part of your CI.
+.PARAMETER Squash
+A switch that causes all of git history to be squashed to just one initial commit for the template, and one for its expansion.
+#>
+[CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium')]
+Param(
+ [Parameter(Mandatory=$true)]
+ [string]$LibraryName,
+ [Parameter(Mandatory=$true)]
+ [string]$Author,
+ [Parameter()]
+ [string]$CodeCovToken,
+ [Parameter()]
+ [string]$CIFeed,
+ [Parameter()]
+ [switch]$Squash
+)
+
+function Replace-Placeholders {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory=$true)]
+ [string]$Path,
+ [Parameter(Mandatory=$true)]
+ $Replacements
+ )
+
+ $Path = Resolve-Path $Path
+ Write-Host "Replacing tokens in `"$Path`""
+ $content = Get-Content -Path $Path | Out-String
+ $Replacements.GetEnumerator() |% {
+ $modifiedContent = $content -replace $_.Key,$_.Value
+ if ($modifiedContent -eq $content) {
+ Write-Error "No $($_.Key) token found to replace."
+ }
+ $content = $modifiedContent
+ }
+ $content = $content.TrimEnd(("`r","`n"))
+ [System.IO.File]::WriteAllLines($Path, $content) # Don't use Set-Content because that adds a UTF8 BOM
+ git add $Path
+}
+
+# Try to find sn.exe if it isn't on the PATH
+$sn = Get-Command sn -ErrorAction SilentlyContinue
+if (-not $sn) {
+ if ($IsMacOS -or $IsLinux) {
+ Write-Error "sn command not found on PATH. Install mono and/or vote up this issue: https://github.com/dotnet/sdk/issues/13560"
+ exit(1)
+ }
+ $snExes = Get-ChildItem -Recurse "${env:ProgramFiles(x86)}\Microsoft SDKs\Windows\sn.exe"
+ if ($snExes) {
+ $sn = Get-Command $snExes[0].FullName
+ } else {
+ Write-Error "sn command not found on PATH and SDK could not be found."
+ exit(1)
+ }
+}
+
+if (-not (& "$PSScriptRoot\tools\Check-DotNetSdk.ps1")) {
+ if ($PSCmdlet.ShouldProcess('Install .NET Core SDK?')) {
+ & "$PSScriptRoot\tools\Install-DotNetSdk.ps1"
+ } else {
+ Write-Error "Matching .NET Core SDK version not found. Install now?"
+ exit 1
+ }
+}
+
+# Verify all commands we use are on the PATH
+('git','dotnet') |% {
+ if (-not (Get-Command $_ -ErrorAction SilentlyContinue)) {
+ Write-Error "$_ command not found on PATH."
+ exit(1)
+ }
+}
+
+Push-Location $PSScriptRoot
+try {
+ if ($Squash) {
+ $originalCommitId = git rev-parse HEAD
+ git reset --soft $(git rev-list --max-parents=0 HEAD)
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ git commit --amend -qm "Initial template from https://github.com/AArnott/Library.Template" -m "Original commit from template $originalCommitId"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ }
+
+ git config core.safecrlf false # Avoid warnings when adding files with mangled line endings
+
+ # Rename project directories and solution
+ git mv Library.sln "$LibraryName.sln"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ git mv src/Library/Library.csproj "src/Library/$LibraryName.csproj"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ git mv src/Library "src/$LibraryName"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ git mv test/Library.Tests/Library.Tests.csproj "test/Library.Tests/$LibraryName.Tests.csproj"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ git mv test/Library.Tests "test/$LibraryName.Tests"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+
+ # Refresh solution file both to update paths and give the projects unique GUIDs
+ dotnet sln remove src/Library/Library.csproj
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ dotnet sln remove test/Library.Tests/Library.Tests.csproj
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ dotnet sln add "src/$LibraryName"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ dotnet sln add "test/$LibraryName.Tests"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ git add "$LibraryName.sln"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+
+ # Update project reference in test project. Add before removal to keep the same ItemGroup in place.
+ dotnet add "test/$LibraryName.Tests" reference "src/$LibraryName"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ dotnet remove "test/$LibraryName.Tests" reference src/Library/Library.csproj
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ git add "test/$LibraryName.Tests/$LibraryName.Tests.csproj"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+
+ # Establish a new strong-name key
+ & $sn.Path -k 2048 strongname.snk
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ git add strongname.snk
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+
+ # Replace placeholders in source files
+ Replace-Placeholders -Path "src/$LibraryName/Calculator.cs" -Replacements @{
+ 'Library'=$LibraryName
+ 'COMPANY-PLACEHOLDER'=$Author
+ }
+ Replace-Placeholders -Path "test/$LibraryName.Tests/CalculatorTests.cs" -Replacements @{
+ 'Library'=$LibraryName
+ 'COMPANY-PLACEHOLDER'=$Author
+ }
+ Replace-Placeholders -Path "src/AssemblyInfo.cs" -Replacements @{
+ 'COMPANY-PLACEHOLDER'=$Author
+ }
+ Replace-Placeholders -Path "LICENSE" -Replacements @{
+ 'COMPANY-PLACEHOLDER'=$Author
+ }
+ Replace-Placeholders -Path "stylecop.json" -Replacements @{
+ 'COMPANY-PLACEHOLDER'=$Author
+ }
+ Replace-Placeholders -Path "Directory.Build.props" -Replacements @{
+ 'COMPANY-PLACEHOLDER'=$Author
+ }
+ Replace-Placeholders -Path "README.md" -Replacements @{
+ "(?m)^.*\[NuGet package\][^`r`n]*"="[](https://nuget.org/packages/$LibraryName)"
+ "(?m)^.*\[Azure Pipelines status\].*`r?`n"=""
+ "(?m)^.*\[GitHub Actions status\].*`r?`n"=""
+ "(?m)^.*\[codecov\].*`r?`n"=""
+ }
+
+ # Specially handle azure-pipelines .yml edits
+ Replace-Placeholders -Path "azure-pipelines/build.yml" -Replacements @{
+ "(?m).*expand-template\.yml(?:\r)?\n" = ""
+ }
+
+ $YmlReplacements = @{}
+ if ($CodeCovToken) {
+ $YmlReplacements['(codecov_token: ).*(#.*)'] = "`$1$CodeCovToken"
+ } else {
+ $YmlReplacements['(codecov_token: ).*(#.*)'] = "#`$1`$2"
+ }
+ if ($CIFeed) {
+ $YmlReplacements['(ci_feed: ).*(#.*)'] = "`$1$CIFeed"
+ } else {
+ $YmlReplacements['(ci_feed: ).*(#.*)'] = "#`$1`$2"
+ }
+ Replace-Placeholders -Path "azure-pipelines.yml" -Replacements $YmlReplacements
+
+ # Self destruct
+ git rm Expand-Template.* Apply-Template.ps1
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ git rm :/azure-pipelines/expand-template.yml
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+
+ # Self-integrity check
+ Get-ChildItem -Recurse -File -Exclude bin,obj,README.md,Expand-Template.* |? { -not $_.FullName.Contains("obj") } |% {
+ $PLACEHOLDERS = Get-Content -Path $_.FullName |? { $_.Contains('PLACEHOLDER') }
+ if ($PLACEHOLDERS) {
+ Write-Error "PLACEHOLDER discovered in $($_.FullName)"
+ }
+ }
+
+ # Commit the changes
+ git commit -qm "Expanded template for $LibraryName" -m "This expansion done by the (now removed) Expand-Template.ps1 script."
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+
+ Write-Host -ForegroundColor Green "Template successfully expanded."
+
+ if ($env:PS1UnderCmd) {
+ # We're running under the Expand-Template.cmd script.
+ # Since we just deleted it from disk cmd.exe will complain. Just advise the user it's OK.
+ Write-Host -ForegroundColor Green 'Disregard an error you may see: "The batch file cannot be found." We just cleaned up after ourselves.'
+ }
+
+} finally {
+ git config --local --unset core.safecrlf
+ Pop-Location
+}
+
+# When testing this script, all the changes can be quickly reverted with this command:
+# git reset HEAD :/README.md :/LICENSE :/azure-pipelines.yml :/src :/test :/azure-pipelines; git co -- :/README.md :/LICENSE :/azure-pipelines.yml :/src :/azure-pipelines; git clean -fd :/src :/test
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..44122ac
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) COMPANY-PLACEHOLDER
+
+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/Library.sln b/Library.sln
new file mode 100644
index 0000000..638efb5
--- /dev/null
+++ b/Library.sln
@@ -0,0 +1,62 @@
+๏ปฟ
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29322.22
+MinimumVisualStudioVersion = 15.0.26124.0
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Library", "src\Library\Library.csproj", "{C06D702E-6FC7-453B-BDDF-608F825EC003}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Library.Tests", "test\Library.Tests\Library.Tests.csproj", "{DC5F3D1C-A9A3-44B7-A3C0-82C1FF4C3336}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1CE9670B-D5FF-46A7-9D00-24E70E6ED48B}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ Directory.Build.props = Directory.Build.props
+ Directory.Build.targets = Directory.Build.targets
+ Directory.Packages.props = Directory.Packages.props
+ global.json = global.json
+ nuget.config = nuget.config
+ README.md = README.md
+ stylecop.json = stylecop.json
+ version.json = version.json
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9E154A29-1796-4B85-BD81-B6A385D8FF71}"
+ ProjectSection(SolutionItems) = preProject
+ src\.editorconfig = src\.editorconfig
+ src\Directory.Build.props = src\Directory.Build.props
+ src\Directory.Build.targets = src\Directory.Build.targets
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{36CCE840-6FE5-4DB9-A8D5-8CF3CB6D342A}"
+ ProjectSection(SolutionItems) = preProject
+ test\.editorconfig = test\.editorconfig
+ test\Directory.Build.props = test\Directory.Build.props
+ test\Directory.Build.targets = test\Directory.Build.targets
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {C06D702E-6FC7-453B-BDDF-608F825EC003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C06D702E-6FC7-453B-BDDF-608F825EC003}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C06D702E-6FC7-453B-BDDF-608F825EC003}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C06D702E-6FC7-453B-BDDF-608F825EC003}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DC5F3D1C-A9A3-44B7-A3C0-82C1FF4C3336}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DC5F3D1C-A9A3-44B7-A3C0-82C1FF4C3336}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DC5F3D1C-A9A3-44B7-A3C0-82C1FF4C3336}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DC5F3D1C-A9A3-44B7-A3C0-82C1FF4C3336}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {9E154A29-1796-4B85-BD81-B6A385D8FF71} = {1CE9670B-D5FF-46A7-9D00-24E70E6ED48B}
+ {36CCE840-6FE5-4DB9-A8D5-8CF3CB6D342A} = {1CE9670B-D5FF-46A7-9D00-24E70E6ED48B}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {E3944F6A-384B-4B0F-B93F-3BD513DC57BD}
+ EndGlobalSection
+EndGlobal
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..33ddbfa
--- /dev/null
+++ b/README.md
@@ -0,0 +1,89 @@
+# Your Library
+
+***An awesome template for your awesome library***
+
+
+
+[](https://dev.azure.com/andrewarnott/OSS/_build/latest?definitionId=29&branchName=main)
+
+[](https://codecov.io/gh/aarnott/library.template)
+
+## Features
+
+* Follow the best and simplest patterns of build, pack and test with dotnet CLI.
+* Init script that installs prerequisites and auth helpers, supporting both non-elevation and elevation modes.
+* Static analyzers: default [Code Analysis](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/overview) and [StyleCop](https://github.com/DotNetAnalyzers/StyleCopAnalyzers)
+* Read-only source tree (builds to top-level bin/obj folders)
+* Auto-versioning (via [Nerdbank.GitVersioning](https://github.com/dotnet/nerdbank.gitversioning))
+* Builds with a "pinned" .NET Core SDK to ensure reproducible builds across machines and across time.
+* Automatically pack the library and publish it as an artifact, and even push it to some NuGet feed for consumption.
+* Testing
+ * Testing on .NET Framework, multiple .NET Core versions
+ * Testing on Windows, Linux and OSX
+ * Tests that crash or hang in Azure Pipelines automatically collect dumps and publish as a pipeline artifact for later investigation.
+* Cloud build support
+ * YAML based build for long-term serviceability, and PR review opportunities for any changes.
+ * Azure Pipelines and GitHub Action support
+ * Emphasis on PowerShell scripts over reliance on tasks for a more locally reproducible build.
+ * Code coverage published to Azure Pipelines
+ * Code coverage published to codecov.io so GitHub PRs get code coverage results added as a PR comment
+
+## Consumption
+
+Once you've expanded this template for your own use, you should **run the `Expand-Template.ps1` script** to customize the template for your own project.
+
+Further customize your repo by:
+
+1. Verify the license is suitable for your goal as it appears in the LICENSE and stylecop.json files and the Directory.Build.props file's `PackageLicenseExpression` property.
+1. Reset or replace the badges at the top of this file.
+
+### Maintaining your repo based on this template
+
+The best way to keep your repo in sync with this template's evolving features and best practices is to periodically merge the template into your repo:
+
+```ps1
+git checkout main # your default branch
+git pull # make sure you're at tip
+git fetch libtemplate # fetch latest Library.Template
+git merge libtemplate/main
+```
+
+There will frequently be merge conflicts to work out, but they will be easier to resolve than running the `Apply-Template.ps1` script every time, which simply blows away all your local changes with the latest from the template.
+
+If you do not already have Library.Template history in your repo or have never completed a merge before, the above steps may produce errors.
+To get it working the first time, follow these steps:
+
+```ps1
+git remote add libtemplate https://github.com/AArnott/Library.Template.git
+git fetch libtemplate
+```
+
+If the `git merge` step described earlier still fails for you, you may need to artificially create your first merge.
+First, you must have a local clone of Library.Template on your box:
+
+```ps1
+git clone https://github.com/AArnott/Library.Template.git
+```
+
+Make sure you have either `main` checked out in that clone, as appropriate to match.
+Use `git rev-parse HEAD` within the Library.Template repo and record the resulting commit as we'll use it later.
+
+Run the `Apply-Template.ps1` script, passing in the path to your own Library.Template-based repo. This will blow away most customizations you may have made to your repo's build authoring. You should *carefully* review all changes to your repo, staging those changes that you want to keep and reverting those that remove customizations you made.
+
+Now it's time to commit your changes. We do this in a very low-level way in order to have git record this as a *merge* commit even though it didn't start as a merge.
+By doing this, git will allow future merges from `libtemplate/main` and only new changes will be brought down, which will be much easier than the `Apply-Template.ps1` script you just ran.
+We create the merge commit with these commands:
+
+1. Be sure to have staged or reverted all the changes in your repo.
+1. Run `git write-tree` within your repo. This will print out a git tree hash.
+1. Run `git commit-tree -p HEAD -p A B -m "Merged latest Library.Template"`, where `A` is the output from `git rev-parse HEAD` that you recorded earlier, and `B` is the output from your prior `git write-tree` command.
+1. Run `git merge X` where `X` is the output of the `git commit-tree` command.
+
+**IMPORTANT**: If using a pull request to get your changes into your repo, you must *merge* your PR. If you *squash* your PR, history will be lost and you will have to repeatedly resolve the same merge conflicts at the next Library.Template update.
+
+**CAUTION**: when merging this for the first time, a github-hosted repo may close issues in your repo with the same number as issues that this repo closed in git commit messages.
+Verify after completing your PR by visiting your github closed issues, sorted by recently updated, and reactivate any that were inadvertently closed by this merge.
+This shouldn't be a recurring issue because going forward, we will avoid referencing github issues with simple `#123` syntax in this repo's history.
+
+Congratulations. You're all done.
+Next time you want to sync to latest from Library.Template, you can the simple `git merge` steps given at the start of this section.
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
new file mode 100644
index 0000000..9867b3d
--- /dev/null
+++ b/azure-pipelines.yml
@@ -0,0 +1,36 @@
+trigger:
+ batch: true
+ branches:
+ include:
+ - main
+ - 'validate/*'
+ paths:
+ exclude:
+ - doc/
+ - '*.md'
+ - .vscode/
+ - .github/
+ - azure-pipelines/release.yml
+
+parameters:
+- name: includeMacOS
+ displayName: Build on macOS
+ type: boolean
+ default: false # macOS is often bogged down in Azure Pipelines
+- name: RunTests
+ displayName: Run tests
+ type: boolean
+ default: true
+
+variables:
+ DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
+ BuildConfiguration: Release
+ codecov_token: 4dc9e7e2-6b01-4932-a180-847b52b43d35 # Get a new one from https://codecov.io/
+ ci_feed: https://pkgs.dev.azure.com/andrewarnott/_packaging/CI/nuget/v3/index.json # Azure Artifacts feed URL
+ NUGET_PACKAGES: $(Agent.TempDirectory)/.nuget/packages/
+
+jobs:
+- template: azure-pipelines/build.yml
+ parameters:
+ includeMacOS: ${{ parameters.includeMacOS }}
+ RunTests: ${{ parameters.RunTests }}
diff --git a/azure-pipelines/Get-ArtifactsStagingDirectory.ps1 b/azure-pipelines/Get-ArtifactsStagingDirectory.ps1
new file mode 100644
index 0000000..391e571
--- /dev/null
+++ b/azure-pipelines/Get-ArtifactsStagingDirectory.ps1
@@ -0,0 +1,15 @@
+Param(
+ [switch]$CleanIfLocal
+)
+if ($env:BUILD_ARTIFACTSTAGINGDIRECTORY) {
+ $ArtifactStagingFolder = $env:BUILD_ARTIFACTSTAGINGDIRECTORY
+} elseif ($env:RUNNER_TEMP) {
+ $ArtifactStagingFolder = "$env:RUNNER_TEMP\_artifacts"
+} else {
+ $ArtifactStagingFolder = [System.IO.Path]::GetFullPath("$PSScriptRoot/../obj/_artifacts")
+ if ($CleanIfLocal -and (Test-Path $ArtifactStagingFolder)) {
+ Remove-Item $ArtifactStagingFolder -Recurse -Force
+ }
+}
+
+$ArtifactStagingFolder
diff --git a/azure-pipelines/Get-CodeCovTool.ps1 b/azure-pipelines/Get-CodeCovTool.ps1
new file mode 100644
index 0000000..ca580b4
--- /dev/null
+++ b/azure-pipelines/Get-CodeCovTool.ps1
@@ -0,0 +1,86 @@
+<#
+.SYNOPSIS
+ Downloads the CodeCov.io uploader tool and returns the path to it.
+.PARAMETER AllowSkipVerify
+ Allows skipping signature verification of the downloaded tool if gpg is not installed.
+#>
+[CmdletBinding()]
+Param(
+ [switch]$AllowSkipVerify
+)
+
+if ($IsMacOS) {
+ $codeCovUrl = "https://uploader.codecov.io/latest/macos/codecov"
+ $toolName = 'codecov'
+}
+elseif ($IsLinux) {
+ $codeCovUrl = "https://uploader.codecov.io/latest/linux/codecov"
+ $toolName = 'codecov'
+}
+else {
+ $codeCovUrl = "https://uploader.codecov.io/latest/windows/codecov.exe"
+ $toolName = 'codecov.exe'
+}
+
+$shaSuffix = ".SHA256SUM"
+$sigSuffix = $shaSuffix + ".sig"
+
+Function Get-FileFromWeb([Uri]$Uri, $OutDir) {
+ $OutFile = Join-Path $OutDir $Uri.Segments[-1]
+ if (!(Test-Path $OutFile)) {
+ Write-Verbose "Downloading $Uri..."
+ if (!(Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null }
+ try {
+ (New-Object System.Net.WebClient).DownloadFile($Uri, $OutFile)
+ } finally {
+ # This try/finally causes the script to abort
+ }
+ }
+
+ $OutFile
+}
+
+$toolsPath = & "$PSScriptRoot\Get-TempToolsPath.ps1"
+$binaryToolsPath = Join-Path $toolsPath codecov
+$testingPath = Join-Path $binaryToolsPath unverified
+$finalToolPath = Join-Path $binaryToolsPath $toolName
+
+if (!(Test-Path $finalToolPath)) {
+ if (Test-Path $testingPath) {
+ Remove-Item -Recurse -Force $testingPath # ensure we download all matching files
+ }
+ $tool = Get-FileFromWeb $codeCovUrl $testingPath
+ $sha = Get-FileFromWeb "$codeCovUrl$shaSuffix" $testingPath
+ $sig = Get-FileFromWeb "$codeCovUrl$sigSuffix" $testingPath
+ $key = Get-FileFromWeb https://keybase.io/codecovsecurity/pgp_keys.asc $testingPath
+
+ if ((Get-Command gpg -ErrorAction SilentlyContinue)) {
+ Write-Host "Importing codecov key" -ForegroundColor Yellow
+ gpg --import $key
+ Write-Host "Verifying signature on codecov hash" -ForegroundColor Yellow
+ gpg --verify $sig $sha
+ } else {
+ if ($AllowSkipVerify) {
+ Write-Warning "gpg not found. Unable to verify hash signature."
+ } else {
+ throw "gpg not found. Unable to verify hash signature. Install gpg or add -AllowSkipVerify to override."
+ }
+ }
+
+ Write-Host "Verifying hash on downloaded tool" -ForegroundColor Yellow
+ $actualHash = (Get-FileHash -Path $tool -Algorithm SHA256).Hash
+ $expectedHash = (Get-Content $sha).Split()[0]
+ if ($actualHash -ne $expectedHash) {
+ # Validation failed. Delete the tool so we can't execute it.
+ #Remove-Item $codeCovPath
+ throw "codecov uploader tool failed signature validation."
+ }
+
+ Copy-Item $tool $finalToolPath
+
+ if ($IsMacOS -or $IsLinux) {
+ chmod u+x $finalToolPath
+ }
+}
+
+return $finalToolPath
diff --git a/azure-pipelines/Get-NuGetTool.ps1 b/azure-pipelines/Get-NuGetTool.ps1
new file mode 100644
index 0000000..3097c87
--- /dev/null
+++ b/azure-pipelines/Get-NuGetTool.ps1
@@ -0,0 +1,22 @@
+<#
+.SYNOPSIS
+ Downloads the NuGet.exe tool and returns the path to it.
+.PARAMETER NuGetVersion
+ The version of the NuGet tool to acquire.
+#>
+Param(
+ [Parameter()]
+ [string]$NuGetVersion='6.4.0'
+)
+
+$toolsPath = & "$PSScriptRoot\Get-TempToolsPath.ps1"
+$binaryToolsPath = Join-Path $toolsPath $NuGetVersion
+if (!(Test-Path $binaryToolsPath)) { $null = mkdir $binaryToolsPath }
+$nugetPath = Join-Path $binaryToolsPath nuget.exe
+
+if (!(Test-Path $nugetPath)) {
+ Write-Host "Downloading nuget.exe $NuGetVersion..." -ForegroundColor Yellow
+ (New-Object System.Net.WebClient).DownloadFile("https://dist.nuget.org/win-x86-commandline/v$NuGetVersion/NuGet.exe", $nugetPath)
+}
+
+return (Resolve-Path $nugetPath).Path
diff --git a/azure-pipelines/Get-ProcDump.ps1 b/azure-pipelines/Get-ProcDump.ps1
new file mode 100644
index 0000000..1493fe4
--- /dev/null
+++ b/azure-pipelines/Get-ProcDump.ps1
@@ -0,0 +1,14 @@
+<#
+.SYNOPSIS
+Downloads 32-bit and 64-bit procdump executables and returns the path to where they were installed.
+#>
+$version = '0.0.1'
+$baseDir = "$PSScriptRoot\..\obj\tools"
+$procDumpToolPath = "$baseDir\procdump.$version\bin"
+if (-not (Test-Path $procDumpToolPath)) {
+ if (-not (Test-Path $baseDir)) { New-Item -Type Directory -Path $baseDir | Out-Null }
+ $baseDir = (Resolve-Path $baseDir).Path # Normalize it
+ & (& $PSScriptRoot\Get-NuGetTool.ps1) install procdump -version $version -PackageSaveMode nuspec -OutputDirectory $baseDir -Source https://api.nuget.org/v3/index.json | Out-Null
+}
+
+(Resolve-Path $procDumpToolPath).Path
diff --git a/azure-pipelines/Get-SymbolFiles.ps1 b/azure-pipelines/Get-SymbolFiles.ps1
new file mode 100644
index 0000000..b5063ce
--- /dev/null
+++ b/azure-pipelines/Get-SymbolFiles.ps1
@@ -0,0 +1,66 @@
+<#
+.SYNOPSIS
+ Collect the list of PDBs built in this repo.
+.PARAMETER Path
+ The directory to recursively search for PDBs.
+.PARAMETER Tests
+ A switch indicating to find PDBs only for test binaries instead of only for shipping shipping binaries.
+#>
+[CmdletBinding()]
+param (
+ [parameter(Mandatory=$true)]
+ [string]$Path,
+ [switch]$Tests
+)
+
+$ActivityName = "Collecting symbols from $Path"
+Write-Progress -Activity $ActivityName -CurrentOperation "Discovery PDB files"
+$PDBs = Get-ChildItem -rec "$Path/*.pdb"
+
+# Filter PDBs to product OR test related.
+$testregex = "unittest|tests|\.test\."
+
+Write-Progress -Activity $ActivityName -CurrentOperation "De-duplicating symbols"
+$PDBsByHash = @{}
+$i = 0
+$PDBs |% {
+ Write-Progress -Activity $ActivityName -CurrentOperation "De-duplicating symbols" -PercentComplete (100 * $i / $PDBs.Length)
+ $hash = Get-FileHash $_
+ $i++
+ Add-Member -InputObject $_ -MemberType NoteProperty -Name Hash -Value $hash.Hash
+ Write-Output $_
+} | Sort-Object CreationTime |% {
+ # De-dupe based on hash. Prefer the first match so we take the first built copy.
+ if (-not $PDBsByHash.ContainsKey($_.Hash)) {
+ $PDBsByHash.Add($_.Hash, $_.FullName)
+ Write-Output $_
+ }
+} |? {
+ if ($Tests) {
+ $_.FullName -match $testregex
+ } else {
+ $_.FullName -notmatch $testregex
+ }
+} |% {
+ # Collect the DLLs/EXEs as well.
+ $rootName = "$($_.Directory)/$($_.BaseName)"
+ if ($rootName.EndsWith('.ni')) {
+ $rootName = $rootName.Substring(0, $rootName.Length - 3)
+ }
+
+ $dllPath = "$rootName.dll"
+ $exePath = "$rootName.exe"
+ if (Test-Path $dllPath) {
+ $BinaryImagePath = $dllPath
+ } elseif (Test-Path $exePath) {
+ $BinaryImagePath = $exePath
+ } else {
+ Write-Warning "`"$_`" found with no matching binary file."
+ $BinaryImagePath = $null
+ }
+
+ if ($BinaryImagePath) {
+ Write-Output $BinaryImagePath
+ Write-Output $_.FullName
+ }
+}
diff --git a/azure-pipelines/Get-TempToolsPath.ps1 b/azure-pipelines/Get-TempToolsPath.ps1
new file mode 100644
index 0000000..bb3da8e
--- /dev/null
+++ b/azure-pipelines/Get-TempToolsPath.ps1
@@ -0,0 +1,13 @@
+if ($env:AGENT_TEMPDIRECTORY) {
+ $path = "$env:AGENT_TEMPDIRECTORY\$env:BUILD_BUILDID"
+} elseif ($env:localappdata) {
+ $path = "$env:localappdata\gitrepos\tools"
+} else {
+ $path = "$PSScriptRoot\..\obj\tools"
+}
+
+if (!(Test-Path $path)) {
+ New-Item -ItemType Directory -Path $Path | Out-Null
+}
+
+(Resolve-Path $path).Path
diff --git a/azure-pipelines/Merge-CodeCoverage.ps1 b/azure-pipelines/Merge-CodeCoverage.ps1
new file mode 100644
index 0000000..5ecabbc
--- /dev/null
+++ b/azure-pipelines/Merge-CodeCoverage.ps1
@@ -0,0 +1,51 @@
+#!/usr/bin/env pwsh
+
+<#
+.SYNOPSIS
+ Merges code coverage reports.
+.PARAMETER Path
+ The path(s) to search for Cobertura code coverage reports.
+.PARAMETER Format
+ The format for the merged result. The default is Cobertura
+.PARAMETER OutputDir
+ The directory the merged result will be written to. The default is `coveragereport` in the root of this repo.
+#>
+[CmdletBinding()]
+Param(
+ [Parameter(Mandatory=$true)]
+ [string[]]$Path,
+ [ValidateSet('Badges', 'Clover', 'Cobertura', 'CsvSummary', 'Html', 'Html_Dark', 'Html_Light', 'HtmlChart', 'HtmlInline', 'HtmlInline_AzurePipelines', 'HtmlInline_AzurePipelines_Dark', 'HtmlInline_AzurePipelines_Light', 'HtmlSummary', 'JsonSummary', 'Latex', 'LatexSummary', 'lcov', 'MarkdownSummary', 'MHtml', 'PngChart', 'SonarQube', 'TeamCitySummary', 'TextSummary', 'Xml', 'XmlSummary')]
+ [string]$Format='Cobertura',
+ [string]$OutputFile=("$PSScriptRoot/../coveragereport/merged.cobertura.xml")
+)
+
+$RepoRoot = [string](Resolve-Path $PSScriptRoot/..)
+Push-Location $RepoRoot
+try {
+ Write-Verbose "Searching $Path for *.cobertura.xml files"
+ $reports = Get-ChildItem -Recurse $Path -Filter *.cobertura.xml
+
+ if ($reports) {
+ $reports |% { $_.FullName } |% {
+ # In addition to replacing {reporoot}, we also normalize on one kind of slash so that the report aggregates data for a file whether data was collected on Windows or not.
+ $xml = [xml](Get-Content -Path $_)
+ $xml.coverage.packages.package.classes.class |? { $_.filename} |% {
+ $_.filename = $_.filename.Replace('{reporoot}', $RepoRoot).Replace([IO.Path]::AltDirectorySeparatorChar, [IO.Path]::DirectorySeparatorChar)
+ }
+
+ $xml.Save($_)
+ }
+
+ $Inputs = $reports |% { Resolve-Path -relative $_.FullName }
+
+ if ((Split-Path $OutputFile) -and -not (Test-Path (Split-Path $OutputFile))) {
+ New-Item -Type Directory -Path (Split-Path $OutputFile) | Out-Null
+ }
+
+ & dotnet tool run dotnet-coverage merge $Inputs -o $OutputFile -f cobertura
+ } else {
+ Write-Error "No reports found to merge."
+ }
+} finally {
+ Pop-Location
+}
diff --git a/azure-pipelines/artifacts/Variables.ps1 b/azure-pipelines/artifacts/Variables.ps1
new file mode 100644
index 0000000..4bc6d21
--- /dev/null
+++ b/azure-pipelines/artifacts/Variables.ps1
@@ -0,0 +1,43 @@
+# This artifact captures all variables defined in the ..\variables folder.
+# It "snaps" the values of these variables where we can compute them during the build,
+# and otherwise captures the scripts to run later during an Azure Pipelines environment release.
+
+$RepoRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot/../..")
+$ArtifactBasePath = "$RepoRoot/obj/_artifacts"
+$VariablesArtifactPath = Join-Path $ArtifactBasePath variables
+if (-not (Test-Path $VariablesArtifactPath)) { New-Item -ItemType Directory -Path $VariablesArtifactPath | Out-Null }
+
+# Copy variables, either by value if the value is calculable now, or by script
+Get-ChildItem "$PSScriptRoot/../variables" |% {
+ $value = $null
+ if (-not $_.BaseName.StartsWith('_')) { # Skip trying to interpret special scripts
+ # First check the environment variables in case the variable was set in a queued build
+ # Always use all caps for env var access because Azure Pipelines converts variables to upper-case for env vars,
+ # and on non-Windows env vars are case sensitive.
+ $envVarName = $_.BaseName.ToUpper()
+ if (Test-Path env:$envVarName) {
+ $value = Get-Content "env:$envVarName"
+ }
+
+ # If that didn't give us anything, try executing the script right now from its original position
+ if (-not $value) {
+ $value = & $_.FullName
+ }
+
+ if ($value) {
+ # We got something, so wrap it with quotes so it's treated like a literal value.
+ $value = "'$value'"
+ }
+ }
+
+ # If that didn't get us anything, just copy the script itself
+ if (-not $value) {
+ $value = Get-Content -Path $_.FullName
+ }
+
+ Set-Content -Path "$VariablesArtifactPath/$($_.Name)" -Value $value
+}
+
+@{
+ "$VariablesArtifactPath" = (Get-ChildItem $VariablesArtifactPath -Recurse);
+}
diff --git a/azure-pipelines/artifacts/_all.ps1 b/azure-pipelines/artifacts/_all.ps1
new file mode 100755
index 0000000..9a22a1d
--- /dev/null
+++ b/azure-pipelines/artifacts/_all.ps1
@@ -0,0 +1,72 @@
+#!/usr/bin/env pwsh
+
+<#
+.SYNOPSIS
+ This script returns all the artifacts that should be collected after a build.
+ Each powershell artifact is expressed as an object with these properties:
+ Source - the full path to the source file
+ ArtifactName - the name of the artifact to upload to
+ ContainerFolder - the relative path within the artifact in which the file should appear
+ Each artifact aggregating .ps1 script should return a hashtable:
+ Key = path to the directory from which relative paths within the artifact should be calculated
+ Value = an array of paths (absolute or relative to the BaseDirectory) to files to include in the artifact.
+ FileInfo objects are also allowed.
+.PARAMETER Force
+ Executes artifact scripts even if they have already been staged.
+#>
+
+[CmdletBinding(SupportsShouldProcess = $true)]
+param (
+ [string]$ArtifactNameSuffix,
+ [switch]$Force
+)
+
+Function EnsureTrailingSlash($path) {
+ if ($path.length -gt 0 -and !$path.EndsWith('\') -and !$path.EndsWith('/')) {
+ $path = $path + [IO.Path]::DirectorySeparatorChar
+ }
+
+ $path.Replace('\', [IO.Path]::DirectorySeparatorChar)
+}
+
+Function Test-ArtifactStaged($artifactName) {
+ $varName = "ARTIFACTSTAGED_$($artifactName.ToUpper())"
+ Test-Path "env:$varName"
+}
+
+Get-ChildItem "$PSScriptRoot\*.ps1" -Exclude "_*" -Recurse | % {
+ $ArtifactName = $_.BaseName
+ if ($Force -or !(Test-ArtifactStaged($ArtifactName + $ArtifactNameSuffix))) {
+ $totalFileCount = 0
+ Write-Verbose "Collecting file list for artifact $($_.BaseName)"
+ $fileGroups = & $_
+ if ($fileGroups) {
+ $fileGroups.GetEnumerator() | % {
+ $BaseDirectory = New-Object Uri ((EnsureTrailingSlash $_.Key.ToString()), [UriKind]::Absolute)
+ $_.Value | ? { $_ } | % {
+ if ($_.GetType() -eq [IO.FileInfo] -or $_.GetType() -eq [IO.DirectoryInfo]) {
+ $_ = $_.FullName
+ }
+
+ $artifact = New-Object -TypeName PSObject
+ Add-Member -InputObject $artifact -MemberType NoteProperty -Name ArtifactName -Value $ArtifactName
+
+ $SourceFullPath = New-Object Uri ($BaseDirectory, $_)
+ Add-Member -InputObject $artifact -MemberType NoteProperty -Name Source -Value $SourceFullPath.LocalPath
+
+ $RelativePath = [Uri]::UnescapeDataString($BaseDirectory.MakeRelative($SourceFullPath))
+ Add-Member -InputObject $artifact -MemberType NoteProperty -Name ContainerFolder -Value (Split-Path $RelativePath)
+
+ Write-Output $artifact
+ $totalFileCount += 1
+ }
+ }
+ }
+
+ if ($totalFileCount -eq 0) {
+ Write-Warning "No files found for the `"$ArtifactName`" artifact."
+ }
+ } else {
+ Write-Host "Skipping $ArtifactName because it has already been staged." -ForegroundColor DarkGray
+ }
+}
diff --git a/azure-pipelines/artifacts/_pipelines.ps1 b/azure-pipelines/artifacts/_pipelines.ps1
new file mode 100644
index 0000000..47321ed
--- /dev/null
+++ b/azure-pipelines/artifacts/_pipelines.ps1
@@ -0,0 +1,45 @@
+<#
+.SYNOPSIS
+ This script translates all the artifacts described by _all.ps1
+ into commands that instruct Azure Pipelines to actually collect those artifacts.
+#>
+
+[CmdletBinding()]
+param (
+ [string]$ArtifactNameSuffix,
+ [switch]$StageOnly,
+ [switch]$AvoidSymbolicLinks
+)
+
+Function Set-PipelineVariable($name, $value) {
+ if ((Test-Path "Env:\$name") -and (Get-Item "Env:\$name").Value -eq $value) {
+ return # already set
+ }
+
+ #New-Item -Path "Env:\$name".ToUpper() -Value $value -Force | Out-Null
+ Write-Host "##vso[task.setvariable variable=$name]$value"
+}
+
+Function Test-ArtifactUploaded($artifactName) {
+ $varName = "ARTIFACTUPLOADED_$($artifactName.ToUpper())"
+ Test-Path "env:$varName"
+}
+
+& "$PSScriptRoot/_stage_all.ps1" -ArtifactNameSuffix $ArtifactNameSuffix -AvoidSymbolicLinks:$AvoidSymbolicLinks |% {
+ # Set a variable which will out-live this script so that a subsequent attempt to collect and upload artifacts
+ # will skip this one from a check in the _all.ps1 script.
+ Set-PipelineVariable "ARTIFACTSTAGED_$($_.Name.ToUpper())" 'true'
+ Write-Host "Staged artifact $($_.Name) to $($_.Path)"
+
+ if (!$StageOnly) {
+ if (Test-ArtifactUploaded $_.Name) {
+ Write-Host "Skipping $($_.Name) because it has already been uploaded." -ForegroundColor DarkGray
+ } else {
+ Write-Host "##vso[artifact.upload containerfolder=$($_.Name);artifactname=$($_.Name);]$($_.Path)"
+
+ # Set a variable which will out-live this script so that a subsequent attempt to collect and upload artifacts
+ # will skip this one from a check in the _all.ps1 script.
+ Set-PipelineVariable "ARTIFACTUPLOADED_$($_.Name.ToUpper())" 'true'
+ }
+ }
+}
diff --git a/azure-pipelines/artifacts/_stage_all.ps1 b/azure-pipelines/artifacts/_stage_all.ps1
new file mode 100644
index 0000000..74d7a38
--- /dev/null
+++ b/azure-pipelines/artifacts/_stage_all.ps1
@@ -0,0 +1,72 @@
+<#
+.SYNOPSIS
+ This script links all the artifacts described by _all.ps1
+ into a staging directory, reading for uploading to a cloud build artifact store.
+ It returns a sequence of objects with Name and Path properties.
+#>
+
+[CmdletBinding()]
+param (
+ [string]$ArtifactNameSuffix,
+ [switch]$AvoidSymbolicLinks
+)
+
+$ArtifactStagingFolder = & "$PSScriptRoot/../Get-ArtifactsStagingDirectory.ps1" -CleanIfLocal
+
+function Create-SymbolicLink {
+ param (
+ $Link,
+ $Target
+ )
+
+ if ($Link -eq $Target) {
+ return
+ }
+
+ if (Test-Path $Link) { Remove-Item $Link }
+ $LinkContainer = Split-Path $Link -Parent
+ if (!(Test-Path $LinkContainer)) { mkdir $LinkContainer }
+ if ($IsMacOS -or $IsLinux) {
+ ln $Target $Link | Out-Null
+ } else {
+ cmd /c "mklink `"$Link`" `"$Target`"" | Out-Null
+ }
+
+ if ($LASTEXITCODE -ne 0) {
+ # Windows requires admin privileges to create symbolic links
+ # unless Developer Mode has been enabled.
+ throw "Failed to create symbolic link at $Link that points to $Target"
+ }
+}
+
+# Stage all artifacts
+$Artifacts = & "$PSScriptRoot\_all.ps1" -ArtifactNameSuffix $ArtifactNameSuffix
+$Artifacts |% {
+ $DestinationFolder = [System.IO.Path]::GetFullPath("$ArtifactStagingFolder/$($_.ArtifactName)$ArtifactNameSuffix/$($_.ContainerFolder)").TrimEnd('\')
+ $Name = "$(Split-Path $_.Source -Leaf)"
+
+ #Write-Host "$($_.Source) -> $($_.ArtifactName)\$($_.ContainerFolder)" -ForegroundColor Yellow
+
+ if (-not (Test-Path $DestinationFolder)) { New-Item -ItemType Directory -Path $DestinationFolder | Out-Null }
+ if (Test-Path -PathType Leaf $_.Source) { # skip folders
+ $TargetPath = Join-Path $DestinationFolder $Name
+ if ($AvoidSymbolicLinks) {
+ Copy-Item -Path $_.Source -Destination $TargetPath
+ } else {
+ Create-SymbolicLink -Link $TargetPath -Target $_.Source
+ }
+ }
+}
+
+$ArtifactNames = $Artifacts |% { "$($_.ArtifactName)$ArtifactNameSuffix" }
+$ArtifactNames += Get-ChildItem env:ARTIFACTSTAGED_* |% {
+ # Return from ALLCAPS to the actual capitalization used for the artifact.
+ $artifactNameAllCaps = "$($_.Name.Substring('ARTIFACTSTAGED_'.Length))"
+ (Get-ChildItem $ArtifactStagingFolder\$artifactNameAllCaps* -Filter $artifactNameAllCaps).Name
+}
+$ArtifactNames | Get-Unique |% {
+ $artifact = New-Object -TypeName PSObject
+ Add-Member -InputObject $artifact -MemberType NoteProperty -Name Name -Value $_
+ Add-Member -InputObject $artifact -MemberType NoteProperty -Name Path -Value (Join-Path $ArtifactStagingFolder $_)
+ Write-Output $artifact
+}
diff --git a/azure-pipelines/artifacts/build_logs.ps1 b/azure-pipelines/artifacts/build_logs.ps1
new file mode 100644
index 0000000..f05358e
--- /dev/null
+++ b/azure-pipelines/artifacts/build_logs.ps1
@@ -0,0 +1,7 @@
+$ArtifactStagingFolder = & "$PSScriptRoot/../Get-ArtifactsStagingDirectory.ps1"
+
+if (!(Test-Path $ArtifactStagingFolder/build_logs)) { return }
+
+@{
+ "$ArtifactStagingFolder/build_logs" = (Get-ChildItem -Recurse "$ArtifactStagingFolder/build_logs")
+}
diff --git a/azure-pipelines/artifacts/coverageResults.ps1 b/azure-pipelines/artifacts/coverageResults.ps1
new file mode 100644
index 0000000..280ff9a
--- /dev/null
+++ b/azure-pipelines/artifacts/coverageResults.ps1
@@ -0,0 +1,23 @@
+$RepoRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..")
+
+$coverageFiles = @(Get-ChildItem "$RepoRoot/test/*.cobertura.xml" -Recurse | Where {$_.FullName -notlike "*/In/*" -and $_.FullName -notlike "*\In\*" })
+
+# Prepare code coverage reports for merging on another machine
+if ($env:SYSTEM_DEFAULTWORKINGDIRECTORY) {
+ Write-Host "Substituting $env:SYSTEM_DEFAULTWORKINGDIRECTORY with `"{reporoot}`""
+ $coverageFiles |% {
+ $content = Get-Content -Path $_ |% { $_ -Replace [regex]::Escape($env:SYSTEM_DEFAULTWORKINGDIRECTORY), "{reporoot}" }
+ Set-Content -Path $_ -Value $content -Encoding UTF8
+ }
+} else {
+ Write-Warning "coverageResults: Azure Pipelines not detected. Machine-neutral token replacement skipped."
+}
+
+if (!((Test-Path $RepoRoot\bin) -and (Test-Path $RepoRoot\obj))) { return }
+
+@{
+ $RepoRoot = (
+ $coverageFiles +
+ (Get-ChildItem "$RepoRoot\obj\*.cs" -Recurse)
+ );
+}
diff --git a/azure-pipelines/artifacts/deployables.ps1 b/azure-pipelines/artifacts/deployables.ps1
new file mode 100644
index 0000000..94c48cd
--- /dev/null
+++ b/azure-pipelines/artifacts/deployables.ps1
@@ -0,0 +1,13 @@
+$RepoRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..")
+$BuildConfiguration = $env:BUILDCONFIGURATION
+if (!$BuildConfiguration) {
+ $BuildConfiguration = 'Debug'
+}
+
+$PackagesRoot = "$RepoRoot/bin/Packages/$BuildConfiguration"
+
+if (!(Test-Path $PackagesRoot)) { return }
+
+@{
+ "$PackagesRoot" = (Get-ChildItem $PackagesRoot -Recurse)
+}
diff --git a/azure-pipelines/artifacts/projectAssetsJson.ps1 b/azure-pipelines/artifacts/projectAssetsJson.ps1
new file mode 100644
index 0000000..d2e85ff
--- /dev/null
+++ b/azure-pipelines/artifacts/projectAssetsJson.ps1
@@ -0,0 +1,9 @@
+$ObjRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..\obj")
+
+if (!(Test-Path $ObjRoot)) { return }
+
+@{
+ "$ObjRoot" = (
+ (Get-ChildItem "$ObjRoot\project.assets.json" -Recurse)
+ );
+}
diff --git a/azure-pipelines/artifacts/symbols.ps1 b/azure-pipelines/artifacts/symbols.ps1
new file mode 100644
index 0000000..9e2c7bd
--- /dev/null
+++ b/azure-pipelines/artifacts/symbols.ps1
@@ -0,0 +1,7 @@
+$BinPath = [System.IO.Path]::GetFullPath("$PSScriptRoot/../../bin")
+if (!(Test-Path $BinPath)) { return }
+$symbolfiles = & "$PSScriptRoot/../Get-SymbolFiles.ps1" -Path $BinPath | Get-Unique
+
+@{
+ "$BinPath" = $SymbolFiles;
+}
diff --git a/azure-pipelines/artifacts/testResults.ps1 b/azure-pipelines/artifacts/testResults.ps1
new file mode 100644
index 0000000..301a437
--- /dev/null
+++ b/azure-pipelines/artifacts/testResults.ps1
@@ -0,0 +1,15 @@
+[CmdletBinding()]
+Param(
+)
+
+$result = @{}
+
+$testRoot = Resolve-Path "$PSScriptRoot\..\..\test"
+$result[$testRoot] = (Get-ChildItem "$testRoot\TestResults" -Recurse -Directory | Get-ChildItem -Recurse -File)
+
+$testlogsPath = "$env:BUILD_ARTIFACTSTAGINGDIRECTORY\test_logs"
+if (Test-Path $testlogsPath) {
+ $result[$testlogsPath] = Get-ChildItem "$testlogsPath\*";
+}
+
+$result
diff --git a/azure-pipelines/artifacts/test_symbols.ps1 b/azure-pipelines/artifacts/test_symbols.ps1
new file mode 100644
index 0000000..ce2b648
--- /dev/null
+++ b/azure-pipelines/artifacts/test_symbols.ps1
@@ -0,0 +1,7 @@
+$BinPath = [System.IO.Path]::GetFullPath("$PSScriptRoot/../../bin")
+if (!(Test-Path $BinPath)) { return }
+$symbolfiles = & "$PSScriptRoot/../Get-SymbolFiles.ps1" -Path $BinPath -Tests | Get-Unique
+
+@{
+ "$BinPath" = $SymbolFiles;
+}
diff --git a/azure-pipelines/build.yml b/azure-pipelines/build.yml
new file mode 100644
index 0000000..3d53ed8
--- /dev/null
+++ b/azure-pipelines/build.yml
@@ -0,0 +1,79 @@
+parameters:
+- name: windowsPool
+ type: object
+ default:
+ vmImage: windows-2022
+- name: includeMacOS
+ type: boolean
+- name: RunTests
+ type: boolean
+ default: true
+
+jobs:
+- job: Windows
+ pool: ${{ parameters.windowsPool }}
+ steps:
+ - checkout: self
+ fetchDepth: 0 # avoid shallow clone so nbgv can do its work.
+ clean: true
+ - template: install-dependencies.yml
+
+ - script: dotnet nbgv cloud -c
+ displayName: โ Set build number
+
+ - template: dotnet.yml
+ parameters:
+ RunTests: ${{ parameters.RunTests }}
+ - template: expand-template.yml
+
+- job: Linux
+ pool:
+ vmImage: Ubuntu 20.04
+ steps:
+ - checkout: self
+ fetchDepth: 0 # avoid shallow clone so nbgv can do its work.
+ clean: true
+ - template: install-dependencies.yml
+ - template: dotnet.yml
+ parameters:
+ RunTests: ${{ parameters.RunTests }}
+ - script: dotnet format --verify-no-changes --no-restore
+ displayName: ๐
Verify formatted code
+ - template: expand-template.yml
+
+- job: macOS
+ condition: ${{ parameters.includeMacOS }}
+ pool:
+ vmImage: macOS-12
+ steps:
+ - checkout: self
+ fetchDepth: 0 # avoid shallow clone so nbgv can do its work.
+ clean: true
+ - template: install-dependencies.yml
+ - template: dotnet.yml
+ parameters:
+ RunTests: ${{ parameters.RunTests }}
+ - template: expand-template.yml
+
+- job: WrapUp
+ dependsOn:
+ - Windows
+ - Linux
+ - macOS
+ pool: ${{ parameters.windowsPool }} # Use Windows agent because PublishSymbols task requires it (https://github.com/microsoft/azure-pipelines-tasks/issues/13821).
+ condition: succeededOrFailed()
+ steps:
+ - checkout: self
+ fetchDepth: 0 # avoid shallow clone so nbgv can do its work.
+ clean: true
+ - template: install-dependencies.yml
+ parameters:
+ initArgs: -NoRestore
+ - template: publish-symbols.yml
+ parameters:
+ includeMacOS: ${{ parameters.includeMacOS }}
+ - ${{ if parameters.RunTests }}:
+ - template: publish-codecoverage.yml
+ parameters:
+ includeMacOS: ${{ parameters.includeMacOS }}
+ - template: publish-deployables.yml
diff --git a/azure-pipelines/dotnet-test-cloud.ps1 b/azure-pipelines/dotnet-test-cloud.ps1
new file mode 100755
index 0000000..24bf812
--- /dev/null
+++ b/azure-pipelines/dotnet-test-cloud.ps1
@@ -0,0 +1,83 @@
+#!/usr/bin/env pwsh
+
+<#
+.SYNOPSIS
+ Runs tests as they are run in cloud test runs.
+.PARAMETER Configuration
+ The configuration within which to run tests
+.PARAMETER Agent
+ The name of the agent. This is used in preparing test run titles.
+.PARAMETER PublishResults
+ A switch to publish results to Azure Pipelines.
+.PARAMETER x86
+ A switch to run the tests in an x86 process.
+.PARAMETER dotnet32
+ The path to a 32-bit dotnet executable to use.
+#>
+[CmdletBinding()]
+Param(
+ [string]$Configuration='Debug',
+ [string]$Agent='Local',
+ [switch]$PublishResults,
+ [switch]$x86,
+ [string]$dotnet32
+)
+
+$RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path
+$ArtifactStagingFolder = & "$PSScriptRoot/Get-ArtifactsStagingDirectory.ps1"
+
+$dotnet = 'dotnet'
+if ($x86) {
+ $x86RunTitleSuffix = ", x86"
+ if ($dotnet32) {
+ $dotnet = $dotnet32
+ } else {
+ $dotnet32Possibilities = "$PSScriptRoot\../obj/tools/x86/.dotnet/dotnet.exe", "$env:AGENT_TOOLSDIRECTORY/x86/dotnet/dotnet.exe", "${env:ProgramFiles(x86)}\dotnet\dotnet.exe"
+ $dotnet32Matches = $dotnet32Possibilities |? { Test-Path $_ }
+ if ($dotnet32Matches) {
+ $dotnet = Resolve-Path @($dotnet32Matches)[0]
+ Write-Host "Running tests using `"$dotnet`"" -ForegroundColor DarkGray
+ } else {
+ Write-Error "Unable to find 32-bit dotnet.exe"
+ return 1
+ }
+ }
+}
+
+& $dotnet test $RepoRoot `
+ --no-build `
+ -c $Configuration `
+ --filter "TestCategory!=FailsInCloudTest" `
+ --collect "Code Coverage;Format=cobertura" `
+ --settings "$PSScriptRoot/test.runsettings" `
+ --blame-hang-timeout 60s `
+ --blame-crash `
+ -bl:"$ArtifactStagingFolder/build_logs/test.binlog" `
+ --diag "$ArtifactStagingFolder/test_logs/diag.log;TraceLevel=info" `
+ --logger trx `
+
+$unknownCounter = 0
+Get-ChildItem -Recurse -Path $RepoRoot\test\*.trx |% {
+ Copy-Item $_ -Destination $ArtifactStagingFolder/test_logs/
+
+ if ($PublishResults) {
+ $x = [xml](Get-Content -Path $_)
+ $runTitle = $null
+ if ($x.TestRun.TestDefinitions -and $x.TestRun.TestDefinitions.GetElementsByTagName('UnitTest')) {
+ $storage = $x.TestRun.TestDefinitions.GetElementsByTagName('UnitTest')[0].storage -replace '\\','/'
+ if ($storage -match '/(?net[^/]+)/(?:(?[^/]+)/)?(?[^/]+)\.dll$') {
+ if ($matches.rid) {
+ $runTitle = "$($matches.lib) ($($matches.tfm), $($matches.rid), $Agent)"
+ } else {
+ $runTitle = "$($matches.lib) ($($matches.tfm)$x86RunTitleSuffix, $Agent)"
+ }
+ }
+ }
+ if (!$runTitle) {
+ $unknownCounter += 1;
+ $runTitle = "unknown$unknownCounter ($Agent$x86RunTitleSuffix)";
+ }
+
+ Write-Host "##vso[results.publish type=VSTest;runTitle=$runTitle;publishRunAttachments=true;resultFiles=$_;failTaskOnFailedTests=true;testRunSystem=VSTS - PTR;]"
+ }
+}
diff --git a/azure-pipelines/dotnet.yml b/azure-pipelines/dotnet.yml
new file mode 100644
index 0000000..5124154
--- /dev/null
+++ b/azure-pipelines/dotnet.yml
@@ -0,0 +1,30 @@
+parameters:
+ RunTests:
+
+steps:
+
+- script: dotnet build -t:build,pack --no-restore -c $(BuildConfiguration) -warnaserror /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog"
+ displayName: ๐ dotnet build
+
+- powershell: azure-pipelines/dotnet-test-cloud.ps1 -Configuration $(BuildConfiguration) -Agent $(Agent.JobName) -PublishResults
+ displayName: ๐งช dotnet test
+ condition: and(succeeded(), ${{ parameters.RunTests }})
+
+- powershell: azure-pipelines/variables/_pipelines.ps1
+ failOnStderr: true
+ displayName: โ Update pipeline variables based on build outputs
+ condition: succeededOrFailed()
+
+- powershell: azure-pipelines/artifacts/_pipelines.ps1 -ArtifactNameSuffix "-$(Agent.JobName)" -Verbose
+ failOnStderr: true
+ displayName: ๐ข Publish artifacts
+ condition: succeededOrFailed()
+
+- ${{ if and(ne(variables['codecov_token'], ''), parameters.RunTests) }}:
+ - powershell: |
+ $ArtifactStagingFolder = & "azure-pipelines/Get-ArtifactsStagingDirectory.ps1"
+ $CoverageResultsFolder = Join-Path $ArtifactStagingFolder "coverageResults-$(Agent.JobName)"
+ azure-pipelines/publish-CodeCov.ps1 -CodeCovToken "$(codecov_token)" -PathToCodeCoverage "$CoverageResultsFolder" -Name "$(Agent.JobName) Coverage Results" -Flags "$(Agent.JobName)Host,$(BuildConfiguration)"
+ displayName: ๐ข Publish code coverage results to codecov.io
+ timeoutInMinutes: 3
+ continueOnError: true
diff --git a/azure-pipelines/expand-template.yml b/azure-pipelines/expand-template.yml
new file mode 100644
index 0000000..d843f1e
--- /dev/null
+++ b/azure-pipelines/expand-template.yml
@@ -0,0 +1,14 @@
+steps:
+- script: |
+ dotnet build-server shutdown
+ git clean -fdx
+ displayName: ๐งน Cleaning repo for template expansion
+- powershell: |
+ git config user.name "test user"
+ git config user.email "andrewarnott@gmail.com"
+ ./Expand-Template.ps1 -LibraryName Calc -Author "Andrew Arnott"
+ displayName: ๐งช Expanding template
+ failOnStderr: true
+# TODO: Verify that all changes are staged to the git index
+- script: dotnet build
+ displayName: ๐ dotnet build (expanded template)
diff --git a/azure-pipelines/install-dependencies.yml b/azure-pipelines/install-dependencies.yml
new file mode 100644
index 0000000..8178266
--- /dev/null
+++ b/azure-pipelines/install-dependencies.yml
@@ -0,0 +1,25 @@
+parameters:
+ initArgs:
+
+steps:
+
+- task: NuGetAuthenticate@1
+ displayName: ๐ Authenticate NuGet feeds
+ inputs:
+ forceReinstallCredentialProvider: true
+
+- powershell: |
+ $AccessToken = '$(System.AccessToken)' # Avoid specifying the access token directly on the init.ps1 command line to avoid it showing up in errors
+ .\init.ps1 -AccessToken $AccessToken ${{ parameters['initArgs'] }} -UpgradePrerequisites -NoNuGetCredProvider
+ dotnet --info
+
+ # Print mono version if it is present.
+ if (Get-Command mono -ErrorAction SilentlyContinue) {
+ mono --version
+ }
+ displayName: โ Install prerequisites
+
+- powershell: azure-pipelines/variables/_pipelines.ps1
+ failOnStderr: true
+ displayName: โ Set pipeline variables based on source
+ name: SetPipelineVariables
diff --git a/azure-pipelines/justnugetorg.nuget.config b/azure-pipelines/justnugetorg.nuget.config
new file mode 100644
index 0000000..765346e
--- /dev/null
+++ b/azure-pipelines/justnugetorg.nuget.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/azure-pipelines/publish-CodeCov.ps1 b/azure-pipelines/publish-CodeCov.ps1
new file mode 100644
index 0000000..9926f01
--- /dev/null
+++ b/azure-pipelines/publish-CodeCov.ps1
@@ -0,0 +1,30 @@
+<#
+.SYNOPSIS
+ Uploads code coverage to codecov.io
+.PARAMETER CodeCovToken
+ Code coverage token to use
+.PARAMETER PathToCodeCoverage
+ Path to root of code coverage files
+.PARAMETER Name
+ Name to upload with codecoverge
+.PARAMETER Flags
+ Flags to upload with codecoverge
+#>
+[CmdletBinding()]
+Param (
+ [Parameter(Mandatory=$true)]
+ [string]$CodeCovToken,
+ [Parameter(Mandatory=$true)]
+ [string]$PathToCodeCoverage,
+ [string]$Name,
+ [string]$Flags
+)
+
+$RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path
+
+Get-ChildItem -Recurse -Path $PathToCodeCoverage -Filter "*.cobertura.xml" | % {
+ $relativeFilePath = Resolve-Path -relative $_.FullName
+
+ Write-Host "Uploading: $relativeFilePath" -ForegroundColor Yellow
+ & (& "$PSScriptRoot/Get-CodeCovTool.ps1") -t $CodeCovToken -f $relativeFilePath -R $RepoRoot -F $Flags -n $Name
+}
diff --git a/azure-pipelines/publish-codecoverage.yml b/azure-pipelines/publish-codecoverage.yml
new file mode 100644
index 0000000..fbb6a39
--- /dev/null
+++ b/azure-pipelines/publish-codecoverage.yml
@@ -0,0 +1,25 @@
+parameters:
+ includeMacOS:
+
+steps:
+- download: current
+ artifact: coverageResults-Windows
+ displayName: ๐ป Download Windows code coverage results
+ continueOnError: true
+- download: current
+ artifact: coverageResults-Linux
+ displayName: ๐ป Download Linux code coverage results
+ continueOnError: true
+- download: current
+ artifact: coverageResults-macOS
+ displayName: ๐ป Download macOS code coverage results
+ continueOnError: true
+ condition: and(succeeded(), ${{ parameters.includeMacOS }})
+- powershell: azure-pipelines/Merge-CodeCoverage.ps1 -Path '$(Pipeline.Workspace)' -OutputFile coveragereport/merged.cobertura.xml -Format Cobertura -Verbose
+ displayName: โ Merge coverage
+- task: PublishCodeCoverageResults@1
+ displayName: ๐ข Publish code coverage results to Azure DevOps
+ inputs:
+ codeCoverageTool: cobertura
+ summaryFileLocation: coveragereport/merged.cobertura.xml
+ failIfCoverageEmpty: true
diff --git a/azure-pipelines/publish-deployables.yml b/azure-pipelines/publish-deployables.yml
new file mode 100644
index 0000000..31e80a4
--- /dev/null
+++ b/azure-pipelines/publish-deployables.yml
@@ -0,0 +1,8 @@
+steps:
+- download: current
+ displayName: ๐ป Download deployables
+ artifact: deployables-Windows
+
+- powershell: dotnet nuget push "$(Resolve-Path '$(Pipeline.Workspace)\deployables-Windows\')*.nupkg" -s $(ci_feed) -k azdo --skip-duplicate
+ displayName: ๐ฆ Push packages to CI feed
+ condition: and(succeeded(), ne(variables['ci_feed'], ''), ne(variables['Build.Reason'], 'PullRequest'))
diff --git a/azure-pipelines/publish-symbols.yml b/azure-pipelines/publish-symbols.yml
new file mode 100644
index 0000000..00c188f
--- /dev/null
+++ b/azure-pipelines/publish-symbols.yml
@@ -0,0 +1,59 @@
+parameters:
+ includeMacOS:
+
+steps:
+- task: DownloadPipelineArtifact@2
+ inputs:
+ artifact: symbols-Windows
+ path: $(Pipeline.Workspace)/symbols/Windows
+ displayName: ๐ป Download Windows symbols
+ continueOnError: true
+- task: DownloadPipelineArtifact@2
+ inputs:
+ artifact: symbols-Linux
+ path: $(Pipeline.Workspace)/symbols/Linux
+ displayName: ๐ป Download Linux symbols
+ continueOnError: true
+- task: DownloadPipelineArtifact@2
+ inputs:
+ artifact: symbols-macOS
+ path: $(Pipeline.Workspace)/symbols/macOS
+ displayName: ๐ป Download macOS symbols
+ continueOnError: true
+ condition: ${{ parameters.includeMacOS }}
+
+- task: DownloadPipelineArtifact@2
+ inputs:
+ artifact: test_symbols-Windows
+ path: $(Pipeline.Workspace)/test_symbols/Windows
+ displayName: ๐ป Download Windows test symbols
+ continueOnError: true
+- task: DownloadPipelineArtifact@2
+ inputs:
+ artifact: test_symbols-Linux
+ path: $(Pipeline.Workspace)/test_symbols/Linux
+ displayName: ๐ป Download Linux test symbols
+ continueOnError: true
+- task: DownloadPipelineArtifact@2
+ inputs:
+ artifact: test_symbols-macOS
+ path: $(Pipeline.Workspace)/test_symbols/macOS
+ displayName: ๐ป Download macOS test symbols
+ continueOnError: true
+ condition: ${{ parameters.includeMacOS }}
+
+- task: PublishSymbols@2
+ inputs:
+ SymbolsFolder: $(Pipeline.Workspace)/symbols
+ SearchPattern: '**/*.pdb'
+ IndexSources: false
+ SymbolServerType: TeamServices
+ displayName: ๐ข Publish symbols
+
+- task: PublishSymbols@2
+ inputs:
+ SymbolsFolder: $(Pipeline.Workspace)/test_symbols
+ SearchPattern: '**/*.pdb'
+ IndexSources: false
+ SymbolServerType: TeamServices
+ displayName: ๐ข Publish test symbols
diff --git a/azure-pipelines/release.yml b/azure-pipelines/release.yml
new file mode 100644
index 0000000..4c9db86
--- /dev/null
+++ b/azure-pipelines/release.yml
@@ -0,0 +1,60 @@
+trigger: none # We only want to trigger manually or based on resources
+pr: none
+
+resources:
+ pipelines:
+ - pipeline: CI
+ source: Library # TODO: This should match the name of your CI pipeline
+ trigger:
+ tags:
+ - auto-release
+
+variables:
+- group: Publishing secrets
+
+jobs:
+- job: release
+ pool:
+ vmImage: ubuntu-latest
+ steps:
+ - checkout: none
+ - powershell: |
+ Write-Host "##vso[build.updatebuildnumber]$(resources.pipeline.CI.runName)"
+ if ('$(resources.pipeline.CI.runName)'.Contains('-')) {
+ Write-Host "##vso[task.setvariable variable=IsPrerelease]true"
+ } else {
+ Write-Host "##vso[task.setvariable variable=IsPrerelease]false"
+ }
+ displayName: โ Set up pipeline
+ - task: UseDotNet@2
+ displayName: โ Install .NET SDK
+ inputs:
+ packageType: sdk
+ version: 6.x
+ - download: CI
+ artifact: deployables-Windows
+ displayName: ๐ป Download deployables-Windows artifact
+ patterns: 'deployables-Windows/*'
+ - task: GitHubRelease@1
+ displayName: ๐ข GitHub release (create)
+ inputs:
+ gitHubConnection: # TODO: fill in service connection here
+ repositoryName: $(Build.Repository.Name)
+ target: $(resources.pipeline.CI.sourceCommit)
+ tagSource: userSpecifiedTag
+ tag: v$(resources.pipeline.CI.runName)
+ title: v$(resources.pipeline.CI.runName)
+ isDraft: true # After running this step, visit the new draft release, edit, and publish.
+ isPreRelease: $(IsPrerelease)
+ assets: $(Pipeline.Workspace)/CI/deployables-Windows/*.nupkg
+ changeLogCompareToRelease: lastNonDraftRelease
+ changeLogType: issueBased
+ changeLogLabels: |
+ [
+ { "label" : "breaking change", "displayName" : "Breaking changes", "state" : "closed" },
+ { "label" : "bug", "displayName" : "Fixes", "state" : "closed" },
+ { "label" : "enhancement", "displayName": "Enhancements", "state" : "closed" }
+ ]
+ - script: dotnet nuget push $(Pipeline.Workspace)/CI/deployables-Windows/*.nupkg -s https://api.nuget.org/v3/index.json --api-key $(NuGetOrgApiKey) --skip-duplicate
+ displayName: ๐ฆ Push packages to nuget.org
+ condition: and(succeeded(), ne(variables['NuGetOrgApiKey'], ''))
diff --git a/azure-pipelines/test.runsettings b/azure-pipelines/test.runsettings
new file mode 100644
index 0000000..4e24a0a
--- /dev/null
+++ b/azure-pipelines/test.runsettings
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+ \.dll$
+ \.exe$
+
+
+ xunit\..*
+
+
+
+
+ ^System\.Diagnostics\.DebuggerHiddenAttribute$
+ ^System\.Diagnostics\.DebuggerNonUserCodeAttribute$
+ ^System\.CodeDom\.Compiler\.GeneratedCodeAttribute$
+ ^System\.Diagnostics\.CodeAnalysis\.ExcludeFromCodeCoverageAttribute$
+
+
+
+
+ True
+
+ True
+
+ True
+
+ False
+
+ False
+
+ False
+
+ True
+
+
+
+
+
+
diff --git a/azure-pipelines/variables/DotNetSdkVersion.ps1 b/azure-pipelines/variables/DotNetSdkVersion.ps1
new file mode 100644
index 0000000..b213fbc
--- /dev/null
+++ b/azure-pipelines/variables/DotNetSdkVersion.ps1
@@ -0,0 +1,2 @@
+$globalJson = Get-Content -Path "$PSScriptRoot\..\..\global.json" | ConvertFrom-Json
+$globalJson.sdk.version
diff --git a/azure-pipelines/variables/_all.ps1 b/azure-pipelines/variables/_all.ps1
new file mode 100755
index 0000000..cc6e881
--- /dev/null
+++ b/azure-pipelines/variables/_all.ps1
@@ -0,0 +1,20 @@
+#!/usr/bin/env pwsh
+
+<#
+.SYNOPSIS
+ This script returns a hashtable of build variables that should be set
+ at the start of a build or release definition's execution.
+#>
+
+[CmdletBinding(SupportsShouldProcess = $true)]
+param (
+)
+
+$vars = @{}
+
+Get-ChildItem "$PSScriptRoot\*.ps1" -Exclude "_*" |% {
+ Write-Host "Computing $($_.BaseName) variable"
+ $vars[$_.BaseName] = & $_
+}
+
+$vars
diff --git a/azure-pipelines/variables/_pipelines.ps1 b/azure-pipelines/variables/_pipelines.ps1
new file mode 100644
index 0000000..11748b8
--- /dev/null
+++ b/azure-pipelines/variables/_pipelines.ps1
@@ -0,0 +1,31 @@
+<#
+.SYNOPSIS
+ This script translates the variables returned by the _all.ps1 script
+ into commands that instruct Azure Pipelines to actually set those variables for other pipeline tasks to consume.
+
+ The build or release definition may have set these variables to override
+ what the build would do. So only set them if they have not already been set.
+#>
+
+[CmdletBinding()]
+param (
+)
+
+(& "$PSScriptRoot\_all.ps1").GetEnumerator() |% {
+ # Always use ALL CAPS for env var names since Azure Pipelines converts variable names to all caps and on non-Windows OS, env vars are case sensitive.
+ $keyCaps = $_.Key.ToUpper()
+ if ((Test-Path "env:$keyCaps") -and (Get-Content "env:$keyCaps")) {
+ Write-Host "Skipping setting $keyCaps because variable is already set to '$(Get-Content env:$keyCaps)'." -ForegroundColor Cyan
+ } else {
+ Write-Host "$keyCaps=$($_.Value)" -ForegroundColor Yellow
+ if ($env:TF_BUILD) {
+ # Create two variables: the first that can be used by its simple name and accessible only within this job.
+ Write-Host "##vso[task.setvariable variable=$keyCaps]$($_.Value)"
+ # and the second that works across jobs and stages but must be fully qualified when referenced.
+ Write-Host "##vso[task.setvariable variable=$keyCaps;isOutput=true]$($_.Value)"
+ } elseif ($env:GITHUB_ACTIONS) {
+ Add-Content -Path $env:GITHUB_ENV -Value "$keyCaps=$($_.Value)"
+ }
+ Set-Item -Path "env:$keyCaps" -Value $_.Value
+ }
+}
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..d24a9b7
--- /dev/null
+++ b/global.json
@@ -0,0 +1,7 @@
+{
+ "sdk": {
+ "version": "8.0.100",
+ "rollForward": "patch",
+ "allowPrerelease": false
+ }
+}
diff --git a/init.cmd b/init.cmd
new file mode 100644
index 0000000..667efab
--- /dev/null
+++ b/init.cmd
@@ -0,0 +1,20 @@
+@echo off
+SETLOCAL
+set PS1UnderCmd=1
+
+:: Get the datetime in a format that can go in a filename.
+set _my_datetime=%date%_%time%
+set _my_datetime=%_my_datetime: =_%
+set _my_datetime=%_my_datetime::=%
+set _my_datetime=%_my_datetime:/=_%
+set _my_datetime=%_my_datetime:.=_%
+set CmdEnvScriptPath=%temp%\envvarscript_%_my_datetime%.cmd
+
+powershell.exe -NoProfile -NoLogo -ExecutionPolicy bypass -Command "try { & '%~dpn0.ps1' %*; exit $LASTEXITCODE } catch { write-host $_; exit 1 }"
+
+:: Set environment variables in the parent cmd.exe process.
+IF EXIST "%CmdEnvScriptPath%" (
+ ENDLOCAL
+ CALL "%CmdEnvScriptPath%"
+ DEL "%CmdEnvScriptPath%"
+)
diff --git a/init.ps1 b/init.ps1
new file mode 100755
index 0000000..e284985
--- /dev/null
+++ b/init.ps1
@@ -0,0 +1,114 @@
+#!/usr/bin/env pwsh
+
+<#
+.SYNOPSIS
+ Installs dependencies required to build and test the projects in this repository.
+.DESCRIPTION
+ This MAY not require elevation, as the SDK and runtimes are installed to a per-user location,
+ unless the `-InstallLocality` switch is specified directing to a per-repo or per-machine location.
+ See detailed help on that switch for more information.
+
+ The CmdEnvScriptPath environment variable may be optionally set to a path to a cmd shell script to be created (or appended to if it already exists) that will set the environment variables in cmd.exe that are set within the PowerShell environment.
+ This is used by init.cmd in order to reapply any new environment variables to the parent cmd.exe process that were set in the powershell child process.
+.PARAMETER InstallLocality
+ A value indicating whether dependencies should be installed locally to the repo or at a per-user location.
+ Per-user allows sharing the installed dependencies across repositories and allows use of a shared expanded package cache.
+ Visual Studio will only notice and use these SDKs/runtimes if VS is launched from the environment that runs this script.
+ Per-repo allows for high isolation, allowing for a more precise recreation of the environment within an Azure Pipelines build.
+ When using 'repo', environment variables are set to cause the locally installed dotnet SDK to be used.
+ Per-repo can lead to file locking issues when dotnet.exe is left running as a build server and can be mitigated by running `dotnet build-server shutdown`.
+ Per-machine requires elevation and will download and install all SDKs and runtimes to machine-wide locations so all applications can find it.
+.PARAMETER NoPrerequisites
+ Skips the installation of prerequisite software (e.g. SDKs, tools).
+.PARAMETER NoNuGetCredProvider
+ Skips the installation of the NuGet credential provider. Useful in pipelines with the `NuGetAuthenticate` task, as a workaround for https://github.com/microsoft/artifacts-credprovider/issues/244.
+ This switch is ignored and installation is skipped when -NoPrerequisites is specified.
+.PARAMETER UpgradePrerequisites
+ Takes time to install prerequisites even if they are already present in case they need to be upgraded.
+ No effect if -NoPrerequisites is specified.
+.PARAMETER NoRestore
+ Skips the package restore step.
+.PARAMETER NoToolRestore
+ Skips the dotnet tool restore step.
+.PARAMETER AccessToken
+ An optional access token for authenticating to Azure Artifacts authenticated feeds.
+.PARAMETER Interactive
+ Runs NuGet restore in interactive mode. This can turn authentication failures into authentication challenges.
+#>
+[CmdletBinding(SupportsShouldProcess = $true)]
+Param (
+ [ValidateSet('repo', 'user', 'machine')]
+ [string]$InstallLocality = 'user',
+ [Parameter()]
+ [switch]$NoPrerequisites,
+ [Parameter()]
+ [switch]$NoNuGetCredProvider,
+ [Parameter()]
+ [switch]$UpgradePrerequisites,
+ [Parameter()]
+ [switch]$NoRestore,
+ [Parameter()]
+ [switch]$NoToolRestore,
+ [Parameter()]
+ [string]$AccessToken,
+ [Parameter()]
+ [switch]$Interactive
+)
+
+$EnvVars = @{}
+$PrependPath = @()
+
+if (!$NoPrerequisites) {
+ if (!$NoNuGetCredProvider) {
+ & "$PSScriptRoot\tools\Install-NuGetCredProvider.ps1" -AccessToken $AccessToken -Force:$UpgradePrerequisites
+ }
+
+ & "$PSScriptRoot\tools\Install-DotNetSdk.ps1" -InstallLocality $InstallLocality
+ if ($LASTEXITCODE -eq 3010) {
+ Exit 3010
+ }
+
+ # The procdump tool and env var is required for dotnet test to collect hang/crash dumps of tests.
+ # But it only works on Windows.
+ if ($env:OS -eq 'Windows_NT') {
+ $EnvVars['PROCDUMP_PATH'] = & "$PSScriptRoot\azure-pipelines\Get-ProcDump.ps1"
+ }
+}
+
+# Workaround nuget credential provider bug that causes very unreliable package restores on Azure Pipelines
+$env:NUGET_PLUGIN_HANDSHAKE_TIMEOUT_IN_SECONDS = 20
+$env:NUGET_PLUGIN_REQUEST_TIMEOUT_IN_SECONDS = 20
+
+Push-Location $PSScriptRoot
+try {
+ $HeaderColor = 'Green'
+
+ $RestoreArguments = @()
+ if ($Interactive) {
+ $RestoreArguments += '--interactive'
+ }
+
+ if (!$NoRestore -and $PSCmdlet.ShouldProcess("NuGet packages", "Restore")) {
+ Write-Host "Restoring NuGet packages" -ForegroundColor $HeaderColor
+ dotnet restore @RestoreArguments
+ if ($lastexitcode -ne 0) {
+ throw "Failure while restoring packages."
+ }
+ }
+
+ if (!$NoToolRestore -and $PSCmdlet.ShouldProcess("dotnet tool", "restore")) {
+ dotnet tool restore @RestoreArguments
+ if ($lastexitcode -ne 0) {
+ throw "Failure while restoring dotnet CLI tools."
+ }
+ }
+
+ & "$PSScriptRoot/tools/Set-EnvVars.ps1" -Variables $EnvVars -PrependPath $PrependPath | Out-Null
+}
+catch {
+ Write-Error $error[0]
+ exit $lastexitcode
+}
+finally {
+ Pop-Location
+}
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 0000000..22f7b80
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/settings.VisualStudio.json b/settings.VisualStudio.json
new file mode 100644
index 0000000..7abb4a0
--- /dev/null
+++ b/settings.VisualStudio.json
@@ -0,0 +1,3 @@
+{
+ "textEditor.codeCleanup.profile": "profile1"
+}
diff --git a/src/.editorconfig b/src/.editorconfig
new file mode 100644
index 0000000..e69de29
diff --git a/src/AssemblyInfo.cs b/src/AssemblyInfo.cs
new file mode 100644
index 0000000..81feda2
--- /dev/null
+++ b/src/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) COMPANY-PLACEHOLDER. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Runtime.InteropServices;
+
+[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
new file mode 100644
index 0000000..9ba7818
--- /dev/null
+++ b/src/Directory.Build.props
@@ -0,0 +1,12 @@
+
+
+
+
+ README.md
+
+
+
+
+
+
+
diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets
new file mode 100644
index 0000000..07f4134
--- /dev/null
+++ b/src/Directory.Build.targets
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/src/Library/Calculator.cs b/src/Library/Calculator.cs
new file mode 100644
index 0000000..706c341
--- /dev/null
+++ b/src/Library/Calculator.cs
@@ -0,0 +1,26 @@
+๏ปฟ// Copyright (c) COMPANY-PLACEHOLDER. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Library;
+
+///
+/// My first class.
+///
+public static class Calculator
+{
+ ///
+ /// Adds two integers.
+ ///
+ /// The first integer.
+ /// The second integer.
+ /// The sum of the two integers.
+ public static int Add(int a, int b) => a + b;
+
+ ///
+ /// Subtracts one integer from another.
+ ///
+ /// The original integer.
+ /// The integer to subtract.
+ /// The difference between the two integers.
+ public static int Subtract(int a, int b) => a - b;
+}
diff --git a/src/Library/Library.csproj b/src/Library/Library.csproj
new file mode 100644
index 0000000..50aff99
--- /dev/null
+++ b/src/Library/Library.csproj
@@ -0,0 +1,5 @@
+
+
+ net6.0;netstandard2.0
+
+
diff --git a/strongname.snk b/strongname.snk
new file mode 100644
index 0000000..a4d0a2c
Binary files /dev/null and b/strongname.snk differ
diff --git a/stylecop.json b/stylecop.json
new file mode 100644
index 0000000..6cd4557
--- /dev/null
+++ b/stylecop.json
@@ -0,0 +1,18 @@
+๏ปฟ{
+ "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
+ "settings": {
+ "documentationRules": {
+ "companyName": "COMPANY-PLACEHOLDER",
+ "copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName} license. See {licenseFile} file in the project root for full license information.",
+ "variables": {
+ "licenseName": "MIT",
+ "licenseFile": "LICENSE"
+ },
+ "fileNamingConvention": "metadata",
+ "xmlHeader": false
+ },
+ "orderingRules": {
+ "usingDirectivesPlacement": "outsideNamespace"
+ }
+ }
+}
diff --git a/test/.editorconfig b/test/.editorconfig
new file mode 100644
index 0000000..74dd4a1
--- /dev/null
+++ b/test/.editorconfig
@@ -0,0 +1,55 @@
+[*.cs]
+
+# SA1600: Elements should be documented
+dotnet_diagnostic.SA1600.severity = silent
+
+# SA1601: Partial elements should be documented
+dotnet_diagnostic.SA1601.severity = silent
+
+# SA1602: Enumeration items should be documented
+dotnet_diagnostic.SA1602.severity = silent
+
+# SA1615: Element return value should be documented
+dotnet_diagnostic.SA1615.severity = silent
+
+# VSTHRD103: Call async methods when in an async method
+dotnet_diagnostic.VSTHRD103.severity = silent
+
+# VSTHRD111: Use .ConfigureAwait(bool)
+dotnet_diagnostic.VSTHRD111.severity = none
+
+# VSTHRD200: Use Async suffix for async methods
+dotnet_diagnostic.VSTHRD200.severity = silent
+
+# CA1014: Mark assemblies with CLSCompliant
+dotnet_diagnostic.CA1014.severity = none
+
+# CA1050: Declare types in namespaces
+dotnet_diagnostic.CA1050.severity = none
+
+# CA1303: Do not pass literals as localized parameters
+dotnet_diagnostic.CA1303.severity = none
+
+# CS1591: Missing XML comment for publicly visible type or member
+dotnet_diagnostic.CS1591.severity = silent
+
+# CA1707: Identifiers should not contain underscores
+dotnet_diagnostic.CA1707.severity = silent
+
+# CA1062: Validate arguments of public methods
+dotnet_diagnostic.CA1062.severity = suggestion
+
+# CA1063: Implement IDisposable Correctly
+dotnet_diagnostic.CA1063.severity = silent
+
+# CA1816: Dispose methods should call SuppressFinalize
+dotnet_diagnostic.CA1816.severity = silent
+
+# CA2007: Consider calling ConfigureAwait on the awaited task
+dotnet_diagnostic.CA2007.severity = none
+
+# SA1401: Fields should be private
+dotnet_diagnostic.SA1401.severity = silent
+
+# SA1133: Do not combine attributes
+dotnet_diagnostic.SA1133.severity = silent
diff --git a/test/Directory.Build.props b/test/Directory.Build.props
new file mode 100644
index 0000000..6c7aa71
--- /dev/null
+++ b/test/Directory.Build.props
@@ -0,0 +1,10 @@
+
+
+
+
+
+ false
+ true
+
+
+
diff --git a/test/Directory.Build.targets b/test/Directory.Build.targets
new file mode 100644
index 0000000..a6e0f4a
--- /dev/null
+++ b/test/Directory.Build.targets
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/test/Library.Tests/CalculatorTests.cs b/test/Library.Tests/CalculatorTests.cs
new file mode 100644
index 0000000..0e7e833
--- /dev/null
+++ b/test/Library.Tests/CalculatorTests.cs
@@ -0,0 +1,23 @@
+๏ปฟ// Copyright (c) COMPANY-PLACEHOLDER. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Library;
+using Xunit;
+
+public class CalculatorTests
+{
+ public CalculatorTests()
+ {
+ }
+
+ [Fact]
+ public void AddOrSubtract()
+ {
+ // This tests aggregation of code coverage across test runs.
+#if NET6_0_OR_GREATER
+ Assert.Equal(3, Calculator.Add(1, 2));
+#else
+ Assert.Equal(-1, Calculator.Subtract(1, 2));
+#endif
+ }
+}
diff --git a/test/Library.Tests/Library.Tests.csproj b/test/Library.Tests/Library.Tests.csproj
new file mode 100644
index 0000000..3a6cc04
--- /dev/null
+++ b/test/Library.Tests/Library.Tests.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net6.0;net472
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Library.Tests/app.config b/test/Library.Tests/app.config
new file mode 100644
index 0000000..61890f0
--- /dev/null
+++ b/test/Library.Tests/app.config
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/tools/Check-DotNetRuntime.ps1 b/tools/Check-DotNetRuntime.ps1
new file mode 100644
index 0000000..9d01210
--- /dev/null
+++ b/tools/Check-DotNetRuntime.ps1
@@ -0,0 +1,41 @@
+<#
+.SYNOPSIS
+ Checks whether a given .NET Core runtime is installed.
+#>
+[CmdletBinding()]
+Param (
+ [Parameter()]
+ [ValidateSet('Microsoft.AspNetCore.App','Microsoft.NETCore.App')]
+ [string]$Runtime='Microsoft.NETCore.App',
+ [Parameter(Mandatory=$true)]
+ [Version]$Version
+)
+
+$dotnet = Get-Command dotnet -ErrorAction SilentlyContinue
+if (!$dotnet) {
+ # Nothing is installed.
+ Write-Output $false
+ exit 1
+}
+
+Function IsVersionMatch {
+ Param(
+ [Parameter()]
+ $actualVersion
+ )
+ return $actualVersion -and
+ $Version.Major -eq $actualVersion.Major -and
+ $Version.Minor -eq $actualVersion.Minor -and
+ (($Version.Build -eq -1) -or ($Version.Build -eq $actualVersion.Build)) -and
+ (($Version.Revision -eq -1) -or ($Version.Revision -eq $actualVersion.Revision))
+}
+
+$installedRuntimes = dotnet --list-runtimes |? { $_.Split()[0] -ieq $Runtime } |% { $v = $null; [Version]::tryparse($_.Split()[1], [ref] $v); $v }
+$matchingRuntimes = $installedRuntimes |? { IsVersionMatch -actualVersion $_ }
+if (!$matchingRuntimes) {
+ Write-Output $false
+ exit 1
+}
+
+Write-Output $true
+exit 0
diff --git a/tools/Check-DotNetSdk.ps1 b/tools/Check-DotNetSdk.ps1
new file mode 100644
index 0000000..6c9fa77
--- /dev/null
+++ b/tools/Check-DotNetSdk.ps1
@@ -0,0 +1,37 @@
+<#
+.SYNOPSIS
+ Checks whether the .NET Core SDK required by this repo is installed.
+#>
+[CmdletBinding()]
+Param (
+)
+
+$dotnet = Get-Command dotnet -ErrorAction SilentlyContinue
+if (!$dotnet) {
+ # Nothing is installed.
+ Write-Output $false
+ exit 1
+}
+
+# We need to set the current directory so dotnet considers the SDK required by our global.json file.
+Push-Location "$PSScriptRoot\.."
+try {
+ dotnet -h 2>&1 | Out-Null
+ if (($LASTEXITCODE -eq 129) -or # On Linux
+ ($LASTEXITCODE -eq -2147450751) # On Windows
+ ) {
+ # These exit codes indicate no matching SDK exists.
+ Write-Output $false
+ exit 2
+ }
+
+ # The required SDK is already installed!
+ Write-Output $true
+ exit 0
+} catch {
+ # I don't know why, but on some build agents (e.g. MicroBuild), an exception is thrown from the `dotnet` invocation when a match is not found.
+ Write-Output $false
+ exit 3
+} finally {
+ Pop-Location
+}
diff --git a/tools/Install-DotNetSdk.ps1 b/tools/Install-DotNetSdk.ps1
new file mode 100644
index 0000000..e190fcf
--- /dev/null
+++ b/tools/Install-DotNetSdk.ps1
@@ -0,0 +1,421 @@
+#!/usr/bin/env pwsh
+
+<#
+.SYNOPSIS
+ Installs the .NET SDK specified in the global.json file at the root of this repository,
+ along with supporting .NET runtimes used for testing.
+.DESCRIPTION
+ This MAY not require elevation, as the SDK and runtimes are installed locally to this repo location,
+ unless `-InstallLocality machine` is specified.
+.PARAMETER InstallLocality
+ A value indicating whether dependencies should be installed locally to the repo or at a per-user location.
+ Per-user allows sharing the installed dependencies across repositories and allows use of a shared expanded package cache.
+ Visual Studio will only notice and use these SDKs/runtimes if VS is launched from the environment that runs this script.
+ Per-repo allows for high isolation, allowing for a more precise recreation of the environment within an Azure Pipelines build.
+ When using 'repo', environment variables are set to cause the locally installed dotnet SDK to be used.
+ Per-repo can lead to file locking issues when dotnet.exe is left running as a build server and can be mitigated by running `dotnet build-server shutdown`.
+ Per-machine requires elevation and will download and install all SDKs and runtimes to machine-wide locations so all applications can find it.
+.PARAMETER SdkOnly
+ Skips installing the runtime.
+.PARAMETER IncludeX86
+ Installs a x86 SDK and runtimes in addition to the x64 ones. Only supported on Windows. Ignored on others.
+.PARAMETER IncludeAspNetCore
+ Installs the ASP.NET Core runtime along with the .NET runtime.
+#>
+[CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Medium')]
+Param (
+ [ValidateSet('repo','user','machine')]
+ [string]$InstallLocality='user',
+ [switch]$SdkOnly,
+ [switch]$IncludeX86,
+ [switch]$IncludeAspNetCore
+)
+
+$DotNetInstallScriptRoot = "$PSScriptRoot/../obj/tools"
+if (!(Test-Path $DotNetInstallScriptRoot)) { New-Item -ItemType Directory -Path $DotNetInstallScriptRoot -WhatIf:$false | Out-Null }
+$DotNetInstallScriptRoot = Resolve-Path $DotNetInstallScriptRoot
+
+# Look up actual required .NET SDK version from global.json
+$sdkVersion = & "$PSScriptRoot/../azure-pipelines/variables/DotNetSdkVersion.ps1"
+
+If ($IncludeX86 -and ($IsMacOS -or $IsLinux)) {
+ Write-Verbose "Ignoring -IncludeX86 switch because 32-bit runtimes are only supported on Windows."
+ $IncludeX86 = $false
+}
+
+$arch = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture
+if (!$arch) { # Windows Powershell leaves this blank
+ $arch = 'x64'
+ if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { $arch = 'ARM64' }
+ if (${env:ProgramFiles(Arm)}) { $arch = 'ARM64' }
+}
+
+# Search for all .NET runtime versions referenced from MSBuild projects and arrange to install them.
+$runtimeVersions = @()
+$windowsDesktopRuntimeVersions = @()
+$aspnetRuntimeVersions = @()
+if (!$SdkOnly) {
+ Get-ChildItem "$PSScriptRoot\..\src\*.*proj","$PSScriptRoot\..\test\*.*proj","$PSScriptRoot\..\Directory.Build.props" -Recurse |% {
+ $projXml = [xml](Get-Content -Path $_)
+ $pg = $projXml.Project.PropertyGroup
+ if ($pg) {
+ $targetFrameworks = @()
+ $tf = $pg.TargetFramework
+ $targetFrameworks += $tf
+ $tfs = $pg.TargetFrameworks
+ if ($tfs) {
+ $targetFrameworks = $tfs -Split ';'
+ }
+ }
+ $targetFrameworks |? { $_ -match 'net(?:coreapp)?(\d+\.\d+)' } |% {
+ $v = $Matches[1]
+ $runtimeVersions += $v
+ $aspnetRuntimeVersions += $v
+ if ($v -ge '3.0' -and -not ($IsMacOS -or $IsLinux)) {
+ $windowsDesktopRuntimeVersions += $v
+ }
+ }
+
+ # Add target frameworks of the form: netXX
+ $targetFrameworks |? { $_ -match 'net(\d+\.\d+)' } |% {
+ $v = $Matches[1]
+ $runtimeVersions += $v
+ $aspnetRuntimeVersions += $v
+ if (-not ($IsMacOS -or $IsLinux)) {
+ $windowsDesktopRuntimeVersions += $v
+ }
+ }
+ }
+}
+
+if (!$IncludeAspNetCore) {
+ $aspnetRuntimeVersions = @()
+}
+
+Function Get-FileFromWeb([Uri]$Uri, $OutDir) {
+ $OutFile = Join-Path $OutDir $Uri.Segments[-1]
+ if (!(Test-Path $OutFile)) {
+ Write-Verbose "Downloading $Uri..."
+ if (!(Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null }
+ try {
+ (New-Object System.Net.WebClient).DownloadFile($Uri, $OutFile)
+ } finally {
+ # This try/finally causes the script to abort
+ }
+ }
+
+ $OutFile
+}
+
+Function Get-InstallerExe(
+ $Version,
+ $Architecture,
+ [ValidateSet('Sdk','Runtime','WindowsDesktop')]
+ [string]$sku
+) {
+ # Get the latest/actual version for the specified one
+ $TypedVersion = $null
+ if (![Version]::TryParse($Version, [ref] $TypedVersion)) {
+ Write-Error "Unable to parse $Version into an a.b.c.d version. This version cannot be installed machine-wide."
+ exit 1
+ }
+
+ if ($TypedVersion.Build -eq -1) {
+ $versionInfo = -Split (Invoke-WebRequest -Uri "https://dotnetcli.blob.core.windows.net/dotnet/$sku/$Version/latest.version" -UseBasicParsing)
+ $Version = $versionInfo[-1]
+ }
+
+ $majorMinor = "$($TypedVersion.Major).$($TypedVersion.Minor)"
+ $ReleasesFile = Join-Path $DotNetInstallScriptRoot "$majorMinor\releases.json"
+ if (!(Test-Path $ReleasesFile)) {
+ Get-FileFromWeb -Uri "https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/$majorMinor/releases.json" -OutDir (Split-Path $ReleasesFile) | Out-Null
+ }
+
+ $releases = Get-Content $ReleasesFile | ConvertFrom-Json
+ $url = $null
+ foreach ($release in $releases.releases) {
+ $filesElement = $null
+ if ($release.$sku.version -eq $Version) {
+ $filesElement = $release.$sku.files
+ }
+ if (!$filesElement -and ($sku -eq 'sdk') -and $release.sdks) {
+ foreach ($sdk in $release.sdks) {
+ if ($sdk.version -eq $Version) {
+ $filesElement = $sdk.files
+ break
+ }
+ }
+ }
+
+ if ($filesElement) {
+ foreach ($file in $filesElement) {
+ if ($file.rid -eq "win-$Architecture") {
+ $url = $file.url
+ Break
+ }
+ }
+
+ if ($url) {
+ Break
+ }
+ }
+ }
+
+ if ($url) {
+ Get-FileFromWeb -Uri $url -OutDir $DotNetInstallScriptRoot
+ } else {
+ throw "Unable to find release of $sku v$Version"
+ }
+}
+
+Function Install-DotNet($Version, $Architecture, [ValidateSet('Sdk','Runtime','WindowsDesktop','AspNetCore')][string]$sku = 'Sdk') {
+ Write-Host "Downloading .NET $sku $Version..."
+ $Installer = Get-InstallerExe -Version $Version -Architecture $Architecture -sku $sku
+ Write-Host "Installing .NET $sku $Version..."
+ cmd /c start /wait $Installer /install /passive /norestart
+ if ($LASTEXITCODE -eq 3010) {
+ Write-Verbose "Restart required"
+ } elseif ($LASTEXITCODE -ne 0) {
+ throw "Failure to install .NET SDK"
+ }
+}
+
+$switches = @()
+$envVars = @{
+ # For locally installed dotnet, skip first time experience which takes a long time
+ 'DOTNET_SKIP_FIRST_TIME_EXPERIENCE' = 'true';
+}
+
+if ($InstallLocality -eq 'machine') {
+ if ($IsMacOS -or $IsLinux) {
+ $DotNetInstallDir = '/usr/share/dotnet'
+ } else {
+ $restartRequired = $false
+ if ($PSCmdlet.ShouldProcess(".NET SDK $sdkVersion", "Install")) {
+ Install-DotNet -Version $sdkVersion -Architecture $arch
+ $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
+
+ if ($IncludeX86) {
+ Install-DotNet -Version $sdkVersion -Architecture x86
+ $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
+ }
+ }
+
+ $runtimeVersions | Sort-Object | Get-Unique |% {
+ if ($PSCmdlet.ShouldProcess(".NET runtime $_", "Install")) {
+ Install-DotNet -Version $_ -sku Runtime -Architecture $arch
+ $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
+
+ if ($IncludeX86) {
+ Install-DotNet -Version $_ -sku Runtime -Architecture x86
+ $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
+ }
+ }
+ }
+
+ $windowsDesktopRuntimeVersions | Sort-Object | Get-Unique |% {
+ if ($PSCmdlet.ShouldProcess(".NET Windows Desktop $_", "Install")) {
+ Install-DotNet -Version $_ -sku WindowsDesktop -Architecture $arch
+ $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
+
+ if ($IncludeX86) {
+ Install-DotNet -Version $_ -sku WindowsDesktop -Architecture x86
+ $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
+ }
+ }
+ }
+
+ $aspnetRuntimeVersions | Sort-Object | Get-Unique |% {
+ if ($PSCmdlet.ShouldProcess("ASP.NET Core $_", "Install")) {
+ Install-DotNet -Version $_ -sku AspNetCore -Architecture $arch
+ $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
+
+ if ($IncludeX86) {
+ Install-DotNet -Version $_ -sku AspNetCore -Architecture x86
+ $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
+ }
+ }
+ }
+ if ($restartRequired) {
+ Write-Host -ForegroundColor Yellow "System restart required"
+ Exit 3010
+ }
+
+ return
+ }
+} elseif ($InstallLocality -eq 'repo') {
+ $DotNetInstallDir = "$DotNetInstallScriptRoot/.dotnet"
+ $DotNetX86InstallDir = "$DotNetInstallScriptRoot/x86/.dotnet"
+} elseif ($env:AGENT_TOOLSDIRECTORY) {
+ $DotNetInstallDir = "$env:AGENT_TOOLSDIRECTORY/dotnet"
+ $DotNetX86InstallDir = "$env:AGENT_TOOLSDIRECTORY/x86/dotnet"
+} else {
+ $DotNetInstallDir = Join-Path $HOME .dotnet
+}
+
+if ($DotNetInstallDir) {
+ if (!(Test-Path $DotNetInstallDir)) { New-Item -ItemType Directory -Path $DotNetInstallDir }
+ $DotNetInstallDir = Resolve-Path $DotNetInstallDir
+ Write-Host "Installing .NET SDK and runtimes to $DotNetInstallDir" -ForegroundColor Blue
+ $envVars['DOTNET_MULTILEVEL_LOOKUP'] = '0'
+ $envVars['DOTNET_ROOT'] = $DotNetInstallDir
+}
+
+if ($IncludeX86) {
+ if ($DotNetX86InstallDir) {
+ if (!(Test-Path $DotNetX86InstallDir)) { New-Item -ItemType Directory -Path $DotNetX86InstallDir }
+ $DotNetX86InstallDir = Resolve-Path $DotNetX86InstallDir
+ Write-Host "Installing x86 .NET SDK and runtimes to $DotNetX86InstallDir" -ForegroundColor Blue
+ } else {
+ # Only machine-wide or repo-wide installations can handle two unique dotnet.exe architectures.
+ Write-Error "The installation location or OS isn't supported for x86 installation. Try a different -InstallLocality value."
+ return 1
+ }
+}
+
+if ($IsMacOS -or $IsLinux) {
+ $DownloadUri = "https://raw.githubusercontent.com/dotnet/install-scripts/0b09de9bc136cacb5f849a6957ebd4062173c148/src/dotnet-install.sh"
+ $DotNetInstallScriptPath = "$DotNetInstallScriptRoot/dotnet-install.sh"
+} else {
+ $DownloadUri = "https://raw.githubusercontent.com/dotnet/install-scripts/0b09de9bc136cacb5f849a6957ebd4062173c148/src/dotnet-install.ps1"
+ $DotNetInstallScriptPath = "$DotNetInstallScriptRoot/dotnet-install.ps1"
+}
+
+if (-not (Test-Path $DotNetInstallScriptPath)) {
+ Invoke-WebRequest -Uri $DownloadUri -OutFile $DotNetInstallScriptPath -UseBasicParsing
+ if ($IsMacOS -or $IsLinux) {
+ chmod +x $DotNetInstallScriptPath
+ }
+}
+
+# In case the script we invoke is in a directory with spaces, wrap it with single quotes.
+# In case the path includes single quotes, escape them.
+$DotNetInstallScriptPathExpression = $DotNetInstallScriptPath.Replace("'", "''")
+$DotNetInstallScriptPathExpression = "& '$DotNetInstallScriptPathExpression'"
+
+$anythingInstalled = $false
+$global:LASTEXITCODE = 0
+
+if ($PSCmdlet.ShouldProcess(".NET SDK $sdkVersion", "Install")) {
+ $anythingInstalled = $true
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Version $sdkVersion -Architecture $arch -InstallDir $DotNetInstallDir $switches"
+
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error ".NET SDK installation failure: $LASTEXITCODE"
+ exit $LASTEXITCODE
+ }
+} else {
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Version $sdkVersion -Architecture $arch -InstallDir $DotNetInstallDir $switches -DryRun"
+}
+
+if ($IncludeX86) {
+ if ($PSCmdlet.ShouldProcess(".NET x86 SDK $sdkVersion", "Install")) {
+ $anythingInstalled = $true
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Version $sdkVersion -Architecture x86 -InstallDir $DotNetX86InstallDir $switches"
+
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error ".NET x86 SDK installation failure: $LASTEXITCODE"
+ exit $LASTEXITCODE
+ }
+ } else {
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Version $sdkVersion -Architecture x86 -InstallDir $DotNetX86InstallDir $switches -DryRun"
+ }
+}
+
+$dotnetRuntimeSwitches = $switches + '-Runtime','dotnet'
+
+$runtimeVersions | Sort-Object -Unique |% {
+ if ($PSCmdlet.ShouldProcess(".NET $Arch runtime $_", "Install")) {
+ $anythingInstalled = $true
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches"
+
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error ".NET SDK installation failure: $LASTEXITCODE"
+ exit $LASTEXITCODE
+ }
+ } else {
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches -DryRun"
+ }
+
+ if ($IncludeX86) {
+ if ($PSCmdlet.ShouldProcess(".NET x86 runtime $_", "Install")) {
+ $anythingInstalled = $true
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $dotnetRuntimeSwitches"
+
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error ".NET SDK installation failure: $LASTEXITCODE"
+ exit $LASTEXITCODE
+ }
+ } else {
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $dotnetRuntimeSwitches -DryRun"
+ }
+ }
+}
+
+$windowsDesktopRuntimeSwitches = $switches + '-Runtime','windowsdesktop'
+
+$windowsDesktopRuntimeVersions | Sort-Object -Unique |% {
+ if ($PSCmdlet.ShouldProcess(".NET WindowsDesktop $arch runtime $_", "Install")) {
+ $anythingInstalled = $true
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches"
+
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error ".NET SDK installation failure: $LASTEXITCODE"
+ exit $LASTEXITCODE
+ }
+ } else {
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches -DryRun"
+ }
+
+ if ($IncludeX86) {
+ if ($PSCmdlet.ShouldProcess(".NET WindowsDesktop x86 runtime $_", "Install")) {
+ $anythingInstalled = $true
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $windowsDesktopRuntimeSwitches"
+
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error ".NET SDK installation failure: $LASTEXITCODE"
+ exit $LASTEXITCODE
+ }
+ } else {
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $windowsDesktopRuntimeSwitches -DryRun"
+ }
+ }
+}
+
+$aspnetRuntimeSwitches = $switches + '-Runtime','aspnetcore'
+
+$aspnetRuntimeVersions | Sort-Object -Unique |% {
+ if ($PSCmdlet.ShouldProcess(".NET ASP.NET Core $arch runtime $_", "Install")) {
+ $anythingInstalled = $true
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches"
+
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error ".NET SDK installation failure: $LASTEXITCODE"
+ exit $LASTEXITCODE
+ }
+ } else {
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches -DryRun"
+ }
+
+ if ($IncludeX86) {
+ if ($PSCmdlet.ShouldProcess(".NET ASP.NET Core x86 runtime $_", "Install")) {
+ $anythingInstalled = $true
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $aspnetRuntimeSwitches"
+
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error ".NET SDK installation failure: $LASTEXITCODE"
+ exit $LASTEXITCODE
+ }
+ } else {
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $aspnetRuntimeSwitches -DryRun"
+ }
+ }
+}
+
+if ($PSCmdlet.ShouldProcess("Set DOTNET environment variables to discover these installed runtimes?")) {
+ & "$PSScriptRoot/Set-EnvVars.ps1" -Variables $envVars -PrependPath $DotNetInstallDir | Out-Null
+}
+
+if ($anythingInstalled -and ($InstallLocality -ne 'machine') -and !$env:TF_BUILD -and !$env:GITHUB_ACTIONS) {
+ Write-Warning ".NET runtimes or SDKs were installed to a non-machine location. Perform your builds or open Visual Studio from this same environment in order for tools to discover the location of these dependencies."
+}
diff --git a/tools/Install-NuGetCredProvider.ps1 b/tools/Install-NuGetCredProvider.ps1
new file mode 100755
index 0000000..496049a
--- /dev/null
+++ b/tools/Install-NuGetCredProvider.ps1
@@ -0,0 +1,76 @@
+#!/usr/bin/env pwsh
+
+<#
+.SYNOPSIS
+ Downloads and installs the Microsoft Artifacts Credential Provider
+ from https://github.com/microsoft/artifacts-credprovider
+ to assist in authenticating to Azure Artifact feeds in interactive development
+ or unattended build agents.
+.PARAMETER Force
+ Forces install of the CredProvider plugin even if one already exists. This is useful to upgrade an older version.
+.PARAMETER AccessToken
+ An optional access token for authenticating to Azure Artifacts authenticated feeds.
+#>
+[CmdletBinding()]
+Param (
+ [Parameter()]
+ [switch]$Force,
+ [Parameter()]
+ [string]$AccessToken
+)
+
+$envVars = @{}
+
+$toolsPath = & "$PSScriptRoot\..\azure-pipelines\Get-TempToolsPath.ps1"
+
+if ($IsMacOS -or $IsLinux) {
+ $installerScript = "installcredprovider.sh"
+ $sourceUrl = "https://raw.githubusercontent.com/microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh"
+} else {
+ $installerScript = "installcredprovider.ps1"
+ $sourceUrl = "https://raw.githubusercontent.com/microsoft/artifacts-credprovider/master/helpers/installcredprovider.ps1"
+}
+
+$installerScript = Join-Path $toolsPath $installerScript
+
+if (!(Test-Path $installerScript) -or $Force) {
+ Invoke-WebRequest $sourceUrl -OutFile $installerScript
+}
+
+$installerScript = (Resolve-Path $installerScript).Path
+
+if ($IsMacOS -or $IsLinux) {
+ chmod u+x $installerScript
+}
+
+& $installerScript -Force:$Force -AddNetfx -InstallNet6
+
+if ($AccessToken) {
+ $endpoints = @()
+
+ $endpointURIs = @()
+ Get-ChildItem "$PSScriptRoot\..\nuget.config" -Recurse |% {
+ $nugetConfig = [xml](Get-Content -Path $_)
+
+ $nugetConfig.configuration.packageSources.add |? { ($_.value -match '^https://pkgs\.dev\.azure\.com/') -or ($_.value -match '^https://[\w\-]+\.pkgs\.visualstudio\.com/') } |% {
+ if ($endpointURIs -notcontains $_.Value) {
+ $endpointURIs += $_.Value
+ $endpoint = New-Object -TypeName PSObject
+ Add-Member -InputObject $endpoint -MemberType NoteProperty -Name endpoint -Value $_.value
+ Add-Member -InputObject $endpoint -MemberType NoteProperty -Name username -Value ado
+ Add-Member -InputObject $endpoint -MemberType NoteProperty -Name password -Value $AccessToken
+ $endpoints += $endpoint
+ }
+ }
+ }
+
+ $auth = New-Object -TypeName PSObject
+ Add-Member -InputObject $auth -MemberType NoteProperty -Name endpointCredentials -Value $endpoints
+
+ $authJson = ConvertTo-Json -InputObject $auth
+ $envVars += @{
+ 'VSS_NUGET_EXTERNAL_FEED_ENDPOINTS'=$authJson;
+ }
+}
+
+& "$PSScriptRoot/Set-EnvVars.ps1" -Variables $envVars | Out-Null
diff --git a/tools/Set-EnvVars.ps1 b/tools/Set-EnvVars.ps1
new file mode 100644
index 0000000..3f6f86b
--- /dev/null
+++ b/tools/Set-EnvVars.ps1
@@ -0,0 +1,97 @@
+<#
+.SYNOPSIS
+ Set environment variables in the environment.
+ Azure Pipeline and CMD environments are considered.
+.PARAMETER Variables
+ A hashtable of variables to be set.
+.PARAMETER PrependPath
+ A set of paths to prepend to the PATH environment variable.
+.OUTPUTS
+ A boolean indicating whether the environment variables can be expected to propagate to the caller's environment.
+.DESCRIPTION
+ The CmdEnvScriptPath environment variable may be optionally set to a path to a cmd shell script to be created (or appended to if it already exists) that will set the environment variables in cmd.exe that are set within the PowerShell environment.
+ This is used by init.cmd in order to reapply any new environment variables to the parent cmd.exe process that were set in the powershell child process.
+#>
+[CmdletBinding(SupportsShouldProcess=$true)]
+Param(
+ [Parameter(Mandatory=$true, Position=1)]
+ $Variables,
+ [string[]]$PrependPath
+)
+
+if ($Variables.Count -eq 0) {
+ return $true
+}
+
+$cmdInstructions = !$env:TF_BUILD -and !$env:GITHUB_ACTIONS -and !$env:CmdEnvScriptPath -and ($env:PS1UnderCmd -eq '1')
+if ($cmdInstructions) {
+ Write-Warning "Environment variables have been set that will be lost because you're running under cmd.exe"
+ Write-Host "Environment variables that must be set manually:" -ForegroundColor Blue
+} else {
+ Write-Host "Environment variables set:" -ForegroundColor Blue
+ Write-Host ($Variables | Out-String)
+ if ($PrependPath) {
+ Write-Host "Paths prepended to PATH: $PrependPath"
+ }
+}
+
+if ($env:TF_BUILD) {
+ Write-Host "Azure Pipelines detected. Logging commands will be used to propagate environment variables and prepend path."
+}
+
+if ($env:GITHUB_ACTIONS) {
+ Write-Host "GitHub Actions detected. Logging commands will be used to propagate environment variables and prepend path."
+}
+
+$CmdEnvScript = ''
+$Variables.GetEnumerator() |% {
+ Set-Item -Path env:$($_.Key) -Value $_.Value
+
+ # If we're running in a cloud CI, set these environment variables so they propagate.
+ if ($env:TF_BUILD) {
+ Write-Host "##vso[task.setvariable variable=$($_.Key);]$($_.Value)"
+ }
+ if ($env:GITHUB_ACTIONS) {
+ Add-Content -Path $env:GITHUB_ENV -Value "$($_.Key)=$($_.Value)"
+ }
+
+ if ($cmdInstructions) {
+ Write-Host "SET $($_.Key)=$($_.Value)"
+ }
+
+ $CmdEnvScript += "SET $($_.Key)=$($_.Value)`r`n"
+}
+
+$pathDelimiter = ';'
+if ($IsMacOS -or $IsLinux) {
+ $pathDelimiter = ':'
+}
+
+if ($PrependPath) {
+ $PrependPath |% {
+ $newPathValue = "$_$pathDelimiter$env:PATH"
+ Set-Item -Path env:PATH -Value $newPathValue
+ if ($cmdInstructions) {
+ Write-Host "SET PATH=$newPathValue"
+ }
+
+ if ($env:TF_BUILD) {
+ Write-Host "##vso[task.prependpath]$_"
+ }
+ if ($env:GITHUB_ACTIONS) {
+ Add-Content -Path $env:GITHUB_PATH -Value $_
+ }
+
+ $CmdEnvScript += "SET PATH=$_$pathDelimiter%PATH%"
+ }
+}
+
+if ($env:CmdEnvScriptPath) {
+ if (Test-Path $env:CmdEnvScriptPath) {
+ $CmdEnvScript = (Get-Content -Path $env:CmdEnvScriptPath) + $CmdEnvScript
+ }
+
+ Set-Content -Path $env:CmdEnvScriptPath -Value $CmdEnvScript
+}
+
+return !$cmdInstructions
diff --git a/version.json b/version.json
new file mode 100644
index 0000000..39add98
--- /dev/null
+++ b/version.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
+ "version": "0.1-beta",
+ "publicReleaseRefSpec": [
+ "^refs/heads/main$",
+ "^refs/heads/v\\d+(?:\\.\\d+)?$"
+ ],
+ "cloudBuild": {
+ "setVersionVariables": false
+ }
+}