├── .github ├── config │ └── pr-autoflow.json ├── dependabot.yml └── workflows │ ├── auto_release.yml │ ├── build.yml │ └── dependabot_approve_and_label.yml ├── .gitignore ├── Documentation └── Examples │ ├── DictionaryExtensionsSample.dib │ ├── GIFs │ ├── Collection │ │ └── addRange.gif │ ├── Dictionary │ │ ├── AddIfNotExists.gif │ │ ├── ReplaceIfExists.gif │ │ └── merge.gif │ ├── Enumerable │ │ ├── AllAndAtLeastOne.gif │ │ ├── Concatenate.gif │ │ ├── DistinctBy.gif │ │ ├── DistinctPreserveOrder.gif │ │ └── HasMinimumCount.gif │ ├── LambdaExpression │ │ ├── ExtractPropertyName.gif │ │ └── GetMemberExpression.gif │ ├── List │ │ └── RemoveAll.gif │ ├── String │ │ ├── AsBase64.gif │ │ ├── AsStream.gif │ │ ├── Base64UrlDecode.gif │ │ ├── Base64UrlEncode.gif │ │ ├── EscapeContentType.gif │ │ ├── GetGraphemeClusters.gif │ │ ├── Reverse.gif │ │ ├── ToCamelCase.gif │ │ ├── UnescapeContentType.gif │ │ └── fromBase64.gif │ ├── Task │ │ └── CastWithConversion.gif │ ├── TaskEx │ │ └── WhenAllMany.gif │ └── Traversal │ │ ├── ForEachAsync.gif │ │ ├── ForEachAtIndex.gif │ │ ├── ForEachAtIndexAsync.gif │ │ ├── ForEachFailEnd.gif │ │ ├── ForEachFailEndAsync.gif │ │ ├── ForEachUntilFalse.gif │ │ ├── ForEachUntilFalseAsync.gif │ │ ├── ForEachUntilTrue.gif │ │ └── ForEachUntilTrueAsync.gif │ ├── ICollectionExtensionsSample.dib │ ├── IEnumerableExtensionsSample.dib │ ├── LambdaExpressionExtensionsSample.dib │ ├── ListExtensionsSample.dib │ ├── README.md │ ├── StringExtensionsSample.dib │ ├── TaskExSample.dib │ ├── TaskExtensionsSample.dib │ └── TraversalExtensionsSample.dib ├── GitVersion.yml ├── LICENSE ├── README.md ├── Solutions ├── .editorconfig ├── Corvus.Extensions.Specs │ ├── CollectionExtensionsFeatures │ │ ├── CollectionExtensions.feature │ │ └── CollectionExtensionsSteps.cs │ ├── Corvus.Extensions.Specs.csproj │ ├── EnumerableExtensionsFeatures │ │ ├── EnumerableExtensions.feature │ │ └── EnumerableExtensionsSteps.cs │ ├── SharedBindings │ │ └── CollectionBindings.cs │ ├── StringExtensionsFeatures │ │ ├── Context │ │ │ └── ExampleEnum.cs │ │ ├── StringExtensions.feature │ │ └── StringExtensionsSteps.cs │ ├── TaskExFeatures │ │ ├── TaskExWhenAllMany.feature │ │ └── TaskExWhenAllManySteps.cs │ ├── TaskExtensionsFeatures │ │ ├── Context │ │ │ ├── SimpleChild.cs │ │ │ └── SimpleParent.cs │ │ ├── TaskExtensions.feature │ │ └── TaskExtensionsFeatureSteps.cs │ ├── TraversalExtensionsFeatures │ │ ├── TraversalExtensions.feature │ │ └── TraversalExtensionsSteps.cs │ ├── packages.lock.json │ └── reqnroll.json ├── Corvus.Extensions.sln ├── Corvus.Extensions │ ├── Corvus.Extensions.csproj │ └── Corvus │ │ └── Extensions │ │ ├── CastTo.cs │ │ ├── CollectionExtensions.cs │ │ ├── DictionaryExtensions.cs │ │ ├── EnumerableExtensions.cs │ │ ├── ExceptionMessages.Designer.cs │ │ ├── ExceptionMessages.resx │ │ ├── LambdaExpressionExtensions.cs │ │ ├── ListExtensions.cs │ │ ├── StringExtensions.cs │ │ ├── TaskEx.cs │ │ ├── TaskExtensions.cs │ │ └── TraversalExtensions.cs ├── PackageIcon.png └── stylecop.json ├── build.ps1 └── imm.yaml /.github/config/pr-autoflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "AUTO_MERGE_PACKAGE_WILDCARD_EXPRESSIONS": "[\"Endjin.*\",\"Corvus.*\"]", 3 | "AUTO_RELEASE_PACKAGE_WILDCARD_EXPRESSIONS": "[\"Corvus.AzureFunctionsKeepAlive\",\"Corvus.Configuration\",\"Corvus.ContentHandling\",\"Corvus.Deployment\",\"Corvus.Deployment.Dataverse\",\"Corvus.DotLiquidAsync\",\"Corvus.EventStore\",\"Corvus.Extensions\",\"Corvus.Extensions.CosmosDb\",\"Corvus.Extensions.Newtonsoft.Json\",\"Corvus.Extensions.System.Text.Json\",\"Corvus.Identity\",\"Corvus.JsonSchema\",\"Corvus.Leasing\",\"Corvus.Monitoring\",\"Corvus.Python\",\"Corvus.Retry\",\"Corvus.Storage\",\"Corvus.Tenancy\"]" 4 | } 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: /Solutions 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | groups: 9 | microsoft-identity: 10 | patterns: 11 | - Microsoft.Identity.* 12 | microsoft-extensions: 13 | patterns: 14 | - Microsoft.Extensions.* 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/auto_release.yml: -------------------------------------------------------------------------------- 1 | name: auto_release 2 | on: 3 | pull_request: 4 | types: [closed] 5 | 6 | jobs: 7 | lookup_default_branch: 8 | runs-on: ubuntu-latest 9 | outputs: 10 | branch_name: ${{ steps.lookup_default_branch.outputs.result }} 11 | head_commit: ${{ steps.lookup_default_branch_head.outputs.result }} 12 | steps: 13 | - name: Lookup default branch name 14 | id: lookup_default_branch 15 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 16 | with: 17 | github-token: ${{ secrets.GITHUB_TOKEN }} 18 | retries: 6 # final retry should wait 64 seconds 19 | retry-exempt-status-codes: 400,401,404,422 # GH will raise rate limits with 403 & 429 status codes 20 | result-encoding: string 21 | script: | 22 | const repo = await github.rest.repos.get({ 23 | owner: context.payload.repository.owner.login, 24 | repo: context.payload.repository.name 25 | }); 26 | return repo.data.default_branch 27 | - name: Display default_branch_name 28 | run: | 29 | echo "default_branch_name : ${{ steps.lookup_default_branch.outputs.result }}" 30 | 31 | - name: Lookup HEAD commit on default branch 32 | id: lookup_default_branch_head 33 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 34 | with: 35 | github-token: ${{ secrets.GITHUB_TOKEN }} 36 | retries: 6 # final retry should wait 64 seconds 37 | retry-exempt-status-codes: 400,401,404,422 # GH will raise rate limits with 403 & 429 status codes 38 | result-encoding: string 39 | script: | 40 | const branch = await github.rest.repos.getBranch({ 41 | owner: context.payload.repository.owner.login, 42 | repo: context.payload.repository.name, 43 | branch: '${{ steps.lookup_default_branch.outputs.result }}' 44 | }); 45 | return branch.data.commit.sha 46 | - name: Display default_branch_name_head 47 | run: | 48 | echo "default_branch_head_commit : ${{ steps.lookup_default_branch_head.outputs.result }}" 49 | 50 | check_for_norelease_label: 51 | runs-on: ubuntu-latest 52 | outputs: 53 | no_release: ${{ steps.check_for_norelease_label.outputs.result }} 54 | steps: 55 | - name: Check for 'no_release' label on PR 56 | id: check_for_norelease_label 57 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 58 | with: 59 | github-token: ${{ secrets.GITHUB_TOKEN }} 60 | retries: 6 # final retry should wait 64 seconds 61 | retry-exempt-status-codes: 400,401,404,422 # GH will raise rate limits with 403 & 429 status codes 62 | script: | 63 | const labels = await github.rest.issues.listLabelsOnIssue({ 64 | owner: context.payload.repository.owner.login, 65 | repo: context.payload.repository.name, 66 | issue_number: context.payload.number 67 | }); 68 | core.info("labels: " + JSON.stringify(labels.data)) 69 | if ( labels.data.map(l => l.name).includes("no_release") ) { 70 | core.info("Label found") 71 | if ( labels.data.map(l => l.name).includes("pending_release") ) { 72 | // Remove the 'pending_release' label 73 | await github.rest.issues.removeLabel({ 74 | owner: context.payload.repository.owner.login, 75 | repo: context.payload.repository.name, 76 | issue_number: context.payload.pull_request.number, 77 | name: 'pending_release' 78 | }) 79 | } 80 | 81 | return true 82 | } 83 | return false 84 | - name: Display 'no_release' status 85 | run: | 86 | echo "no_release: ${{ steps.check_for_norelease_label.outputs.result }}" 87 | 88 | check_ready_to_release: 89 | runs-on: ubuntu-latest 90 | needs: [check_for_norelease_label,lookup_default_branch] 91 | if: | 92 | needs.check_for_norelease_label.outputs.no_release == 'false' 93 | outputs: 94 | no_open_prs: ${{ steps.watch_dependabot_prs.outputs.is_complete }} 95 | pending_release_pr_list: ${{ steps.get_release_pending_pr_list.outputs.result }} 96 | ready_to_release: ${{ steps.set_ready_for_release.outputs.result }} 97 | steps: 98 | - name: Get Open PRs 99 | id: get_open_pr_list 100 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 101 | with: 102 | github-token: ${{ secrets.GITHUB_TOKEN }} 103 | retries: 6 # final retry should wait 64 seconds 104 | retry-exempt-status-codes: 400,401,404,422 # GH will raise rate limits with 403 & 429 status codes 105 | # find all open PRs that are targetting the default branch (i.e. main/master) 106 | # return their titles, so they can parsed later to determine if they are 107 | # Dependabot PRs and whether we should wait for them to be auto-merged before 108 | # allowing a release event. 109 | script: | 110 | const pulls = await github.rest.pulls.list({ 111 | owner: context.payload.repository.owner.login, 112 | repo: context.payload.repository.name, 113 | state: 'open', 114 | base: '${{ needs.lookup_default_branch.outputs.branch_name }}' 115 | }); 116 | return JSON.stringify(pulls.data.map(p=>p.title)) 117 | result-encoding: string 118 | - name: Display open_pr_list 119 | run: | 120 | cat <`#${p.number} '${p.title}' in ${p.repository_url}`); 137 | core.info(`allPrs: ${JSON.stringify(allPrs)}`); 138 | 139 | releasePendingPrDetails = pulls.data.items. 140 | filter(function (x) { return x.labels.map(l=>l.name).includes('pending_release') }). 141 | filter(function (x) { return !x.labels.map(l=>l.name).includes('no_release') }). 142 | map(p=>`#${p.number} '${p.title}' in ${p.repository_url}`); 143 | core.info(`releasePendingPrDetails: ${JSON.stringify(releasePendingPrDetails)}`); 144 | 145 | const release_pending_prs = pulls.data.items. 146 | filter(function (x) { return x.labels.map(l=>l.name).includes('pending_release') }). 147 | filter(function (x) { return !x.labels.map(l=>l.name).includes('no_release') }). 148 | map(p=>p.number); 149 | core.info(`release_pending_prs: ${JSON.stringify(release_pending_prs)}`); 150 | core.setOutput('is_release_pending', (release_pending_prs.length > 0)); 151 | return JSON.stringify(release_pending_prs); 152 | result-encoding: string 153 | 154 | - name: Display release_pending_pr_list 155 | run: | 156 | cat <> $GITHUB_PATH 224 | - name: Run GitVersion 225 | id: run_gitversion 226 | run: | 227 | pwsh -noprofile -c 'dotnet-gitversion /diag' 228 | 229 | - name: Generate token 230 | id: generate_token 231 | uses: tibdex/github-app-token@32691ba7c9e7063bd457bd8f2a5703138591fa58 # v1.9 232 | with: 233 | app_id: ${{ secrets.ENDJIN_BOT_APP_ID }} 234 | private_key: ${{ secrets.ENDJIN_BOT_PRIVATE_KEY }} 235 | 236 | - name: Create SemVer tag 237 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 238 | with: 239 | github-token: ${{ steps.generate_token.outputs.token }} 240 | retries: 6 # final retry should wait 64 seconds 241 | retry-exempt-status-codes: 400,401,404,422 # GH will raise rate limits with 403 & 429 status codes 242 | script: | 243 | const uri_path = '/repos/' + context.payload.repository.owner.login + '/' + context.payload.repository.name + '/git/refs' 244 | const tag = await github.request(('POST ' + uri_path), { 245 | owner: context.payload.repository.owner.login, 246 | repo: context.payload.repository.name, 247 | ref: 'refs/tags/${{ env.GitVersion_MajorMinorPatch }}', 248 | sha: '${{ needs.lookup_default_branch.outputs.head_commit }}' 249 | }) 250 | 251 | - name: Remove 'release_pending' label from PRs 252 | id: remove_pending_release_labels 253 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 254 | with: 255 | github-token: '${{ steps.generate_token.outputs.token }}' 256 | retries: 6 # final retry should wait 64 seconds 257 | retry-exempt-status-codes: 400,401,404,422 # GH will raise rate limits with 403 & 429 status codes 258 | script: | 259 | core.info('PRs to unlabel: ${{ needs.check_ready_to_release.outputs.pending_release_pr_list }}') 260 | const pr_list = JSON.parse('${{ needs.check_ready_to_release.outputs.pending_release_pr_list }}') 261 | core.info(`pr_list: ${pr_list}`) 262 | for (const i of pr_list) { 263 | core.info(`Removing label 'pending_release' from issue #${i}`) 264 | github.rest.issues.removeLabel({ 265 | owner: context.payload.repository.owner.login, 266 | repo: context.payload.repository.name, 267 | issue_number: i, 268 | name: 'pending_release' 269 | }); 270 | } 271 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - '*' 8 | pull_request: 9 | branches: 10 | - main 11 | workflow_dispatch: 12 | inputs: 13 | forcePublish: 14 | description: When true the Publish stage will always be run, otherwise it only runs for tagged versions. 15 | required: false 16 | default: false 17 | type: boolean 18 | skipCleanup: 19 | description: When true the pipeline clean-up stage will not be run. For example, the cache used between pipeline stages will be retained. 20 | required: false 21 | default: false 22 | type: boolean 23 | 24 | concurrency: 25 | group: ${{ github.workflow }}-${{ github.sha }} 26 | cancel-in-progress: true 27 | 28 | permissions: 29 | actions: write # enable cache clean-up 30 | checks: write # enable test result annotations 31 | contents: write # enable creating releases 32 | issues: read 33 | packages: write # enable publishing packages 34 | pull-requests: write # enable test result annotations 35 | 36 | jobs: 37 | prepareConfig: 38 | name: Prepare Configuration 39 | runs-on: ubuntu-latest 40 | outputs: 41 | RESOLVED_ENV_VARS: ${{ steps.prepareEnvVarsAndSecrets.outputs.environmentVariablesYamlBase64 }} 42 | RESOLVED_SECRETS: ${{ steps.prepareEnvVarsAndSecrets.outputs.secretsYamlBase64 }} 43 | steps: 44 | # Declare any environment variables and/or secrets that need to be available inside the build process 45 | - uses: endjin/Endjin.RecommendedPractices.GitHubActions/actions/prepare-env-vars-and-secrets@main 46 | id: prepareEnvVarsAndSecrets 47 | with: 48 | environmentVariablesYaml: | 49 | BUILDVAR_NuGetPublishSource: "${{ startsWith(github.ref, 'refs/tags/') && 'https://api.nuget.org/v3/index.json' || format('https://nuget.pkg.github.com/{0}/index.json', github.repository_owner) }}" 50 | secretsYaml: | 51 | NUGET_API_KEY: "${{ startsWith(github.ref, 'refs/tags/') && secrets.NUGET_APIKEY || secrets.BUILD_PUBLISHER_PAT }}" 52 | 53 | build: 54 | needs: prepareConfig 55 | uses: endjin/Endjin.RecommendedPractices.GitHubActions/.github/workflows/scripted-build-matrix-pipeline.yml@main 56 | with: 57 | netSdkVersion: '8.0.x' 58 | # additionalNetSdkVersion: '7.0.x' 59 | # workflow_dispatch inputs are always strings, the type property is just for the UI 60 | forcePublish: ${{ github.event.inputs.forcePublish == 'true' }} 61 | skipCleanup: ${{ github.event.inputs.skipCleanup == 'true' }} 62 | testPhaseMatrixJson: | 63 | { 64 | "os": ["windows-latest", "ubuntu-latest"], 65 | "dotnetFramework": ["net8.0"] 66 | } 67 | # testArtifactName: '' 68 | # testArtifactPath: '' 69 | compilePhaseEnv: ${{ needs.prepareConfig.outputs.RESOLVED_ENV_VARS }} 70 | testPhaseEnv: ${{ needs.prepareConfig.outputs.RESOLVED_ENV_VARS }} 71 | packagePhaseEnv: ${{ needs.prepareConfig.outputs.RESOLVED_ENV_VARS }} 72 | publishPhaseEnv: ${{ needs.prepareConfig.outputs.RESOLVED_ENV_VARS }} 73 | secrets: 74 | compilePhaseAzureCredentials: ${{ secrets.AZURE_READER_CREDENTIALS }} 75 | # testPhaseAzureCredentials: ${{ secrets.TESTS_KV_READER_CREDENTIALS }} 76 | # packagePhaseAzureCredentials: ${{ secrets.AZURE_PUBLISH_CREDENTIALS }} 77 | # publishPhaseAzureCredentials: ${{ secrets.AZURE_PUBLISH_CREDENTIALS }} 78 | # compilePhaseSecrets: ${{ needs.prepareConfig.outputs.RESOLVED_SECRETS }} 79 | # testPhaseSecrets: ${{ needs.prepareConfig.outputs.RESOLVED_SECRETS }} 80 | # packagePhaseSecrets: ${{ needs.prepareConfig.outputs.RESOLVED_SECRETS }} 81 | publishPhaseSecrets: ${{ needs.prepareConfig.outputs.RESOLVED_SECRETS }} -------------------------------------------------------------------------------- /.github/workflows/dependabot_approve_and_label.yml: -------------------------------------------------------------------------------- 1 | name: approve_and_label 2 | on: 3 | pull_request: 4 | types: [opened, reopened] 5 | 6 | permissions: 7 | contents: write 8 | issues: write 9 | pull-requests: write 10 | 11 | jobs: 12 | evaluate_dependabot_pr: 13 | runs-on: ubuntu-latest 14 | name: Parse Dependabot PR title 15 | # Don't process PRs from forked repos 16 | if: 17 | github.event.pull_request.head.repo.full_name == github.repository 18 | outputs: 19 | dependency_name: ${{ steps.parse_dependabot_pr_automerge.outputs.dependency_name }} 20 | version_from: ${{ steps.parse_dependabot_pr_automerge.outputs.version_from }} 21 | version_to: ${{ steps.parse_dependabot_pr_automerge.outputs.version_to }} 22 | is_auto_merge_candidate: ${{ steps.parse_dependabot_pr_automerge.outputs.is_interesting_package }} 23 | is_auto_release_candidate: ${{ steps.parse_dependabot_pr_autorelease.outputs.is_interesting_package }} 24 | semver_increment: ${{ steps.parse_dependabot_pr_automerge.outputs.semver_increment }} 25 | steps: 26 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 27 | - name: Read pr-autoflow configuration 28 | id: get_pr_autoflow_config 29 | uses: endjin/pr-autoflow/actions/read-configuration@v4 30 | with: 31 | config_file: .github/config/pr-autoflow.json 32 | - name: Dependabot PR - AutoMerge Candidate 33 | id: parse_dependabot_pr_automerge 34 | uses: endjin/pr-autoflow/actions/dependabot-pr-parser@v4 35 | with: 36 | pr_title: ${{ github.event.pull_request.title }} 37 | package_wildcard_expressions: ${{ steps.get_pr_autoflow_config.outputs.AUTO_MERGE_PACKAGE_WILDCARD_EXPRESSIONS }} 38 | - name: Dependabot PR - AutoRelease Candidate 39 | id: parse_dependabot_pr_autorelease 40 | uses: endjin/pr-autoflow/actions/dependabot-pr-parser@v4 41 | with: 42 | pr_title: ${{ github.event.pull_request.title }} 43 | package_wildcard_expressions: ${{ steps.get_pr_autoflow_config.outputs.AUTO_RELEASE_PACKAGE_WILDCARD_EXPRESSIONS }} 44 | - name: debug 45 | run: | 46 | echo "dependency_name : ${{ steps.parse_dependabot_pr_automerge.outputs.dependency_name }}" 47 | echo "is_interesting_package (merge) : ${{ steps.parse_dependabot_pr_automerge.outputs.is_interesting_package }}" 48 | echo "is_interesting_package (release) : ${{ steps.parse_dependabot_pr_autorelease.outputs.is_interesting_package }}" 49 | echo "semver_increment : ${{ steps.parse_dependabot_pr_automerge.outputs.semver_increment }}" 50 | 51 | approve: 52 | runs-on: ubuntu-latest 53 | needs: evaluate_dependabot_pr 54 | name: Approve auto-mergeable dependabot PRs 55 | if: | 56 | (github.actor == 'dependabot[bot]' || github.actor == 'dependjinbot[bot]' || github.actor == 'nektos/act') && 57 | needs.evaluate_dependabot_pr.outputs.is_auto_merge_candidate == 'True' 58 | steps: 59 | - name: Show PR Details 60 | run: | 61 | echo "<------------------------------------------------>" 62 | echo "dependency_name : ${{needs.evaluate_dependabot_pr.outputs.dependency_name}}" 63 | echo "semver_increment : ${{needs.evaluate_dependabot_pr.outputs.semver_increment}}" 64 | echo "auto_merge : ${{needs.evaluate_dependabot_pr.outputs.is_auto_merge_candidate}}" 65 | echo "auto_release : ${{needs.evaluate_dependabot_pr.outputs.is_auto_release_candidate}}" 66 | echo "from_version : ${{needs.evaluate_dependabot_pr.outputs.version_from}}" 67 | echo "to_version : ${{needs.evaluate_dependabot_pr.outputs.version_to}}" 68 | echo "<------------------------------------------------>" 69 | shell: bash 70 | - name: Approve pull request 71 | if: | 72 | needs.evaluate_dependabot_pr.outputs.is_auto_merge_candidate == 'True' && 73 | (needs.evaluate_dependabot_pr.outputs.semver_increment == 'minor' || needs.evaluate_dependabot_pr.outputs.semver_increment == 'patch') 74 | run: | 75 | gh pr review "${{ github.event.pull_request.html_url }}" --approve -b "Thank you dependabot 🎊" 76 | env: 77 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | - name: 'Update PR body' 79 | if: | 80 | needs.evaluate_dependabot_pr.outputs.is_auto_merge_candidate == 'True' && 81 | (needs.evaluate_dependabot_pr.outputs.semver_increment == 'minor' || needs.evaluate_dependabot_pr.outputs.semver_increment == 'patch') 82 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 83 | with: 84 | github-token: '${{ secrets.GITHUB_TOKEN }}' 85 | retries: 6 # final retry should wait 64 seconds 86 | retry-exempt-status-codes: 400,401,404,422 # GH will raise rate limits with 403 & 429 status codes 87 | script: | 88 | await github.rest.pulls.update({ 89 | owner: context.payload.repository.owner.login, 90 | repo: context.payload.repository.name, 91 | pull_number: context.payload.pull_request.number, 92 | body: "Bumps '${{needs.evaluate_dependabot_pr.outputs.dependency_name}}' from ${{needs.evaluate_dependabot_pr.outputs.version_from}} to ${{needs.evaluate_dependabot_pr.outputs.version_to}}" 93 | }) 94 | 95 | check_for_norelease_label: 96 | runs-on: ubuntu-latest 97 | outputs: 98 | no_release: ${{ steps.check_for_norelease_label.outputs.result }} 99 | steps: 100 | - name: Check for 'no_release' label on PR 101 | id: check_for_norelease_label 102 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 103 | with: 104 | github-token: ${{ secrets.GITHUB_TOKEN }} 105 | retries: 6 # final retry should wait 64 seconds 106 | retry-exempt-status-codes: 400,401,404,422 # GH will raise rate limits with 403 & 429 status codes 107 | script: | 108 | const labels = await github.rest.issues.listLabelsOnIssue({ 109 | owner: context.payload.repository.owner.login, 110 | repo: context.payload.repository.name, 111 | issue_number: context.payload.number 112 | }); 113 | core.info("labels: " + JSON.stringify(labels.data)) 114 | if ( labels.data.map(l => l.name).includes("no_release") ) { 115 | core.info("Label found") 116 | return true 117 | } 118 | return false 119 | - name: Display 'no_release' status 120 | run: | 121 | echo "no_release: ${{ steps.check_for_norelease_label.outputs.result }}" 122 | 123 | label_auto_merge: 124 | runs-on: ubuntu-latest 125 | needs: 126 | - evaluate_dependabot_pr 127 | - check_for_norelease_label 128 | name: 'Automerge & Label' 129 | steps: 130 | # Get a token for a different identity so any auto-merge that happens in the next step is 131 | # able to trigger other workflows (i.e. our 'auto_release' workflow) 132 | # NOTE: This requires the app details to be defined as 'Dependabot' secrets, rather than 133 | # the usual 'Action' secrets as this workflow is triggered by Dependabot. 134 | - name: Generate token 135 | id: generate_token 136 | uses: tibdex/github-app-token@32691ba7c9e7063bd457bd8f2a5703138591fa58 # v1.9 137 | with: 138 | app_id: ${{ secrets.DEPENDJINBOT_APP_ID }} 139 | private_key: ${{ secrets.DEPENDJINBOT_PRIVATE_KEY }} 140 | # Run the auto-merge in the GitHub App context, so the event can trigger other workflows 141 | - name: 'Set dependabot PR to auto-merge' 142 | if: | 143 | (github.actor == 'dependabot[bot]' || github.actor == 'dependjinbot[bot]' || github.actor == 'nektos/act') && 144 | needs.evaluate_dependabot_pr.outputs.is_auto_merge_candidate == 'True' && 145 | (needs.evaluate_dependabot_pr.outputs.semver_increment == 'minor' || needs.evaluate_dependabot_pr.outputs.semver_increment == 'patch') 146 | run: | 147 | gh pr merge ${{ github.event.pull_request.number }} -R ${{ github.repository }} --auto --squash 148 | env: 149 | GITHUB_TOKEN: '${{ steps.generate_token.outputs.token }}' 150 | - name: 'Label non-dependabot PRs and auto-releasable dependabot PRs with "pending_release"' 151 | if: | 152 | needs.check_for_norelease_label.outputs.no_release == 'false' && 153 | ( 154 | (github.actor != 'dependabot[bot]' && github.actor != 'dependjinbot[bot]') || 155 | needs.evaluate_dependabot_pr.outputs.is_auto_release_candidate == 'True' 156 | ) 157 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 158 | with: 159 | github-token: '${{ secrets.GITHUB_TOKEN }}' 160 | retries: 6 # final retry should wait 64 seconds 161 | retry-exempt-status-codes: 400,401,404,422 # GH will raise rate limits with 403 & 429 status codes 162 | script: | 163 | await github.rest.issues.addLabels({ 164 | owner: context.payload.repository.owner.login, 165 | repo: context.payload.repository.name, 166 | issue_number: context.payload.pull_request.number, 167 | labels: ['pending_release'] 168 | }) 169 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | 332 | # Cobertura code coverage file 333 | coverage.cobertura.xml 334 | 335 | # VS Code config files 336 | /.vscode 337 | 338 | # SpecFlow generated feature files 339 | *.feature.cs 340 | 341 | # Scripted build migration 342 | *.sbom.* 343 | *.covenant.* 344 | _codeCoverage/ 345 | _packages/ -------------------------------------------------------------------------------- /Documentation/Examples/DictionaryExtensionsSample.dib: -------------------------------------------------------------------------------- 1 | #!markdown 2 | 3 | ## DictionaryExtensions - Sample notebook 4 | 5 | #!markdown 6 | 7 | First, add a reference to the `Corvus.Extensions` NuGet package. 8 | 9 | #!csharp 10 | 11 | #r "nuget: Corvus.Extensions, 1.1.4" 12 | 13 | using Corvus.Extensions; 14 | 15 | #!markdown 16 | 17 | ### AddIfNotExists 18 | 19 | Example using the `AddIfNotExists()` extension method. First, a dictionary containing two key value pairs. Using the extension method to add a value that already exists returns the same dictionary. Using it to add a value not already in the dictionary returns the updated dictionary containing the new value. 20 | 21 | #!csharp 22 | 23 | var myDictionary = new Dictionary { { "Hello", 1 }, { "World", 2 } }; 24 | 25 | #!csharp 26 | 27 | myDictionary.AddIfNotExists("Hello", 3); // returns false, does not add to dictionary 28 | 29 | return myDictionary; 30 | 31 | #!csharp 32 | 33 | myDictionary.AddIfNotExists("Goodbye", 3); // returns true, adds to dictionary 34 | 35 | return myDictionary; 36 | 37 | #!markdown 38 | 39 | ### Replace if exists 40 | 41 | #!markdown 42 | 43 | Replaces a value in a key, but only if the key already exists. 44 | 45 | #!csharp 46 | 47 | var myDictionary = new Dictionary { { "Hello", 1 }, { "World", 2 } }; 48 | 49 | #!csharp 50 | 51 | myDictionary.ReplaceIfExists("Hello", 3); // returns true, sets "Hello" to 3 52 | 53 | return myDictionary; 54 | 55 | #!csharp 56 | 57 | myDictionary.ReplaceIfExists("Goodbye", 3); // returns false, adds new key value pair to dictionary 58 | 59 | return myDictionary; 60 | 61 | #!markdown 62 | 63 | ### Merge 64 | 65 | #!markdown 66 | 67 | The union of two dictionaries. Note that this uses AddIfNotExists() semantics, so the values in the first dictionary will be preserved. 68 | 69 | #!csharp 70 | 71 | var myDictionary1 = new Dictionary { { "Hello", 1 }, { "World", 2 } }; 72 | var myDictionary2 = new Dictionary { { "Hello", 3 }, { "Goodbye", 4 } }; 73 | 74 | myDictionary1.Merge(myDictionary2); 75 | 76 | return myDictionary1; 77 | 78 | // Results in: 79 | // { { "Hello", 1 }, { "World", 2 }, { "Goodbye", 4 } } 80 | -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/Collection/addRange.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/Collection/addRange.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/Dictionary/AddIfNotExists.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/Dictionary/AddIfNotExists.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/Dictionary/ReplaceIfExists.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/Dictionary/ReplaceIfExists.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/Dictionary/merge.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/Dictionary/merge.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/Enumerable/AllAndAtLeastOne.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/Enumerable/AllAndAtLeastOne.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/Enumerable/Concatenate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/Enumerable/Concatenate.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/Enumerable/DistinctBy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/Enumerable/DistinctBy.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/Enumerable/DistinctPreserveOrder.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/Enumerable/DistinctPreserveOrder.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/Enumerable/HasMinimumCount.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/Enumerable/HasMinimumCount.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/LambdaExpression/ExtractPropertyName.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/LambdaExpression/ExtractPropertyName.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/LambdaExpression/GetMemberExpression.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/LambdaExpression/GetMemberExpression.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/List/RemoveAll.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/List/RemoveAll.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/String/AsBase64.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/String/AsBase64.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/String/AsStream.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/String/AsStream.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/String/Base64UrlDecode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/String/Base64UrlDecode.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/String/Base64UrlEncode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/String/Base64UrlEncode.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/String/EscapeContentType.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/String/EscapeContentType.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/String/GetGraphemeClusters.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/String/GetGraphemeClusters.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/String/Reverse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/String/Reverse.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/String/ToCamelCase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/String/ToCamelCase.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/String/UnescapeContentType.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/String/UnescapeContentType.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/String/fromBase64.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/String/fromBase64.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/Task/CastWithConversion.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/Task/CastWithConversion.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/TaskEx/WhenAllMany.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/TaskEx/WhenAllMany.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/Traversal/ForEachAsync.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/Traversal/ForEachAsync.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/Traversal/ForEachAtIndex.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/Traversal/ForEachAtIndex.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/Traversal/ForEachAtIndexAsync.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/Traversal/ForEachAtIndexAsync.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/Traversal/ForEachFailEnd.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/Traversal/ForEachFailEnd.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/Traversal/ForEachFailEndAsync.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/Traversal/ForEachFailEndAsync.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/Traversal/ForEachUntilFalse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/Traversal/ForEachUntilFalse.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/Traversal/ForEachUntilFalseAsync.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/Traversal/ForEachUntilFalseAsync.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/Traversal/ForEachUntilTrue.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/Traversal/ForEachUntilTrue.gif -------------------------------------------------------------------------------- /Documentation/Examples/GIFs/Traversal/ForEachUntilTrueAsync.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Documentation/Examples/GIFs/Traversal/ForEachUntilTrueAsync.gif -------------------------------------------------------------------------------- /Documentation/Examples/ICollectionExtensionsSample.dib: -------------------------------------------------------------------------------- 1 | #!markdown 2 | 3 | ## ICollectionExtensions - Sample Notebook 4 | 5 | #!markdown 6 | 7 | First, add a reference to the `Corvus.Extensions` NuGet package. 8 | 9 | #!csharp 10 | 11 | #r "nuget: Corvus.Extensions, 1.1.4" 12 | 13 | using Corvus.Extensions; 14 | 15 | #!markdown 16 | 17 | ### AddRange() 18 | 19 | #!markdown 20 | 21 | Add a range of items to the collection. 22 | 23 | #!csharp 24 | 25 | var myDestinationList = new List { 5,6,7,8 }; 26 | var myItemsList = new List { 1,2,3,4 }; 27 | myDestinationList.AddRange(myItemsList); 28 | 29 | return myDestinationList; 30 | -------------------------------------------------------------------------------- /Documentation/Examples/IEnumerableExtensionsSample.dib: -------------------------------------------------------------------------------- 1 | #!markdown 2 | 3 | ## IEnumerableExtensions - Sample Notebook 4 | 5 | #!markdown 6 | 7 | First, add a reference to the `Corvus.Extensions` NuGet package. 8 | 9 | #!csharp 10 | 11 | #r "nuget: Corvus.Extensions, 1.1.4" 12 | 13 | using Corvus.Extensions; 14 | 15 | #!markdown 16 | 17 | ### DistinctPreserveOrder() 18 | 19 | #!markdown 20 | 21 | This emits an enumerable of the distinct items in the target, preserving their original ordering. 22 | 23 | The built-in LINQ operator [`Distinct`](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.distinct?view=net-7.0#system-linq-enumerable-distinct-1(system-collections-generic-ienumerable((-0)))) can be used to return the distinct elements from a sequence. However, the [documentation](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.distinct?view=net-7.0#system-linq-enumerable-distinct-1(system-collections-generic-ienumerable((-0)))) makes no guarantee about preserving the original ordering of the elements. 24 | 25 | #!csharp 26 | 27 | var list = new List { 1, 2, 3, 2, 4, 5, 6, 3 }; 28 | var result = list.DistinctPreserveOrder(); 29 | 30 | return result; 31 | 32 | #!markdown 33 | 34 | ### DistinctBy() 35 | 36 | #!markdown 37 | 38 | This allows you to provide a function to provide the value for equality comparison for each item. 39 | 40 | #!csharp 41 | 42 | class SomeType 43 | { 44 | public string Prop1 { get; set; } 45 | public int Prop2 { get; set; } 46 | } 47 | 48 | var list = new List { new SomeType { Prop1 = "Hello", Prop2 = 1 }, 49 | new SomeType { Prop1 = "Hello", Prop2 = 3 }, 50 | new SomeType { Prop1 = "World", Prop2 = 1 } }; 51 | var result = list.DistinctBy(i => i.Prop1); 52 | 53 | return result; 54 | 55 | #!markdown 56 | 57 | ### Concatenate() 58 | 59 | #!markdown 60 | 61 | This gives you the ability to concatenate mutiple enumerables, using the params pattern. 62 | 63 | #!csharp 64 | 65 | var list = new List { 1, 2, 3 }; 66 | var list2 = new List { 4, 5, 6 }; 67 | var list3 = new List { 7, 8, 9 }; 68 | var list4 = new List { 10, 11, 12 }; 69 | 70 | return list.Concatenate(list2, list3, list4); 71 | 72 | #!markdown 73 | 74 | ### HasMinimumCount() 75 | 76 | #!markdown 77 | 78 | This determines whether the enumerable has at least a given number of items in it. 79 | 80 | #!csharp 81 | 82 | var list = new List { 1, 2, 3 }; 83 | 84 | #!csharp 85 | 86 | return list.HasMinimumCount(3); 87 | 88 | #!csharp 89 | 90 | return list.HasMinimumCount(4); 91 | 92 | #!markdown 93 | 94 | ### AllAndAtLeastOne() 95 | 96 | #!markdown 97 | 98 | This is an efficient implementation of the combination of the built-in LINQ operators [Any()](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.any?view=net-6.0) & [`All()`](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.all?view=net-6.0) that avoids starting the enumeration twice. It determines if the collection is non-empty, and that every element also matches some predicate. 99 | 100 | This method is also useful because of the counterintuitive behavior of `All()` with empty collections, whereby `items.All()` returns true if `items` is an empty collection. `items.AllAndAtLeastOne()` returns false if `items` is an empty collection, as one of the examples below shows. 101 | 102 | #!csharp 103 | 104 | var list = new List { 1, 2, 3 }; 105 | var list2 = new List(); 106 | 107 | #!csharp 108 | 109 | return list.AllAndAtLeastOne(i => i <= 3); // true 110 | 111 | #!csharp 112 | 113 | return list.AllAndAtLeastOne(i => i <= 2); // false 114 | 115 | #!csharp 116 | 117 | return list2.AllAndAtLeastOne(i => i <= 2); // false 118 | -------------------------------------------------------------------------------- /Documentation/Examples/LambdaExpressionExtensionsSample.dib: -------------------------------------------------------------------------------- 1 | #!markdown 2 | 3 | ## Lambda Expression Extensions - Sample Notebook 4 | 5 | #!markdown 6 | 7 | First, add a reference to the `Corvus.Extensions` NuGet package. 8 | 9 | #!csharp 10 | 11 | #r "nuget: Corvus.Extensions, 1.1.4" 12 | 13 | using Corvus.Extensions; 14 | using System.Linq.Expressions; 15 | 16 | #!markdown 17 | 18 | ### ExtractPropertyName() 19 | 20 | #!markdown 21 | 22 | Extracts a property name from a lambda expression, throwing if that expression is not a `MemberExpression`. 23 | 24 | #!markdown 25 | 26 | This can be useful in code that bridges between .NET types and external storage. In particular, if a library wants to allow C# expressions for writing queries against storage, it can be neccessary to find out which properties a C# expression uses, to be able to work out what that maps to in the external storage. 27 | 28 | #!csharp 29 | 30 | Expression> getter = x => x.Length; 31 | 32 | #!csharp 33 | 34 | getter.ExtractPropertyName() 35 | 36 | #!markdown 37 | 38 | ### GetMemberExpression() 39 | 40 | #!markdown 41 | 42 | Extracts a `MemberExpression` from a `LambdaExpression`, throwing if the body is not a `MemberExpression`. 43 | 44 | #!markdown 45 | 46 | This allows a more direct expression of the expectation that an expression has this particular form. It allows us to avoid cluttering up the code with exception throwing, which can improve readability. 47 | 48 | #!csharp 49 | 50 | getter.GetMemberExpression().Expression 51 | -------------------------------------------------------------------------------- /Documentation/Examples/ListExtensionsSample.dib: -------------------------------------------------------------------------------- 1 | #!markdown 2 | 3 | ## ListExtensions - Sample Notebook 4 | 5 | #!markdown 6 | 7 | First, add a reference to the `Corvus.Extensions` NuGet package. 8 | 9 | #!csharp 10 | 11 | #r "nuget: Corvus.Extensions, 1.1.4" 12 | 13 | #!markdown 14 | 15 | Add using statement for `Corvus.Extensions` namespace 16 | 17 | #!csharp 18 | 19 | using Corvus.Extensions; 20 | 21 | #!markdown 22 | 23 | ### `RemoveAll()` 24 | 25 | Example using the `RemoveAll()` extension method. This removes all items from a list that match a predicate. 26 | 27 | #!markdown 28 | 29 | ### Examples 30 | 31 | #!markdown 32 | 33 | Example 1 using a list of `int` 34 | 35 | #!csharp 36 | 37 | var list = new List { 1, 2, 3, 4, 5, 6 }; 38 | list.RemoveAll(i => i < 3); 39 | list 40 | 41 | // The list now contains: 42 | // { 3, 4, 5, 6 } 43 | 44 | #!markdown 45 | 46 | Example 2 using a list of `string` 47 | 48 | #!csharp 49 | 50 | var list = new List { "dog", "cat", "mouse", "pig", "dog" }; 51 | list.RemoveAll(i => i != "dog"); 52 | list 53 | // The list now contains: 54 | // { "dog", "dog" } 55 | -------------------------------------------------------------------------------- /Documentation/Examples/README.md: -------------------------------------------------------------------------------- 1 | # Corvus.Extensions sample notebooks 2 | 3 | In this folder, you will find a number of notebooks containing examples for the extension methods in the [`Corvus.Extensions\Solutions\Corvus\Extensions`](https://github.com/corvus-dotnet/Corvus.Extensions/tree/main/Solutions/Corvus.Extensions/Corvus/Extensions) folder. 4 | 5 | ## How to use .NET Interactive notebooks 6 | 7 | In order to use .NET Interactive notebooks, download the .NET Interactive Notebooks extensions from the [marketplace](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.dotnet-interactive-vscode). 8 | 9 | You'll need to have the following pre-requisites installed: 10 | - Visual Studio Code (can be downloaded from [here](https://code.visualstudio.com/)) 11 | - the latest [.NET 6 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) 12 | 13 | To run a notebook, open one of the files with the .dib extension in this repository. Select "Run All" from the top menu of the notebook to run all the cells, or run each cell individually by clicking on the "Execute Cell" button on the left of each cell. -------------------------------------------------------------------------------- /Documentation/Examples/StringExtensionsSample.dib: -------------------------------------------------------------------------------- 1 | #!markdown 2 | 3 | ## StringExtensions - Sample Notebook 4 | 5 | #!markdown 6 | 7 | First, add a reference to the `Corvus.Extensions` NuGet package. 8 | 9 | #!csharp 10 | 11 | #r "nuget: Corvus.Extensions, 1.1.4" 12 | 13 | #!markdown 14 | 15 | Add using statement for `Corvus.Extensions` namespace 16 | 17 | #!csharp 18 | 19 | using System.IO; 20 | using Corvus.Extensions; 21 | 22 | #!markdown 23 | 24 | ### `AsBase64()` 25 | 26 | #!markdown 27 | 28 | Convert the provided string to a base 64 representation of its byte representation in a particular encoding. 29 | 30 | #!markdown 31 | 32 | #### Examples 33 | 34 | The example below converts a string's byte representation in Unicode to a base 64 representation. 35 | 36 | #!csharp 37 | 38 | string hello = "Hello"; 39 | string helloEncoded = hello.AsBase64(Encoding.Unicode); 40 | helloEncoded 41 | 42 | #!markdown 43 | 44 | ### `Base64UrlEncode()` 45 | 46 | #!markdown 47 | 48 | Convert the provided string to a base 64 representation of its byte representation in the UTF8 encoding, 49 | with a URL-safe representation. 50 | 51 | #!markdown 52 | 53 | #### Examples 54 | 55 | #!csharp 56 | 57 | string url = "https://www.youtube.com/watch?v=pcc-EXzK3vI"; 58 | string urlEncoded = url.Base64UrlEncode(); 59 | urlEncoded 60 | 61 | #!markdown 62 | 63 | ### `Base64UrlDecode()` 64 | 65 | #!markdown 66 | 67 | Convert the provided string from a base 64 representation of its byte representation in the UTF8 encoding 68 | with a URL-safe representation. 69 | 70 | #!markdown 71 | 72 | #### Examples 73 | 74 | #!csharp 75 | 76 | string urlEncoded = "aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g_dj1wY2MtRVh6SzN2SQ"; 77 | string urlDecoded = urlEncoded.Base64UrlDecode(); 78 | urlDecoded 79 | 80 | #!markdown 81 | 82 | ### `AsStream()` 83 | 84 | Provide a stream over the string in the specified encoding. 85 | 86 | #!markdown 87 | 88 | #### Examples 89 | 90 | #!csharp 91 | 92 | string myString = "hello"; 93 | Stream myStream = myString.AsStream(Encoding.ASCII); 94 | return myStream.ToString(); 95 | 96 | #!markdown 97 | 98 | ### `EscapeContentType()` 99 | 100 | #!markdown 101 | 102 | Escape a content type string. 103 | 104 | #!csharp 105 | 106 | string contentTypeString = "text/html"; 107 | return contentTypeString.EscapeContentType(); 108 | 109 | #!markdown 110 | 111 | ### `FromBase64()` 112 | 113 | #!markdown 114 | 115 | Decode a string from a base64-encoded byte array with the specified text encoding. 116 | 117 | #!csharp 118 | 119 | string base64String = "SGVsbG8="; 120 | return base64String.FromBase64(Encoding.ASCII); 121 | 122 | #!markdown 123 | 124 | ### `GetGraphemeClusters()` 125 | 126 | #!markdown 127 | 128 | Enumerate the grapheme clusters in a string. 129 | 130 | This method is a wrapper around [StringInfo.GetTextElementEnumerator](https://learn.microsoft.com/en-us/dotnet/api/system.globalization.stringinfo.gettextelementenumerator?view=net-7.0), which returns an enumerator that iterates through the text elements of a string. `GetGraphemeClusters()` returns an `IEnumerable`, meaning the functionality can be used with LINQ. 131 | 132 | A note about grapheme clusters. A C# `string` is a sequence of `char`s, however each `char` in a `string` does not necessarily correspond to a visible character. This is where the notion of grapheme clusters come in. For example, the `string` "a" is a grapheme cluster containing one `char`, whilst the `string` "🎉" is a grapheme cluster containing two `char`s, as the example below demonstrates. See [this](https://learn.microsoft.com/en-us/dotnet/standard/base-types/character-encoding-introduction) article for more information on character encodings and grapheme structures in .NET. 133 | 134 | You can think of `GetGraphemeClusters()` as enumerating through the "logical characters" in a string. 135 | 136 | #!markdown 137 | 138 | #### Examples 139 | 140 | #!csharp 141 | 142 | string myString = "hello"; 143 | return myString.GetGraphemeClusters(); 144 | 145 | #!csharp 146 | 147 | string a = "a"; // Grapheme cluster with one char 148 | Console.WriteLine($"Length of string: {a.Length}"); 149 | return a.GetGraphemeClusters(); 150 | 151 | #!csharp 152 | 153 | string celebrationEmoji = "🎉"; // Grapheme cluster with two chars 154 | Console.WriteLine($"Length of string: {celebrationEmoji.Length}"); 155 | return celebrationEmoji.GetGraphemeClusters(); 156 | 157 | #!markdown 158 | 159 | ### `Reverse()` 160 | 161 | Reverse the string. 162 | 163 | #!markdown 164 | 165 | #### Examples 166 | 167 | #!csharp 168 | 169 | string myString = "hello"; 170 | return myString.Reverse(); 171 | 172 | #!markdown 173 | 174 | ### `UnescapeContentType()` 175 | 176 | Unescape a content type string. 177 | 178 | #!markdown 179 | 180 | #### Examples 181 | 182 | #!csharp 183 | 184 | string escapedContentTypeString = "text%60html"; 185 | return escapedContentTypeString.UnescapeContentType(); 186 | 187 | #!markdown 188 | 189 | ### `ToCamelCase()` 190 | 191 | Convert a string to camel case from pascal case. 192 | 193 | #!markdown 194 | 195 | #### Examples 196 | 197 | #!csharp 198 | 199 | string myString = "HelloFromEndjin"; 200 | return myString.ToCamelCase(); 201 | -------------------------------------------------------------------------------- /Documentation/Examples/TaskExSample.dib: -------------------------------------------------------------------------------- 1 | #!markdown 2 | 3 | ## TaskEx - Sample notebook 4 | 5 | #!markdown 6 | 7 | First, add a reference to the `Corvus.Extensions` NuGet package. 8 | 9 | #!csharp 10 | 11 | #r "nuget: Corvus.Extensions, 1.1.4" 12 | 13 | #!markdown 14 | 15 | Add using statement for `Corvus.Extensions` namespace 16 | 17 | #!csharp 18 | 19 | using System.IO; 20 | using Corvus.Extensions; 21 | using Corvus.Extensions.Tasks; 22 | 23 | #!markdown 24 | 25 | ### WhenAllMany() 26 | 27 | Passes the elements of a sequence to a callback that projects each element to a `Task>` and flattens the sequences produced by the resulting tasks into one `Task>`. 28 | 29 | #!markdown 30 | 31 | #### Examples 32 | 33 | The following example uses `WhenAllMany` to asynchronously download all the OPML feeds in a set of feed collections and return a flattened collection of the results. An OPML feed is an XML file containing information like the title, description and link for each post on a given blog site. 34 | 35 | #!csharp 36 | 37 | using System; 38 | using System.Net; 39 | using System.Collections.Generic; 40 | using System.Net.NetworkInformation; 41 | using System.Net.Http; 42 | using System.Threading; 43 | using System.Threading.Tasks; 44 | 45 | #!markdown 46 | 47 | Create a record to represent a feed. 48 | 49 | #!csharp 50 | 51 | public record Feed(string Name, string Author, string Format, Uri Url); 52 | 53 | #!markdown 54 | 55 | Create a class to represent the result of a downloaded feed 56 | 57 | #!csharp 58 | 59 | public class FeedDownloadResult 60 | { 61 | public string Content { get; set; } 62 | 63 | public HttpStatusCode StatusCode { get; set; } 64 | } 65 | 66 | #!markdown 67 | 68 | Create a class to represent a collection of feeds 69 | 70 | #!csharp 71 | 72 | public class FeedCollection 73 | { 74 | public FeedCollection(Feed[] feeds) 75 | { 76 | this.Feeds = feeds; 77 | } 78 | 79 | public IEnumerable Feeds { get; private set; } 80 | 81 | public string Name { get; set; } 82 | 83 | public async Task> GetAllFeedsAsync() 84 | { 85 | List> feedTasks = new List>(); 86 | foreach (Feed feed in this.Feeds) 87 | { 88 | Task feedDownloadResult = GetFeedAsync(feed); 89 | feedTasks.Add(feedDownloadResult); 90 | } 91 | FeedDownloadResult[] results = await Task.WhenAll(feedTasks).ConfigureAwait(false); 92 | return results; 93 | } 94 | 95 | private async Task GetFeedAsync(Feed feed) 96 | { 97 | var request = new HttpRequestMessage(HttpMethod.Get, feed.Url); 98 | HttpClient client = new HttpClient(); 99 | HttpResponseMessage response = await client.SendAsync(request).ConfigureAwait(false); 100 | 101 | return new FeedDownloadResult 102 | { 103 | Content = response.IsSuccessStatusCode ? await response.Content.ReadAsStringAsync() : null, 104 | StatusCode = response.StatusCode 105 | }; 106 | } 107 | 108 | } 109 | 110 | #!markdown 111 | 112 | Create an array of `FeedCollection` objects, each containing multiple `Feed` objects. 113 | 114 | #!csharp 115 | 116 | FeedCollection[] feedCollections = 117 | { 118 | new FeedCollection 119 | ( 120 | new Feed[] 121 | { 122 | new Feed("Azure Weekly", "endjin", "RSS", new Uri("https://azureweekly.info/rss.xml")), 123 | new Feed("Power BI Weekly", "endjin", "RSS", new Uri("https://powerbiweekly.info/rss.xml")) 124 | } 125 | ), 126 | 127 | new FeedCollection 128 | ( 129 | new Feed[] 130 | { 131 | new Feed("endjin.com", "endjin", "RSS", new Uri("https://endjin.com/rss.xml")), 132 | new Feed("Scott Hanselman Blog", "Scott Hanselman", "RSS", new Uri("http://feeds.feedblitz.com/scotthanselman")) 133 | } 134 | ) 135 | }; 136 | 137 | #!markdown 138 | 139 | Define a function that takes a `FeedCollection` object and simply calls the `GetAllFeedsAsync` on it, which will return a `Task>`. 140 | 141 | #!csharp 142 | 143 | Func>> mapper = (feedCollection) => 144 | { 145 | return feedCollection.GetAllFeedsAsync(); 146 | }; 147 | 148 | #!markdown 149 | 150 | Then call the `WhenAllMany` method, passing in our `Func` and the `FeedCollection[]` object defined earlier. As a result, each of the `FeedColection`s in `feedCollections` is going to passed to the `Func`, `mapper`, the resulting sequences produced by the multiple `Task>` will then be flattened into one `Task>`. 151 | 152 | #!csharp 153 | 154 | IList result = await TaskEx.WhenAllMany(feedCollections, mapper); 155 | return result[0].Content; 156 | -------------------------------------------------------------------------------- /Documentation/Examples/TaskExtensionsSample.dib: -------------------------------------------------------------------------------- 1 | #!markdown 2 | 3 | ## TaskExtensions - Sample notebook 4 | 5 | #!markdown 6 | 7 | First, add a reference to the `Corvus.Extensions` NuGet package. 8 | 9 | #!csharp 10 | 11 | #r "nuget: Corvus.Extensions, 1.1.4" 12 | 13 | #!markdown 14 | 15 | Add using statement for `Corvus.Extensions` namespace 16 | 17 | #!csharp 18 | 19 | using Corvus.Extensions; 20 | using System; 21 | using System.Collections.Generic; 22 | using System.Net.NetworkInformation; 23 | using System.Threading; 24 | using System.Threading.Tasks; 25 | 26 | #!markdown 27 | 28 | ## Examples 29 | 30 | #!markdown 31 | 32 | ### `CastWithConversion()` 33 | 34 | Checks to see if a `Task` actually has a result and converts to a `Task` of the relevant type. 35 | 36 | #!csharp 37 | 38 | class Base 39 | { 40 | 41 | } 42 | 43 | class Derived : Base 44 | { 45 | 46 | } 47 | 48 | #!markdown 49 | 50 | The following cell returns an error, as `Task` is not covariant. 51 | 52 | #!csharp 53 | 54 | Task t = Task.FromResult(new Derived()); 55 | 56 | Task tb = (Task)t; 57 | tb.Result 58 | 59 | #!markdown 60 | 61 | Using `CastWithConversion<>()` allows us to cast the `Task` to a `Task`. 62 | 63 | #!csharp 64 | 65 | Task tb = t.CastWithConversion(); 66 | tb.Result 67 | -------------------------------------------------------------------------------- /Documentation/Examples/TraversalExtensionsSample.dib: -------------------------------------------------------------------------------- 1 | #!markdown 2 | 3 | ## Traversal Extensions - Sample Notebook 4 | 5 | #!markdown 6 | 7 | First, add a reference to the `Corvus.Extensions` NuGet package. 8 | 9 | #!csharp 10 | 11 | #r "nuget: Corvus.Extensions, 1.1.4" 12 | 13 | using Corvus.Extensions; 14 | 15 | #!markdown 16 | 17 | ### ForEachAsync() 18 | 19 | #!markdown 20 | 21 | Execute an async action for each item in the enumerable. 22 | 23 | #!csharp 24 | 25 | public async Task IsEqualToOne(int x) 26 | { 27 | await Task.Delay(TimeSpan.FromSeconds(0.5)); 28 | bool result = x.Equals(1); 29 | Console.WriteLine(result); 30 | } 31 | 32 | #!csharp 33 | 34 | var list = new List { 1, 2, 3, 4, 5 }; 35 | await list.ForEachAsync(IsEqualToOne) 36 | 37 | #!markdown 38 | 39 | The `ForEachAsync()` method is here for historical interest. It is now possible to write the following and get the same output: 40 | 41 | #!csharp 42 | 43 | foreach (int x in list) 44 | { 45 | await IsEqualToOne(x); 46 | } 47 | 48 | #!markdown 49 | 50 | ### ForEachAtIndex() 51 | 52 | #!markdown 53 | 54 | Execute an action for each item in the enumerable with the index of the item in the enumerable. 55 | 56 | #!csharp 57 | 58 | static void ConsolePrint(string s, int i) 59 | { 60 | Console.WriteLine(i); 61 | Console.WriteLine(s); 62 | } 63 | 64 | Action printAction = ConsolePrint; 65 | 66 | #!csharp 67 | 68 | IEnumerable list = new List { "foo", "bar", "quux" }; 69 | list.ForEachAtIndex(printAction); 70 | 71 | #!markdown 72 | 73 | Using `ForEachAtIndex` in the previous example is equivalent to doing the following: 74 | 75 | #!csharp 76 | 77 | int i = 0; 78 | 79 | foreach (string v in list) 80 | { 81 | printAction(v, i++); 82 | } 83 | 84 | #!markdown 85 | 86 | ### ForEachAtIndexAsync() 87 | 88 | #!markdown 89 | 90 | Execute an async action for each item in the enumerable, in turn, with the index of the item in the enumerable. 91 | 92 | #!csharp 93 | 94 | static async Task ConsolePrint(string s, int i) 95 | { 96 | await Task.Delay(TimeSpan.FromSeconds(0.5)); 97 | Console.WriteLine(s); 98 | Console.WriteLine(i); 99 | } 100 | 101 | #!csharp 102 | 103 | IEnumerable list = new List { "foo", "bar", "quux" }; 104 | await list.ForEachAtIndexAsync(ConsolePrint); 105 | 106 | #!markdown 107 | 108 | ### ForEachFailEnd() 109 | 110 | #!markdown 111 | 112 | Execute an action for each item in the enumerable. 113 | 114 | If any operation fails, then the enumeration is continued to the end when an Aggregate Exception is thrown containing the exceptions thrown by any failed operations. 115 | 116 | This is useful when cleaning up Azure resources that were set up for testing purposes, for example. It makes sure that even if one step fails, the process doesn't stop, as this would mean that some potentially expensive resources aren't deleted. 117 | 118 | #!csharp 119 | 120 | IEnumerable list = new List { 1, 2, 8, 0, 5 }; 121 | 122 | static void Divide(int x) 123 | { 124 | int result = 10/x; 125 | Console.WriteLine(result); 126 | } 127 | 128 | list.ForEachFailEnd(Divide) 129 | 130 | #!markdown 131 | 132 | ### ForEachFailEndAsync() 133 | 134 | #!markdown 135 | 136 | Execute an async action for each item in the enumerable. 137 | 138 | Returns a task which completes when the enumeration has completed. 139 | 140 | If any operation fails, then the enumeration is continued to the end when an Aggregate Exception is thrown containing the exceptions thrown by any failed operations. 141 | 142 | #!csharp 143 | 144 | IEnumerable list = new List { 1, 2, 8, 0, 5 }; 145 | 146 | public async Task DivideAsync(int x) 147 | { 148 | await Task.Delay(TimeSpan.FromSeconds(0.5)); 149 | int result = 10/x; 150 | Console.WriteLine(result); 151 | } 152 | 153 | await list.ForEachFailEndAsync(DivideAsync) 154 | 155 | #!markdown 156 | 157 | ### ForEachUntilFalse() 158 | 159 | #!markdown 160 | 161 | Execute an action for each item in the enumerable. 162 | 163 | Returns false if the enumeration returned early, otherwise true. 164 | 165 | #!csharp 166 | 167 | IEnumerable list = new List { 1, 2, 8, 0, 5 }; 168 | 169 | static bool IsEqualToZero(int x) 170 | { 171 | bool result = x.Equals(0); 172 | return result; 173 | } 174 | 175 | list.ForEachUntilFalse(IsEqualToZero) 176 | 177 | #!markdown 178 | 179 | ### ForEachUntilFalseAsync() 180 | 181 | #!markdown 182 | 183 | Execute an async action for each item in the enumerable. 184 | 185 | Returns a task whose result is False if the enumeration returned early, otherwise returns true. 186 | 187 | #!csharp 188 | 189 | IEnumerable list = new List { 1, 2, 8, 0, 5 }; 190 | 191 | public async Task IsEqualToZero(int x) 192 | { 193 | await Task.Delay(TimeSpan.FromSeconds(0.5)); 194 | bool result = x.Equals(0); 195 | return result; 196 | } 197 | 198 | await list.ForEachUntilFalseAsync(IsEqualToZero) 199 | 200 | #!markdown 201 | 202 | ### ForEachUntilTrue() 203 | 204 | #!markdown 205 | 206 | Execute an action for each item in the enumerable. 207 | 208 | Returns true if the action terminated early, otherwise false. 209 | 210 | #!csharp 211 | 212 | IEnumerable list = new List { 1, 2, 8, 0, 5 }; 213 | 214 | static bool IsEqualToZero(int x) 215 | { 216 | bool result = x.Equals(0); 217 | return result; 218 | } 219 | 220 | list.ForEachUntilTrue(IsEqualToZero) 221 | 222 | #!markdown 223 | 224 | ### ForEachUntilTrueAsync() 225 | 226 | #!markdown 227 | 228 | Execute an async action for each item in the enumerable. 229 | 230 | Returns a task whose result is True if the action terminated early, otherwise returns False. 231 | 232 | #!csharp 233 | 234 | IEnumerable list = new List { 1, 2, 8, 0, 5 }; 235 | 236 | public async Task IsEqualToZero(int x) 237 | { 238 | await Task.Delay(TimeSpan.FromSeconds(0.5)); 239 | bool result = x.Equals(0); 240 | return result; 241 | } 242 | 243 | await list.ForEachUntilTrueAsync(IsEqualToZero) 244 | -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE 2 | # This file was generated by the pr-autoflow mechanism as a result of executing this action: 3 | # https://github.com/endjin/endjin-codeops/actions/workflows/deploy_pr_autoflow.yml 4 | # This repository participates in this mechanism due to an entry in one of these files: 5 | # https://github.com/endjin/endjin-codeops/blob/main/repo-level-processes/config/live 6 | 7 | mode: ContinuousDeployment 8 | branches: 9 | master: 10 | regex: ^main 11 | tag: preview 12 | increment: patch 13 | dependabot-pr: 14 | regex: ^dependabot 15 | tag: dependabot 16 | source-branches: 17 | - develop 18 | - master 19 | - release 20 | - feature 21 | - support 22 | - hotfix 23 | next-version: "1.1" 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Solutions/.editorconfig: -------------------------------------------------------------------------------- 1 | # Note: this file was added to your solution as a result of one or more projects using the Endjin.RecommendedPractices.Build.Common NuGet package. 2 | # You can edit this file (e.g., to remove these comments), and it will not be updated - the package just checks for its presence, and copies 3 | # this file. If you don't want this file (but you want to use the NuGet package that puts it here), add this setting to all projects 4 | # using Endjin.RecommendedPractices.Build.Common: 5 | # true 6 | # and then delete this file. That setting will prevent the package from recreating this file. 7 | 8 | # Code style rules for enforcing the endjin house style 9 | # See: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference 10 | 11 | # XML project files 12 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 13 | indent_size = 2 14 | 15 | # XML config files 16 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 17 | indent_size = 2 18 | 19 | # C# files 20 | [*.{cs,csx}] 21 | indent_size = 4 22 | insert_final_newline = false 23 | 24 | # Core editorconfig formatting - indentation 25 | 26 | # use soft tabs (spaces) for indentation 27 | indent_style = space 28 | 29 | # Formatting - indentation options 30 | 31 | csharp_indent_case_contents = true 32 | csharp_indent_switch_labels = true 33 | 34 | # Formatting - new line options 35 | 36 | csharp_new_line_before_catch = true 37 | csharp_new_line_before_else = true 38 | csharp_new_line_before_open_brace = object_collection, object_collection_array_initalizers, accessors, lambdas, control_blocks, methods, properties, types 39 | 40 | # Formatting - spacing options 41 | 42 | csharp_space_after_cast = false 43 | csharp_space_after_colon_in_inheritance_clause = true 44 | csharp_space_after_keywords_in_control_flow_statements = true 45 | csharp_space_before_colon_in_inheritance_clause = true 46 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 47 | csharp_space_between_method_call_name_and_opening_parenthesis = false 48 | csharp_space_between_method_call_parameter_list_parentheses = false 49 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 50 | csharp_space_between_method_declaration_parameter_list_parentheses = false 51 | 52 | # Formatting - wrapping options 53 | 54 | csharp_preserve_single_line_blocks = true 55 | csharp_preserve_single_line_statements = true 56 | 57 | 58 | # Style - expression bodied member options 59 | 60 | csharp_style_expression_bodied_accessors = false:none 61 | csharp_style_expression_bodied_constructors = false:none 62 | csharp_style_expression_bodied_methods = false:none 63 | csharp_style_expression_bodied_properties = false:none 64 | 65 | # Style - expression level options 66 | 67 | csharp_style_inlined_variable_declaration = true:suggestion 68 | dotnet_style_predefined_type_for_member_access = true:suggestion 69 | 70 | # Style - implicit and explicit types 71 | 72 | csharp_style_var_for_built_in_types = false:warning 73 | csharp_style_var_when_type_is_apparent = true:warning 74 | csharp_style_var_elsewhere = false:warning 75 | 76 | # Style - language keyword and framework type options 77 | 78 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 79 | 80 | # Style - qualification options 81 | 82 | dotnet_style_qualification_for_field = true:suggestion 83 | dotnet_style_qualification_for_method = true:suggestion 84 | dotnet_style_qualification_for_property = true:suggestion 85 | dotnet_style_qualification_for_event = true:suggestion 86 | 87 | # Style - using directives and namespace declarations 88 | csharp_using_directive_placement = inside_namespace:warning 89 | dotnet_sort_system_directives_first = true 90 | 91 | # Code analyzer rules (formerly in StyleCop.ruleset) 92 | # SA1025 makes it impossible to write decent-looking switch expressions because it 93 | # doesn't allow multiple whitespace characters in a row. There seems to be no way to 94 | # disable this rule just inside switch expressions, so we reluctantly live without it. 95 | dotnet_diagnostic.SA1025.severity = None 96 | # StyleCop's insistence that constructors' summaries must always start with exactly 97 | # the same boilerplate text is not deeply helpful. Saying "Creates a Foo" 98 | # is in most cases better than the "Initializes a new instance of the Foo type" 99 | # that this rule insists on. 100 | dotnet_diagnostic.SA1642.severity = None 101 | 102 | # CLS Compliance - not relevant for most of what we work on. 103 | dotnet_diagnostic.CA1014.severity = None 104 | 105 | # Standard exception constructors - not helpful when the exception absolutely needs certain information 106 | dotnet_diagnostic.CA1032.severity = None 107 | dotnet_diagnostic.RCS1194.severity = None 108 | 109 | # Localize string constants - apps that want localization can turn this back on 110 | dotnet_diagnostic.CA1303.severity = None 111 | -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions.Specs/CollectionExtensionsFeatures/CollectionExtensions.feature: -------------------------------------------------------------------------------- 1 | Feature: CollectionExtensions 2 | In order to manipulate collections 3 | As a developer 4 | I want extension methods 5 | 6 | Scenario Outline: Add a list to an existing list 7 | Given I have a destination list containing 8 | And I have a source list containing 9 | When I call AddRange on the destination list passing in the source list 10 | Then the destination should now be 11 | 12 | Examples: 13 | | DestinationInitialContents | SourceInitialContents | ExpectedResult | 14 | | 1, 2, 3 | 4, 5, 6 | 1, 2, 3, 4, 5, 6 | 15 | | 1, 2, 3 | | 1, 2, 3 | 16 | | | 4, 5, 6 | 4, 5, 6 | 17 | | | | | 18 | | 1, 2, 3 | 2, 3, 4 | 1, 2, 3, 2, 3, 4 | 19 | 20 | Scenario: Add list to null 21 | Given I have a null destination list 22 | And I have a source list containing 1, 2, 3 23 | When I call AddRange on the destination list passing in the source list expecting an exception 24 | Then AddRange should have thrown an ArgumentNullException 25 | 26 | Scenario: Add null to list 27 | Given I have a destination list containing 1, 2, 3 28 | And I have a null source list 29 | When I call AddRange on the destination list passing in the source list expecting an exception 30 | Then AddRange should have thrown an ArgumentNullException 31 | 32 | Scenario: Add a list to itself 33 | Given I have a destination list containing 1, 2, 3 34 | And I use the destination list as the source list 35 | When I call AddRange on the destination list passing in the source list expecting an exception 36 | Then AddRange should have thrown an ArgumentException -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions.Specs/CollectionExtensionsFeatures/CollectionExtensionsSteps.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Endjin Limited. All rights reserved. 3 | // 4 | 5 | namespace Corvus.Extensions.Specs.CollectionExtensionsFeatures 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using NUnit.Framework; 11 | using Reqnroll; 12 | 13 | [Binding] 14 | public class CollectionExtensionsSteps 15 | { 16 | private ICollection? destination; 17 | private List? source; 18 | private Exception? exception; 19 | 20 | [Given("I have a destination list containing (.*)")] 21 | public void GivenIHaveADestinationListContaining(List listContents) 22 | { 23 | this.destination = listContents; 24 | } 25 | 26 | [Given("I have a null destination list")] 27 | public void GivenIHaveANullDestinationList() 28 | { 29 | this.destination = null; 30 | } 31 | 32 | [Given("I have a source list containing (.*)")] 33 | public void GivenIHaveASourceListContaining(List listContents) 34 | { 35 | this.source = listContents; 36 | } 37 | 38 | [Given("I have a null source list")] 39 | public void GivenIHaveANullSourceList() 40 | { 41 | this.source = null; 42 | } 43 | 44 | [Given("I use the destination list as the source list")] 45 | public void GivenIUseTheDestinationListAsTheSourceList() 46 | { 47 | this.source = (List?)this.destination; 48 | } 49 | 50 | [When("I call AddRange on the destination list passing in the source list")] 51 | public void WhenICallAddRangeOnTheDestinationListPassingInTheSourceList() 52 | { 53 | this.destination!.AddRange(this.source!); 54 | } 55 | 56 | [When("I call AddRange on the destination list passing in the source list expecting an exception")] 57 | public void WhenICallAddRangeOnTheDestinationListPassingInTheSourceListExpectingAnException() 58 | { 59 | try 60 | { 61 | this.WhenICallAddRangeOnTheDestinationListPassingInTheSourceList(); 62 | } 63 | catch (ArgumentException ex) 64 | when (ex.GetType() == typeof(ArgumentException) 65 | || ex.GetType() == typeof(ArgumentNullException)) 66 | { 67 | this.exception = ex; 68 | } 69 | } 70 | 71 | [Then("the destination should now be (.*)")] 72 | public void ThenTheDestinationShouldNowBe(List listContents) 73 | { 74 | CollectionAssert.AreEqual(listContents, this.destination); 75 | } 76 | 77 | [Then("AddRange should have thrown an ArgumentNullException")] 78 | public void ThenAddRangeShouldHaveThrownAnArgumentNullException() 79 | { 80 | Assert.IsInstanceOf(this.exception); 81 | } 82 | 83 | [Then("AddRange should have thrown an ArgumentException")] 84 | public void ThenAddRangeShouldHaveThrownAnArgumentException() 85 | { 86 | Assert.IsInstanceOf(this.exception); 87 | } 88 | 89 | [StepArgumentTransformation] 90 | public List TransformToListOfInt(string input) 91 | { 92 | return input.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(text => int.Parse(text)).ToList(); 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions.Specs/Corvus.Extensions.Specs.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | net8.0 6 | enable 7 | false 8 | true 9 | Corvus.Extensions.Specs 10 | 16 | SA0001;SA1204;SA1600;SA1602;CS1591 17 | 18 | 19 | 20 | 29 | true 30 | 31 | 40 | true 41 | 42 | 43 | 44 | 45 | 46 | all 47 | runtime; build; native; contentfiles; analyzers; buildtransitive 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | PreserveNewest 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions.Specs/EnumerableExtensionsFeatures/EnumerableExtensions.feature: -------------------------------------------------------------------------------- 1 | Feature: EnumerableExtensions 2 | In order to manipulate enumerations 3 | As a developer 4 | I want a variety of Linq extensions 5 | 6 | Scenario: A collection of 5 items contains at least 5 items 7 | Given I have a collection with 5 items 8 | When I check if a collection has at least 5 items 9 | Then the result should be true 10 | 11 | Scenario: A collection of 5 items contains at least 6 items 12 | Given I have a collection with 5 items 13 | When I check if a collection has at least 6 items 14 | Then the result should be false 15 | 16 | Scenario: A collection of 5 items contains at least 4 items 17 | Given I have a collection with 5 items 18 | When I check if a collection has at least 4 items 19 | Then the result should be true 20 | 21 | Scenario: A collection of 0 items contains at least 4 items 22 | Given I have a collection with 0 items 23 | When I check if a collection has at least 4 items 24 | Then the result should be false 25 | 26 | Scenario: A collection of 0 items contains at least 0 items 27 | Given I have a collection with 0 items 28 | When I check if a collection has at least 0 items 29 | Then an ArgumentOutOfRangeException should be thrown 30 | 31 | Scenario: A collection of 4 items contains at least 0 items 32 | Given I have a collection with 4 items 33 | When I check if a collection has at least 0 items 34 | Then an ArgumentOutOfRangeException should be thrown 35 | 36 | Scenario: A list of 5 items contains at least 5 items 37 | Given I have a list with 5 items 38 | When I check if a collection has at least 5 items 39 | Then the result should be true 40 | 41 | Scenario: A list of 5 items contains at least 6 items 42 | Given I have a list with 5 items 43 | When I check if a collection has at least 6 items 44 | Then the result should be false 45 | 46 | Scenario: A list of 5 items contains at least 4 items 47 | Given I have a list with 5 items 48 | When I check if a collection has at least 4 items 49 | Then the result should be true 50 | 51 | Scenario: A list of 0 items contains at least 4 items 52 | Given I have a list with 0 items 53 | When I check if a collection has at least 4 items 54 | Then the result should be false 55 | 56 | Scenario: A list of 0 items contains at least 0 items 57 | Given I have a list with 0 items 58 | When I check if a collection has at least 0 items 59 | Then an ArgumentOutOfRangeException should be thrown 60 | 61 | Scenario: A list of 4 items contains at least 0 items 62 | Given I have a list with 4 items 63 | When I check if a collection has at least 0 items 64 | Then an ArgumentOutOfRangeException should be thrown 65 | -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions.Specs/EnumerableExtensionsFeatures/EnumerableExtensionsSteps.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Endjin Limited. All rights reserved. 3 | // 4 | 5 | namespace Corvus.Extensions.Specs.EnumerableExtensionsFeatures 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using Reqnroll; 10 | 11 | [Binding] 12 | public class EnumerableExtensionsSteps 13 | { 14 | private readonly ScenarioContext context; 15 | 16 | public EnumerableExtensionsSteps(ScenarioContext context) 17 | { 18 | this.context = context; 19 | } 20 | 21 | [When("I check if a collection has at least (.*) items")] 22 | public void WhenICheckIfACollectionHasAtLeastItems(int minimumItemCount) 23 | { 24 | try 25 | { 26 | IEnumerable collection = this.context.Get>("Collection"); 27 | bool result = collection.HasMinimumCount(minimumItemCount); 28 | 29 | this.context.Set(result, "Result"); 30 | } 31 | catch (Exception ex) 32 | { 33 | this.context.Set(ex, "Exception"); 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions.Specs/SharedBindings/CollectionBindings.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Endjin Limited. All rights reserved. 3 | // 4 | 5 | namespace Corvus.Extensions.Specs.SharedBindings 6 | { 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using Reqnroll; 10 | 11 | [Binding] 12 | public class CollectionBindings 13 | { 14 | private readonly ScenarioContext context; 15 | 16 | public CollectionBindings(ScenarioContext context) 17 | { 18 | this.context = context; 19 | } 20 | 21 | [Given("I have a collection with (.*) items")] 22 | public void GivenIHaveACollectionWithItems(int numberOfItemsInCollection) 23 | { 24 | IEnumerable collection = Enumerable.Range(1, numberOfItemsInCollection); 25 | this.context.Set(collection, "Collection"); 26 | } 27 | 28 | [Given("I have a list with (.*) items")] 29 | public void GivenIHaveAListWithItems(int numberOfItemsInCollection) 30 | { 31 | var collection = Enumerable.Range(1, numberOfItemsInCollection).ToList(); 32 | this.context.Set(collection, "Collection"); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions.Specs/StringExtensionsFeatures/Context/ExampleEnum.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Endjin Limited. All rights reserved. 3 | // 4 | 5 | namespace Corvus.Extensions.Specs.StringExtensionsFeature.Context 6 | { 7 | internal enum ExampleEnum 8 | { 9 | First, 10 | Second, 11 | Third, 12 | } 13 | } -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions.Specs/StringExtensionsFeatures/StringExtensions.feature: -------------------------------------------------------------------------------- 1 | Feature: StringExtensions 2 | In order to manipulate strings 3 | As a developer 4 | I want a variety of helper methods 5 | 6 | Scenario: Convert a string to a base 64 encoded string 7 | Given the string "I will be base 64 encoded" 8 | When I base 64 encode the string 9 | Then the result should equal "SSB3aWxsIGJlIGJhc2UgNjQgZW5jb2RlZA==" 10 | 11 | Scenario: Convert a string from a base 64 encoded string 12 | Given the string "SSB3YXMgYmFzZSA2NCBlbmNvZGVk" 13 | When I base 64 decode the string 14 | Then the result should equal "I was base 64 encoded" 15 | 16 | Scenario: Convert a string to a base 64 encoded UTF16 string 17 | Given the string "I will be base 64 encoded" 18 | When I base 64 encode the string with UTF16 encoding 19 | Then the result should equal "SQAgAHcAaQBsAGwAIABiAGUAIABiAGEAcwBlACAANgA0ACAAZQBuAGMAbwBkAGUAZAA=" 20 | 21 | Scenario: Convert a string from a base 64 encoded UTF16 string 22 | Given the string "SQAgAHcAYQBzACAAYgBhAHMAZQAgADYANAAgAGUAbgBjAG8AZABlAGQA" 23 | When I base 64 decode the string with UTF16 encoding 24 | Then the result should equal "I was base 64 encoded" 25 | 26 | Scenario: Reverse a string 27 | Given the string "The quick brown fox jumped over the lazy dog" 28 | When I reverse the string 29 | Then the result should equal "god yzal eht revo depmuj xof nworb kciuq ehT" 30 | 31 | Scenario: Reverse a string in arabic (this translates to 'From Wikipedia, the free encyclopedia') 32 | Given the string "از ویکی‌پدیا، دانشنامهٔ آزاد" 33 | When I reverse the string 34 | Then the result should equal "دازآ هٔمانشناد ،ایدپی‌کیو زا" 35 | 36 | Scenario: Get a UTF8 stream from a string 37 | Given the string "The quick brown fox jumped over the lazy dog" 38 | When I get a stream for the string 39 | Then I should be able to use a UTF8 converter to read the string "The quick brown fox jumped over the lazy dog" from the stream 40 | 41 | Scenario: Get a Unicode stream from a string 42 | Given the string "The quick brown fox jumped over the lazy dog" 43 | When I get a Unicode stream for the string 44 | Then I should be able to use a Unicode converter to read the string "The quick brown fox jumped over the lazy dog" from the stream 45 | 46 | Scenario: Get a big-endian Unicode stream from a string 47 | Given the string "The quick brown fox jumped over the lazy dog" 48 | When I get a big endian Unicode stream for the string 49 | Then I should be able to use a big endian Unicode converter to read the string "The quick brown fox jumped over the lazy dog" from the stream 50 | 51 | Scenario: Get a UTF8 stream from a string in arabic 52 | Given the string "از ویکی‌پدیا، دانشنامهٔ آزاد" 53 | When I get a stream for the string 54 | Then I should be able to use a UTF8 converter to read the string "از ویکی‌پدیا، دانشنامهٔ آزاد" from the stream 55 | 56 | Scenario: Get a Unicode stream from a string in arabic 57 | Given the string "از ویکی‌پدیا، دانشنامهٔ آزاد" 58 | When I get a Unicode stream for the string 59 | Then I should be able to use a Unicode converter to read the string "از ویکی‌پدیا، دانشنامهٔ آزاد" from the stream 60 | 61 | Scenario: Get a big-endian Unicode stream from a string in arabic 62 | Given the string "از ویکی‌پدیا، دانشنامهٔ آزاد" 63 | When I get a big endian Unicode stream for the string 64 | Then I should be able to use a big endian Unicode converter to read the string "از ویکی‌پدیا، دانشنامهٔ آزاد" from the stream 65 | -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions.Specs/StringExtensionsFeatures/StringExtensionsSteps.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Endjin Limited. All rights reserved. 3 | // 4 | 5 | namespace Corvus.Extensions.Specs.StringExtensionsFeature 6 | { 7 | using System; 8 | using System.IO; 9 | using System.Text; 10 | using Corvus.Extensions; 11 | using Corvus.Extensions.Specs.StringExtensionsFeature.Context; 12 | using NUnit.Framework; 13 | using Reqnroll; 14 | 15 | [Binding] 16 | public class StringExtensionsSteps 17 | { 18 | private readonly ScenarioContext context; 19 | 20 | public StringExtensionsSteps(ScenarioContext context) 21 | { 22 | this.context = context; 23 | } 24 | 25 | [Given("the current culture is (.*)")] 26 | public void GivenTheCurrentCultureIs(string culture) 27 | { 28 | this.context.Add("Culture", culture); 29 | } 30 | 31 | [Given("the empty string")] 32 | public void GivenTheEmptyString() 33 | { 34 | this.context.Add("Subject", string.Empty); 35 | } 36 | 37 | [Given("the null string")] 38 | public void GivenTheNullString() 39 | { 40 | this.context.Add("NullSubject", true); 41 | } 42 | 43 | [Given(@"the string ""(.*)""")] 44 | public void GivenTheString(string subject) 45 | { 46 | this.context.Add("Subject", subject); 47 | } 48 | 49 | [Then("an ArgumentException should be thrown")] 50 | public void ThenAnArgumentExceptionShouldBeThrown() 51 | { 52 | Assert.IsTrue(this.context.ContainsKey("Exception")); 53 | Assert.IsNotNull(this.context.Get("Exception")); 54 | } 55 | 56 | [Then(@"I should be able to use a big endian Unicode converter to read the string ""(.*)"" from the stream")] 57 | public void ThenIShouldBeAbleToUseABigEndianUnicodeConverterToReadTheStringFromTheStream(string result) 58 | { 59 | using Stream stream = this.GetRequiredResult(); 60 | using var reader = new StreamReader(stream, Encoding.BigEndianUnicode); 61 | Assert.AreEqual(result, reader.ReadToEnd()); 62 | } 63 | 64 | [Then(@"I should be able to use a Unicode converter to read the string ""(.*)"" from the stream")] 65 | public void ThenIShouldBeAbleToUseAUnicodeConverterToReadTheStringFromTheStream(string result) 66 | { 67 | using Stream stream = this.GetRequiredResult(); 68 | using var reader = new StreamReader(stream, Encoding.Unicode); 69 | Assert.AreEqual(result, reader.ReadToEnd()); 70 | } 71 | 72 | [Then(@"I should be able to use a UTF8 converter to read the string ""(.*)"" from the stream")] 73 | public void ThenIShouldBeAbleToUseAUTFConverterToReadTheStringFromTheStream(string result) 74 | { 75 | using Stream stream = this.GetRequiredResult(); 76 | using var reader = new StreamReader(stream, Encoding.UTF8); 77 | Assert.AreEqual(result, reader.ReadToEnd()); 78 | } 79 | 80 | [Then(@"the result should be ExampleEnum\.Second")] 81 | public void ThenTheResultShouldBeExampleEnum_Second() 82 | { 83 | Assert.AreEqual(ExampleEnum.Second, this.context.Get("Result")); 84 | } 85 | 86 | [Then(@"the result should equal ""(.*)""")] 87 | public void ThenTheResultShouldBe(string result) 88 | { 89 | Assert.AreEqual(result, this.GetResult()); 90 | } 91 | 92 | [When("I base 64 encode the string")] 93 | public void WhenIBase64EncodeTheString() 94 | { 95 | this.SetResult(this.GetRequiredSubject().AsBase64()); 96 | } 97 | 98 | [When("I base 64 decode the string")] 99 | public void WhenIBaseDecodeTheString() 100 | { 101 | this.SetResult(this.GetRequiredSubject().FromBase64()); 102 | } 103 | 104 | [When("I base 64 decode the string with UTF16 encoding")] 105 | public void WhenIBaseDecodeTheStringWithUTFEncoding() 106 | { 107 | this.SetResult(this.GetRequiredSubject().FromBase64(Encoding.Unicode)); 108 | } 109 | 110 | [When("I base 64 encode the string with UTF16 encoding")] 111 | public void WhenIBaseEncodeTheStringWithUTFEncoding() 112 | { 113 | this.SetResult(this.GetRequiredSubject().AsBase64(Encoding.Unicode)); 114 | } 115 | 116 | [When("I get a big endian Unicode stream for the string")] 117 | public void WhenIGetABigEndianUnicodeStreamForTheString() 118 | { 119 | this.SetResult(this.GetRequiredSubject().AsStream(Encoding.BigEndianUnicode)); 120 | } 121 | 122 | [When("I get a stream for the string")] 123 | public void WhenIGetAStreamForTheString() 124 | { 125 | this.SetResult(this.GetRequiredSubject().AsStream()); 126 | } 127 | 128 | [When("I get a Unicode stream for the string")] 129 | public void WhenIGetAUnicodeStreamForTheString() 130 | { 131 | this.SetResult(this.GetRequiredSubject().AsStream(Encoding.Unicode)); 132 | } 133 | 134 | [When("I reverse the string")] 135 | public void WhenIReverseTheString() 136 | { 137 | this.SetResult(this.GetRequiredSubject().Reverse()); 138 | } 139 | 140 | private T GetRequiredResult() 141 | { 142 | if (this.context.ContainsKey("NullResult")) 143 | { 144 | throw new InvalidOperationException("The result was null, but this test requires a non-null value"); 145 | } 146 | 147 | return this.context.Get("Result"); 148 | } 149 | 150 | private T? GetResult() 151 | where T : class 152 | { 153 | if (this.context.ContainsKey("NullResult")) 154 | { 155 | return default; 156 | } 157 | 158 | return this.context.Get("Result"); 159 | } 160 | 161 | private string GetRequiredSubject() 162 | { 163 | if (this.context.ContainsKey("NullSubject")) 164 | { 165 | throw new InvalidOperationException("The subject was null, but this test requires a non-null value"); 166 | } 167 | 168 | return this.context.Get("Subject"); 169 | } 170 | 171 | private void SetResult(object result) 172 | { 173 | if (result == null) 174 | { 175 | this.context.Add("NullResult", true); 176 | } 177 | else 178 | { 179 | this.context.Add("Result", result); 180 | } 181 | } 182 | } 183 | } -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions.Specs/TaskExFeatures/TaskExWhenAllMany.feature: -------------------------------------------------------------------------------- 1 | Feature: TaskExWhenAllMany 2 | In order to fan out tasks and collect all of the results 3 | As a developer 4 | I want a combination of Task.WhenALl and SelectMany 5 | 6 | Background: 7 | Given I have a collection with 5 items 8 | When I invoke TaskEx.WhenAllMany 9 | 10 | Scenario: Mapping function invoked for all items 11 | Then the mapping function should have been invoked for all items 12 | 13 | Scenario: Mapping function invoked concurrently 14 | Then the mapping function should have been invoked concurrently 15 | 16 | Scenario: Results from all items in the final result 17 | Then the results from all items should be in the final result 18 | 19 | Scenario: Results produced in consistent order 20 | Then the results should be ordered by the original collection order 21 | And for each original item the results should be in the order that the mapping function returned them -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions.Specs/TaskExFeatures/TaskExWhenAllManySteps.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Endjin Limited. All rights reserved. 3 | // 4 | 5 | namespace Corvus.Extensions.Specs.TaskExFeatures 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Corvus.Extensions.Tasks; 13 | using NUnit.Framework; 14 | using Reqnroll; 15 | 16 | [Binding] 17 | public class TaskExWhenAllManySteps 18 | { 19 | private readonly ScenarioContext context; 20 | private readonly object mapperLock = new(); 21 | private readonly List<(int SourceItem, IList<(int, string)> Result)> mapperInvocationsAndResults = new(); 22 | private IList<(int SourceItem, string Result)>? result; 23 | private int mappersInProgress = 0; 24 | private int maxMappersInProgress = 0; 25 | 26 | public TaskExWhenAllManySteps(ScenarioContext context) 27 | { 28 | this.context = context; 29 | } 30 | 31 | private IEnumerable Collection => this.context.Get>("Collection"); 32 | 33 | [When(@"I invoke TaskEx\.WhenAllMany")] 34 | public async Task WhenIInvokeTaskEx_WhenAllManyAsync() 35 | { 36 | var countdown = new Barrier(this.Collection.Count()); 37 | 38 | async Task> Mapper(int sourceItem) 39 | { 40 | lock (this.mapperLock) 41 | { 42 | this.mappersInProgress++; 43 | this.maxMappersInProgress = Math.Max(this.maxMappersInProgress, this.mappersInProgress); 44 | } 45 | 46 | await Task.Run(() => countdown.SignalAndWait(TimeSpan.FromSeconds(5))).ConfigureAwait(false); 47 | 48 | var result = Enumerable.Range(0, sourceItem).Select(i => (sourceItem, $"{i}!")).ToList(); 49 | 50 | lock (this.mapperLock) 51 | { 52 | this.mapperInvocationsAndResults.Add((sourceItem, result)); 53 | 54 | this.mappersInProgress--; 55 | } 56 | 57 | return result; 58 | } 59 | 60 | this.result = await TaskEx.WhenAllMany(this.Collection, Mapper).ConfigureAwait(false); 61 | } 62 | 63 | [Then("the mapping function should have been invoked for all items")] 64 | public void ThenTheMappingFunctionShouldHaveBeenInvokedForAllItems() 65 | { 66 | Assert.AreEqual(this.Collection.Count(), this.mapperInvocationsAndResults.Count); 67 | } 68 | 69 | [Then("the mapping function should have been invoked concurrently")] 70 | public void ThenTheMappingFunctionShouldHaveBeenInvokedConcurrently() 71 | { 72 | Assert.AreEqual(this.Collection.Count(), this.maxMappersInProgress); 73 | } 74 | 75 | [Then("the results from all items should be in the final result")] 76 | public void ThenTheResultsFromAllItemsShouldBeInTheFinalResult() 77 | { 78 | IEnumerable<(int, string)> expectedResults = this.mapperInvocationsAndResults.SelectMany(r => r.Result); 79 | 80 | CollectionAssert.AreEquivalent(expectedResults, this.result); 81 | } 82 | 83 | [Then("the results should be ordered by the original collection order")] 84 | public void ThenTheResultsShouldBeOrderedByTheOriginalCollectionOrder() 85 | { 86 | if (this.result == null) 87 | { 88 | throw new InvalidOperationException("Result is null"); 89 | } 90 | 91 | IEnumerable groupedResults = this.result.GroupBy(r => r.SourceItem).Select(g => g.Key); 92 | 93 | CollectionAssert.AreEqual(this.Collection, groupedResults); 94 | } 95 | 96 | [Then("for each original item the results should be in the order that the mapping function returned them")] 97 | public void ThenForEachOriginalItemTheResultsShouldBeInTheOrderThatTheMappingFunctionReturnedThem() 98 | { 99 | if (this.result == null) 100 | { 101 | throw new InvalidOperationException("Result is null"); 102 | } 103 | 104 | var groupedResults = this.result.GroupBy(r => r.SourceItem).ToDictionary(x => x.Key, x => x.ToList()); 105 | Dictionary> expectedResults = this.mapperInvocationsAndResults.ToDictionary(x => x.SourceItem, x => x.Result); 106 | 107 | foreach (int i in this.Collection) 108 | { 109 | CollectionAssert.AreEqual(expectedResults[i], groupedResults[i]); 110 | } 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions.Specs/TaskExtensionsFeatures/Context/SimpleChild.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Endjin Limited. All rights reserved. 3 | // 4 | 5 | namespace Corvus.Extensions.SpecsTaskExtensionsFeatures.Context 6 | { 7 | internal class SimpleChild : SimpleParent 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions.Specs/TaskExtensionsFeatures/Context/SimpleParent.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Endjin Limited. All rights reserved. 3 | // 4 | 5 | namespace Corvus.Extensions.SpecsTaskExtensionsFeatures.Context 6 | { 7 | internal class SimpleParent 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions.Specs/TaskExtensionsFeatures/TaskExtensions.feature: -------------------------------------------------------------------------------- 1 | Feature: TaskExtensionsFeature 2 | In order to convert the return type of a task, but retain a Task 3 | As a developer 4 | I want to be able to cast from a Task[ 5 | 6 | Scenario: Cast from a Task between compatible value types 7 | Given I start a task which generates an integer value 1 8 | When I cast the task to an integer 9 | Then the task result should be an integer value 1 10 | 11 | Scenario: Cast from a Task between incompatible value types 12 | Given I start a task which generates an integer value 1 13 | When I cast the task to a double 14 | Then an InvalidCastException should be thrown 15 | 16 | Scenario: Cast from a Task which has no result 17 | Given I start a task with no result 18 | When I cast the task to a double 19 | Then an InvalidCastException should be thrown 20 | 21 | Scenario: Cast from a Task between compatible reference types 22 | Given I start a task which generates a SimpleChild 23 | When I cast the task to a SimpleParent 24 | Then the task result should be a SimpleParent 25 | 26 | Scenario: Cast from a Task between incompatible reference types 27 | Given I start a task which generates a SimpleParent 28 | When I cast the task to a SimpleChild 29 | Then an InvalidCastException should be thrown 30 | 31 | Scenario: Cast from a simple Task between compatible reference types 32 | Given I start a task which generates a SimpleChild 33 | When I cast from a Task to a SimpleParent 34 | Then the task result should be a SimpleParent 35 | 36 | Scenario: Cast from a simple Task between incompatible reference types 37 | Given I start a task which generates a SimpleParent 38 | When I cast from a Task to a SimpleChild 39 | Then an InvalidCastException should be thrown -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions.Specs/TaskExtensionsFeatures/TaskExtensionsFeatureSteps.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Endjin Limited. All rights reserved. 3 | // 4 | 5 | namespace Corvus.Extensions.SpecsTaskExtensionsFeatures 6 | { 7 | using System; 8 | using System.Threading.Tasks; 9 | using Corvus.Extensions.SpecsTaskExtensionsFeatures.Context; 10 | using NUnit.Framework; 11 | using Reqnroll; 12 | 13 | [Binding] 14 | public class TaskExtensionsFeatureSteps 15 | { 16 | private readonly ScenarioContext context; 17 | 18 | public TaskExtensionsFeatureSteps(ScenarioContext context) 19 | { 20 | this.context = context; 21 | } 22 | 23 | [Given("I start a task which generates an integer value (.*)")] 24 | public void GivenIStartATaskWhichGeneratesAnIntegerValue(int p0) 25 | { 26 | this.context.Set(Task.FromResult(p0), "SourceTask"); 27 | } 28 | 29 | [Given("I start a task with no result")] 30 | public void GivenIStartATaskWithNoResult() 31 | { 32 | this.context.Set(Task.CompletedTask, "SourceTask"); 33 | } 34 | 35 | [When("I cast the task to an integer")] 36 | public async Task WhenICastTheTaskToAnInt() 37 | { 38 | Task sourceTask = this.context.Get("SourceTask"); 39 | 40 | try 41 | { 42 | Task result = sourceTask.CastWithConversion(); 43 | this.context.Set(result, "Result"); 44 | await result.ConfigureAwait(false); 45 | } 46 | catch (InvalidCastException ex) 47 | { 48 | await sourceTask.ConfigureAwait(false); 49 | this.context.Set(ex, "Exception"); 50 | } 51 | } 52 | 53 | [Then("the task result should be an integer value (.*)")] 54 | public async Task ThenTheTaskResultShouldBeADoubleValue(int p0) 55 | { 56 | Task task = this.context.Get>("Result"); 57 | int result = await task.ConfigureAwait(false); 58 | Assert.AreEqual(p0, result); 59 | } 60 | 61 | [When("I cast the task to a double")] 62 | public async Task WhenICastTheTaskToAnDouble() 63 | { 64 | Task sourceTask = this.context.Get("SourceTask"); 65 | 66 | try 67 | { 68 | Task result = sourceTask.CastWithConversion(); 69 | this.context.Set(result, "Result"); 70 | await result.ConfigureAwait(false); 71 | } 72 | catch (InvalidCastException ex) 73 | { 74 | await sourceTask.ConfigureAwait(false); 75 | this.context.Set(ex, "Exception"); 76 | } 77 | } 78 | 79 | [Then("an InvalidCastException should be thrown")] 80 | public void ThenAnInvalidCastExceptionShouldBeThrown() 81 | { 82 | Assert.IsTrue(this.context.ContainsKey("Exception")); 83 | Assert.IsNotNull(this.context.Get("Exception")); 84 | } 85 | 86 | [Given("I start a task which generates a SimpleChild")] 87 | public void GivenIStartATaskWhichGeneratesASimpleChild() 88 | { 89 | this.context.Set>(Task.FromResult(new SimpleChild()), "SourceTask"); 90 | } 91 | 92 | [Given("I start a task which generates a SimpleParent")] 93 | public void GivenIStartATaskWhichGeneratesASimpleParent() 94 | { 95 | this.context.Set>(Task.FromResult(new SimpleParent()), "SourceTask"); 96 | } 97 | 98 | [When("I cast the task to a SimpleChild")] 99 | public async Task WhenICastTheTaskToASimpleChild() 100 | { 101 | Task sourceTask = this.context.Get>("SourceTask"); 102 | 103 | try 104 | { 105 | Task result = sourceTask.CastWithConversion(); 106 | this.context.Set(result, "Result"); 107 | await result.ConfigureAwait(false); 108 | } 109 | catch (InvalidCastException ex) 110 | { 111 | await sourceTask.ConfigureAwait(false); 112 | this.context.Set(ex, "Exception"); 113 | } 114 | } 115 | 116 | [When("I cast the task to a SimpleParent")] 117 | public async Task WhenICastTheTaskToASimpleParent() 118 | { 119 | Task sourceTask = this.context.Get>("SourceTask"); 120 | try 121 | { 122 | Task result = sourceTask.CastWithConversion(); 123 | this.context.Set(result, "Result"); 124 | await result.ConfigureAwait(false); 125 | } 126 | catch (InvalidCastException ex) 127 | { 128 | await sourceTask.ConfigureAwait(false); 129 | this.context.Set(ex, "Exception"); 130 | } 131 | } 132 | 133 | [When("I cast from a Task to a SimpleParent")] 134 | public async Task WhenICastFromATaskToASimpleParent() 135 | { 136 | Task sourceTask = this.context.Get>("SourceTask"); 137 | 138 | try 139 | { 140 | Task result = sourceTask.Cast(); 141 | this.context.Set(result, "Result"); 142 | await result.ConfigureAwait(false); 143 | } 144 | catch (InvalidCastException ex) 145 | { 146 | await sourceTask.ConfigureAwait(false); 147 | this.context.Set(ex, "Exception"); 148 | } 149 | } 150 | 151 | [When("I cast from a Task to a SimpleChild")] 152 | public async Task WhenICastFromATaskToASimpleChild() 153 | { 154 | Task sourceTask = this.context.Get>("SourceTask"); 155 | 156 | try 157 | { 158 | Task result = sourceTask.Cast(); 159 | this.context.Set(result, "Result"); 160 | await result.ConfigureAwait(false); 161 | } 162 | catch (InvalidCastException ex) 163 | { 164 | await sourceTask.ConfigureAwait(false); 165 | this.context.Set(ex, "Exception"); 166 | } 167 | } 168 | 169 | [Then("the task result should be a SimpleParent")] 170 | public async Task ThenTheTaskResultShouldBeASimpleParent() 171 | { 172 | if (this.context.ContainsKey("Result")) 173 | { 174 | Task task = this.context.Get>("Result"); 175 | SimpleParent result = await task.ConfigureAwait(false); 176 | Assert.IsInstanceOf(result); 177 | } 178 | } 179 | } 180 | } -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions.Specs/TraversalExtensionsFeatures/TraversalExtensions.feature: -------------------------------------------------------------------------------- 1 | Feature: TraversalExtensions 2 | In order to manipulate enumerations 3 | As a developer 4 | I want a variety of Linq extensions 5 | 6 | Scenario: A collection is enumerated with foreach 7 | Given the following collections 8 | | Collection | Value | 9 | | 1 | 1 | 10 | | 1 | 2 | 11 | | 1 | 3 | 12 | | 1 | 4 | 13 | | 1 | 5 | 14 | When I enumerate collection 1 with foreach add to another collection 15 | Then Collection 2 should match Collection 1 16 | 17 | Scenario: A collection is enumerated with foreach with no action 18 | Given the following collections 19 | | Collection | Value | 20 | | 1 | 1 | 21 | | 1 | 2 | 22 | | 1 | 3 | 23 | | 1 | 4 | 24 | | 1 | 5 | 25 | When I enumerate collection 1 with foreach with no action 26 | Then an ArgumentNullException should be thrown for parameter "onNext" 27 | 28 | Scenario: A collection is enumerated with foreachatindex 29 | Given the following collections 30 | | Collection | Value | 31 | | 1 | 1 | 32 | | 1 | 2 | 33 | | 1 | 3 | 34 | | 1 | 4 | 35 | | 1 | 5 | 36 | When I enumerate collection 1 with foreachatindex add to another collection 37 | Then Collection 2 should match Collection 1 38 | And each index should be passed in order 39 | 40 | Scenario: A collection is enumerated with foreachatindex with no action 41 | Given the following collections 42 | | Collection | Value | 43 | | 1 | 1 | 44 | | 1 | 2 | 45 | | 1 | 3 | 46 | | 1 | 4 | 47 | | 1 | 5 | 48 | When I enumerate collection 1 with foreachatindex with no action 49 | Then an ArgumentNullException should be thrown for parameter "action" 50 | 51 | Scenario: A collection is enumerated with foreachfailend 52 | Given the following collections 53 | | Collection | Value | 54 | | 1 | 1 | 55 | | 1 | 2 | 56 | | 1 | 3 | 57 | | 1 | 4 | 58 | | 1 | 5 | 59 | When I enumerate collection 1 with foreachfailend add to another collection 60 | Then Collection 2 should match Collection 1 61 | 62 | Scenario: A collection is enumerated with foreachfailend with a failing action 63 | Given the following collections 64 | | Collection | Value | 65 | | 1 | 1 | 66 | | 1 | 2 | 67 | | 1 | 3 | 68 | | 1 | 4 | 69 | | 1 | 5 | 70 | When I enumerate collection 1 with foreachfailend with a failing action 71 | Then an Aggregate exception should be thrown containing 5 "InvalidOperationException" instances 72 | 73 | Scenario: A collection is enumerated with foreachfailend with no action 74 | Given the following collections 75 | | Collection | Value | 76 | | 1 | 1 | 77 | | 1 | 2 | 78 | | 1 | 3 | 79 | | 1 | 4 | 80 | | 1 | 5 | 81 | When I enumerate collection 1 with foreachfailend with no action 82 | Then an ArgumentNullException should be thrown for parameter "action" 83 | 84 | Scenario: A null collection is enumerated with foreach 85 | When I enumerate a null collection with foreach 86 | Then an ArgumentNullException should be thrown for parameter "source" 87 | 88 | Scenario: A null collection is enumerated with foreachfailend 89 | When I enumerate a null collection with foreachfailend 90 | Then an ArgumentNullException should be thrown 91 | 92 | Scenario: A null collection is enumerated with foreachatindex 93 | When I enumerate a null collection with foreachatindex 94 | Then an ArgumentNullException should be thrown 95 | 96 | Scenario: A collection is enumerated with foreachasync 97 | Given the following collections 98 | | Collection | Value | 99 | | 1 | 1 | 100 | | 1 | 2 | 101 | | 1 | 3 | 102 | | 1 | 4 | 103 | | 1 | 5 | 104 | When I enumerate collection 1 with foreachasync add to another collection 105 | Then Collection 2 should match Collection 1 106 | 107 | Scenario: A collection is enumerated with foreachasync with no action 108 | Given the following collections 109 | | Collection | Value | 110 | | 1 | 1 | 111 | | 1 | 2 | 112 | | 1 | 3 | 113 | | 1 | 4 | 114 | | 1 | 5 | 115 | When I enumerate collection 1 with foreachasync with no action 116 | Then an ArgumentNullException should be thrown for parameter "action" 117 | 118 | Scenario: A null collection is enumerated with foreachasync 119 | When I enumerate a null collection with foreachasync 120 | Then an ArgumentNullException should be thrown 121 | 122 | Scenario: A collection is enumerated with foreachfailendasync 123 | Given the following collections 124 | | Collection | Value | 125 | | 1 | 1 | 126 | | 1 | 2 | 127 | | 1 | 3 | 128 | | 1 | 4 | 129 | | 1 | 5 | 130 | When I enumerate collection 1 with foreachfailendasync add to another collection 131 | Then Collection 2 should match Collection 1 132 | 133 | Scenario: A collection is enumerated with foreachfailendasync with a failing action 134 | Given the following collections 135 | | Collection | Value | 136 | | 1 | 1 | 137 | | 1 | 2 | 138 | | 1 | 3 | 139 | | 1 | 4 | 140 | | 1 | 5 | 141 | When I enumerate collection 1 with foreachfailendasync with a failing action 142 | Then an Aggregate exception should be thrown containing 5 "InvalidOperationException" instances 143 | 144 | Scenario: A collection is enumerated with foreachfailendasync with no action 145 | Given the following collections 146 | | Collection | Value | 147 | | 1 | 1 | 148 | | 1 | 2 | 149 | | 1 | 3 | 150 | | 1 | 4 | 151 | | 1 | 5 | 152 | When I enumerate collection 1 with foreachfailendasync with no action 153 | Then an ArgumentNullException should be thrown for parameter "action" 154 | 155 | Scenario: A null collection is enumerated with foreachfailendasync 156 | When I enumerate a null collection with foreachfailendasync 157 | Then an ArgumentNullException should be thrown 158 | 159 | 160 | Scenario: A collection is enumerated with foreachatindexasync 161 | Given the following collections 162 | | Collection | Value | 163 | | 1 | 1 | 164 | | 1 | 2 | 165 | | 1 | 3 | 166 | | 1 | 4 | 167 | | 1 | 5 | 168 | When I enumerate collection 1 with foreachatindexasync add to another collection 169 | Then Collection 2 should match Collection 1 170 | And each index should be passed in order 171 | 172 | Scenario: A collection is enumerated with foreachatindexasync with no action 173 | Given the following collections 174 | | Collection | Value | 175 | | 1 | 1 | 176 | | 1 | 2 | 177 | | 1 | 3 | 178 | | 1 | 4 | 179 | | 1 | 5 | 180 | When I enumerate collection 1 with foreachatindexasync with no action 181 | Then an ArgumentNullException should be thrown for parameter "action" 182 | 183 | Scenario: A null collection is enumerated with foreachatindexasync 184 | When I enumerate a null collection with foreachatindexasync 185 | Then an ArgumentNullException should be thrown 186 | 187 | 188 | Scenario: When you merge two dictionaries 189 | Given the following dictionaries 190 | | Dictionary | Key | Value | 191 | | 1 | 1 | 3 | 192 | | 1 | 4 | 1 | 193 | | 1 | 5 | 2 | 194 | | 2 | 1 | 1 | 195 | | 2 | 2 | 3 | 196 | | 2 | 3 | 5 | 197 | | 2 | 4 | 3 | 198 | | 3 | 1 | 1 | 199 | | 3 | 2 | 3 | 200 | | 3 | 3 | 5 | 201 | | 3 | 4 | 3 | 202 | | 3 | 5 | 2 | 203 | When I merge Dictionary 1 with Dictionary 2 204 | Then Dictionary 2 should equal Dictionary 3 -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions.Specs/packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": { 4 | "net8.0": { 5 | "Corvus.Testing.ReqnRoll.NUnit": { 6 | "type": "Direct", 7 | "requested": "[4.0.3, )", 8 | "resolved": "4.0.3", 9 | "contentHash": "S+ZtQJ6+ZoJrN4EWyDzMp1vTJ+/8T7LYIODlN1kg21QuLDVWmKla9wTAR1pi+JV2S9PmOEFaska6UYwb1PIQQw==", 10 | "dependencies": { 11 | "Corvus.Testing.ReqnRoll": "4.0.3", 12 | "Microsoft.NET.Test.Sdk": "17.12.0", 13 | "NUnit3TestAdapter": "4.6.0", 14 | "Reqnroll.NUnit": "2.2.1", 15 | "coverlet.msbuild": "6.0.2" 16 | } 17 | }, 18 | "Endjin.RecommendedPractices.GitHub": { 19 | "type": "Direct", 20 | "requested": "[2.1.15, )", 21 | "resolved": "2.1.15", 22 | "contentHash": "9aF/aMeS2+oUb15XRiVp5clB9DuXtYx+Ld5usvwegjickNEa5taX3tCMjkevgAtipIIgXA2X33lzjQu+nRBS4A==", 23 | "dependencies": { 24 | "Endjin.RecommendedPractices": "2.1.15", 25 | "Microsoft.SourceLink.GitHub": "1.1.1" 26 | } 27 | }, 28 | "Roslynator.Analyzers": { 29 | "type": "Direct", 30 | "requested": "[4.12.10, )", 31 | "resolved": "4.12.10", 32 | "contentHash": "Wecq3nfhTvnJPxX87hKbjMdX1xeCAMJf9rdBX3nQLTntQs9v0fFbUB2eQSSOArXMuFh7MxjLWaL4+b6XMi1NDA==" 33 | }, 34 | "StyleCop.Analyzers": { 35 | "type": "Direct", 36 | "requested": "[1.2.0-beta.556, )", 37 | "resolved": "1.2.0-beta.556", 38 | "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", 39 | "dependencies": { 40 | "StyleCop.Analyzers.Unstable": "1.2.0.556" 41 | } 42 | }, 43 | "Corvus.Testing.ReqnRoll": { 44 | "type": "Transitive", 45 | "resolved": "4.0.3", 46 | "contentHash": "kEUdpWI/zqpDUB06BVlTP0QGM11adk5gSJsJW07u1S+tvqsI2Q5gs7E1aBGYggyuOMMwXDHg2jjHBQJPJCDOIQ==", 47 | "dependencies": { 48 | "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", 49 | "Microsoft.Extensions.DependencyInjection": "8.0.0", 50 | "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", 51 | "Reqnroll": "2.2.1" 52 | } 53 | }, 54 | "coverlet.msbuild": { 55 | "type": "Transitive", 56 | "resolved": "6.0.2", 57 | "contentHash": "8b4jBNH7mcQy1otyQErjjIUuGD74XxKZ1wvDufbY7jhWwckl7wIa+icjwdPYeI0aYMS4Tp63LIZvyMFjWwOMDw==" 58 | }, 59 | "Cucumber.CucumberExpressions": { 60 | "type": "Transitive", 61 | "resolved": "17.1.0", 62 | "contentHash": "IZFDLLwrUCKuGDXQzmAWZa+kQFpVmVwWD8wEl0UWGOR83UQfzqg8hZeSSNUOXlW0Kh3vMO6kEm3FpNC4amKMaw==" 63 | }, 64 | "Endjin.RecommendedPractices": { 65 | "type": "Transitive", 66 | "resolved": "2.1.15", 67 | "contentHash": "PHl/jXZTYfsPa/Ef27aEbbofvhe3hGHtY2Tbp/YlxTdQE2Z+XLoWvoilSWpzb96tWWCuHqCwwjD2sv5dktxztw==", 68 | "dependencies": { 69 | "Microsoft.Build.Tasks.Git": "1.1.1" 70 | } 71 | }, 72 | "Gherkin": { 73 | "type": "Transitive", 74 | "resolved": "30.0.0", 75 | "contentHash": "jZM9zm9eg62vRSAOtYvcmQh4w0oYqjP6myKxMaJAlQq9VCKm4wnTfHhS5QRnLhx/sH3R5jTaoQAKLs5hhjuq7g==" 76 | }, 77 | "Microsoft.Build.Tasks.Git": { 78 | "type": "Transitive", 79 | "resolved": "1.1.1", 80 | "contentHash": "AT3HlgTjsqHnWpBHSNeR0KxbLZD7bztlZVj7I8vgeYG9SYqbeFGh0TM/KVtC6fg53nrWHl3VfZFvb5BiQFcY6Q==" 81 | }, 82 | "Microsoft.CodeCoverage": { 83 | "type": "Transitive", 84 | "resolved": "17.12.0", 85 | "contentHash": "4svMznBd5JM21JIG2xZKGNanAHNXplxf/kQDFfLHXQ3OnpJkayRK/TjacFjA+EYmoyuNXHo/sOETEfcYtAzIrA==" 86 | }, 87 | "Microsoft.Extensions.Configuration.Abstractions": { 88 | "type": "Transitive", 89 | "resolved": "8.0.0", 90 | "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", 91 | "dependencies": { 92 | "Microsoft.Extensions.Primitives": "8.0.0" 93 | } 94 | }, 95 | "Microsoft.Extensions.DependencyInjection": { 96 | "type": "Transitive", 97 | "resolved": "8.0.0", 98 | "contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==", 99 | "dependencies": { 100 | "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" 101 | } 102 | }, 103 | "Microsoft.Extensions.DependencyInjection.Abstractions": { 104 | "type": "Transitive", 105 | "resolved": "8.0.1", 106 | "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" 107 | }, 108 | "Microsoft.Extensions.DependencyModel": { 109 | "type": "Transitive", 110 | "resolved": "8.0.2", 111 | "contentHash": "mUBDZZRgZrSyFOsJ2qJJ9fXfqd/kXJwf3AiDoqLD9m6TjY5OO/vLNOb9fb4juC0487eq4hcGN/M2Rh/CKS7QYw==" 112 | }, 113 | "Microsoft.Extensions.Primitives": { 114 | "type": "Transitive", 115 | "resolved": "8.0.0", 116 | "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" 117 | }, 118 | "Microsoft.NET.Test.Sdk": { 119 | "type": "Transitive", 120 | "resolved": "17.12.0", 121 | "contentHash": "kt/PKBZ91rFCWxVIJZSgVLk+YR+4KxTuHf799ho8WNiK5ZQpJNAEZCAWX86vcKrs+DiYjiibpYKdGZP6+/N17w==", 122 | "dependencies": { 123 | "Microsoft.CodeCoverage": "17.12.0", 124 | "Microsoft.TestPlatform.TestHost": "17.12.0" 125 | } 126 | }, 127 | "Microsoft.NETCore.Platforms": { 128 | "type": "Transitive", 129 | "resolved": "1.1.0", 130 | "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" 131 | }, 132 | "Microsoft.NETCore.Targets": { 133 | "type": "Transitive", 134 | "resolved": "1.1.0", 135 | "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" 136 | }, 137 | "Microsoft.SourceLink.Common": { 138 | "type": "Transitive", 139 | "resolved": "1.1.1", 140 | "contentHash": "WMcGpWKrmJmzrNeuaEb23bEMnbtR/vLmvZtkAP5qWu7vQsY59GqfRJd65sFpBszbd2k/bQ8cs8eWawQKAabkVg==" 141 | }, 142 | "Microsoft.SourceLink.GitHub": { 143 | "type": "Transitive", 144 | "resolved": "1.1.1", 145 | "contentHash": "IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==", 146 | "dependencies": { 147 | "Microsoft.Build.Tasks.Git": "1.1.1", 148 | "Microsoft.SourceLink.Common": "1.1.1" 149 | } 150 | }, 151 | "Microsoft.TestPlatform.ObjectModel": { 152 | "type": "Transitive", 153 | "resolved": "17.12.0", 154 | "contentHash": "TDqkTKLfQuAaPcEb3pDDWnh7b3SyZF+/W9OZvWFp6eJCIiiYFdSB6taE2I6tWrFw5ywhzOb6sreoGJTI6m3rSQ==", 155 | "dependencies": { 156 | "System.Reflection.Metadata": "1.6.0" 157 | } 158 | }, 159 | "Microsoft.TestPlatform.TestHost": { 160 | "type": "Transitive", 161 | "resolved": "17.12.0", 162 | "contentHash": "MiPEJQNyADfwZ4pJNpQex+t9/jOClBGMiCiVVFuELCMSX2nmNfvUor3uFVxNNCg30uxDP8JDYfPnMXQzsfzYyg==", 163 | "dependencies": { 164 | "Microsoft.TestPlatform.ObjectModel": "17.12.0", 165 | "Newtonsoft.Json": "13.0.1" 166 | } 167 | }, 168 | "NETStandard.Library": { 169 | "type": "Transitive", 170 | "resolved": "2.0.0", 171 | "contentHash": "7jnbRU+L08FXKMxqUflxEXtVymWvNOrS8yHgu9s6EM8Anr6T/wIX4nZ08j/u3Asz+tCufp3YVwFSEvFTPYmBPA==", 172 | "dependencies": { 173 | "Microsoft.NETCore.Platforms": "1.1.0" 174 | } 175 | }, 176 | "Newtonsoft.Json": { 177 | "type": "Transitive", 178 | "resolved": "13.0.1", 179 | "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" 180 | }, 181 | "NUnit": { 182 | "type": "Transitive", 183 | "resolved": "3.13.1", 184 | "contentHash": "vWBvrSelmTYwqHWvO3dA63z7cOpaFR/3nJ9MMVLBkWeaWa7oiglPPm5g1h96B4i2XXqjFexxhR5MyMjmIJYPfg==", 185 | "dependencies": { 186 | "NETStandard.Library": "2.0.0" 187 | } 188 | }, 189 | "NUnit3TestAdapter": { 190 | "type": "Transitive", 191 | "resolved": "4.6.0", 192 | "contentHash": "R7e1+a4vuV/YS+ItfL7f//rG+JBvVeVLX4mHzFEZo4W1qEKl8Zz27AqvQSAqo+BtIzUCo4aAJMYa56VXS4hudw==" 193 | }, 194 | "Reqnroll": { 195 | "type": "Transitive", 196 | "resolved": "2.2.1", 197 | "contentHash": "v83D63zou3tsmcPp1MV0CLgt0DzThJwVm941ZSOHHNMnY7TbxBMWDimMifORjbhlukA6CQSNy+u0/Tx3d3Kaxg==", 198 | "dependencies": { 199 | "Cucumber.CucumberExpressions": "17.1.0", 200 | "Gherkin": "30.0.0", 201 | "Microsoft.Extensions.DependencyModel": "8.0.2", 202 | "SpecFlow.Internal.Json": "1.0.8", 203 | "System.Runtime.Loader": "4.3.0" 204 | } 205 | }, 206 | "Reqnroll.NUnit": { 207 | "type": "Transitive", 208 | "resolved": "2.2.1", 209 | "contentHash": "xNEj6ZNKyglOV0nk11082HJMVSSgTScpRGBEyC38o4CaRgg6SwHXf43i2wUfy423mFsHQ449+QRiw8UlHYTN2g==", 210 | "dependencies": { 211 | "NUnit": "3.13.1", 212 | "Reqnroll": "[2.2.1]", 213 | "Reqnroll.Tools.MsBuild.Generation": "[2.2.1]" 214 | } 215 | }, 216 | "Reqnroll.Tools.MsBuild.Generation": { 217 | "type": "Transitive", 218 | "resolved": "2.2.1", 219 | "contentHash": "A8a5dcZXfk3UlRJur9XzkJsBNHr/hwrHEzCYdJe1VUV1/NsN4cPK+S+7DV0haR4sJ2PfDhqds3FOK4rVRNDXmQ==", 220 | "dependencies": { 221 | "Reqnroll": "[2.2.1]" 222 | } 223 | }, 224 | "SpecFlow.Internal.Json": { 225 | "type": "Transitive", 226 | "resolved": "1.0.8", 227 | "contentHash": "lVCC/Rie7N5rFoc7YxPS0nneLfsWSTIMMlkndwxhaD8MxBp3Bsv1HeiVjVwXCjWaQeoqZcvIy52fF5Xit00ZLw==" 228 | }, 229 | "StyleCop.Analyzers.Unstable": { 230 | "type": "Transitive", 231 | "resolved": "1.2.0.556", 232 | "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" 233 | }, 234 | "System.Interactive": { 235 | "type": "Transitive", 236 | "resolved": "6.0.1", 237 | "contentHash": "q2E+Rkj35aGIPdZFKoBfI46LU1hGLHrhg9diif3psaRFeiMwSYom0VI0XHIQFYtGHzq2g1+3awHvK+7YntfNIQ==" 238 | }, 239 | "System.IO": { 240 | "type": "Transitive", 241 | "resolved": "4.3.0", 242 | "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", 243 | "dependencies": { 244 | "Microsoft.NETCore.Platforms": "1.1.0", 245 | "Microsoft.NETCore.Targets": "1.1.0", 246 | "System.Runtime": "4.3.0", 247 | "System.Text.Encoding": "4.3.0", 248 | "System.Threading.Tasks": "4.3.0" 249 | } 250 | }, 251 | "System.Reflection": { 252 | "type": "Transitive", 253 | "resolved": "4.3.0", 254 | "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", 255 | "dependencies": { 256 | "Microsoft.NETCore.Platforms": "1.1.0", 257 | "Microsoft.NETCore.Targets": "1.1.0", 258 | "System.IO": "4.3.0", 259 | "System.Reflection.Primitives": "4.3.0", 260 | "System.Runtime": "4.3.0" 261 | } 262 | }, 263 | "System.Reflection.Metadata": { 264 | "type": "Transitive", 265 | "resolved": "1.6.0", 266 | "contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ==" 267 | }, 268 | "System.Reflection.Primitives": { 269 | "type": "Transitive", 270 | "resolved": "4.3.0", 271 | "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", 272 | "dependencies": { 273 | "Microsoft.NETCore.Platforms": "1.1.0", 274 | "Microsoft.NETCore.Targets": "1.1.0", 275 | "System.Runtime": "4.3.0" 276 | } 277 | }, 278 | "System.Runtime": { 279 | "type": "Transitive", 280 | "resolved": "4.3.0", 281 | "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", 282 | "dependencies": { 283 | "Microsoft.NETCore.Platforms": "1.1.0", 284 | "Microsoft.NETCore.Targets": "1.1.0" 285 | } 286 | }, 287 | "System.Runtime.Loader": { 288 | "type": "Transitive", 289 | "resolved": "4.3.0", 290 | "contentHash": "DHMaRn8D8YCK2GG2pw+UzNxn/OHVfaWx7OTLBD/hPegHZZgcZh3H6seWegrC4BYwsfuGrywIuT+MQs+rPqRLTQ==", 291 | "dependencies": { 292 | "System.IO": "4.3.0", 293 | "System.Reflection": "4.3.0", 294 | "System.Runtime": "4.3.0" 295 | } 296 | }, 297 | "System.Text.Encoding": { 298 | "type": "Transitive", 299 | "resolved": "4.3.0", 300 | "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", 301 | "dependencies": { 302 | "Microsoft.NETCore.Platforms": "1.1.0", 303 | "Microsoft.NETCore.Targets": "1.1.0", 304 | "System.Runtime": "4.3.0" 305 | } 306 | }, 307 | "System.Threading.Tasks": { 308 | "type": "Transitive", 309 | "resolved": "4.3.0", 310 | "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", 311 | "dependencies": { 312 | "Microsoft.NETCore.Platforms": "1.1.0", 313 | "Microsoft.NETCore.Targets": "1.1.0", 314 | "System.Runtime": "4.3.0" 315 | } 316 | }, 317 | "corvus.extensions": { 318 | "type": "Project", 319 | "dependencies": { 320 | "System.Interactive": "[6.0.1, )" 321 | } 322 | } 323 | } 324 | } 325 | } -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions.Specs/reqnroll.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.reqnroll.net/reqnroll-config-latest.json", 3 | "stepAssemblies": [ 4 | { "assembly": "Corvus.Testing.Reqnroll" } 5 | ] 6 | } -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.12.35707.178 d17.12 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Extensions", "Corvus.Extensions\Corvus.Extensions.csproj", "{9D67BDA5-267B-4D00-BC88-0481D9E942AA}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Corvus.Extensions.Specs", "Corvus.Extensions.Specs\Corvus.Extensions.Specs.csproj", "{87FE0449-50AA-4F23-BAB4-47D2222CFDDC}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {9D67BDA5-267B-4D00-BC88-0481D9E942AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {9D67BDA5-267B-4D00-BC88-0481D9E942AA}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {9D67BDA5-267B-4D00-BC88-0481D9E942AA}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {9D67BDA5-267B-4D00-BC88-0481D9E942AA}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {87FE0449-50AA-4F23-BAB4-47D2222CFDDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {87FE0449-50AA-4F23-BAB4-47D2222CFDDC}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {87FE0449-50AA-4F23-BAB4-47D2222CFDDC}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {87FE0449-50AA-4F23-BAB4-47D2222CFDDC}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {C2FE6918-F86F-4AAC-BC28-D5141DEB816B} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions/Corvus.Extensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | netstandard2.0 7 | enable 8 | 9 | true 10 | All 11 | 12 | 13 | 14 | Apache-2.0 15 | 16 | 17 | 18 | en-GB 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | 30 | 31 | True 32 | True 33 | ExceptionMessages.resx 34 | 35 | 36 | True 37 | True 38 | ExceptionMessages.resx 39 | 40 | 41 | True 42 | True 43 | Strings.resx 44 | 45 | 46 | 47 | 48 | 49 | ResXFileCodeGenerator 50 | ExceptionMessages.Designer.cs 51 | Corvus.Extensions 52 | 53 | 54 | ResXFileCodeGenerator 55 | ExceptionMessages.Designer.cs 56 | 57 | 58 | ResXFileCodeGenerator 59 | Strings.Designer.cs 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions/Corvus/Extensions/CastTo.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Endjin Limited. All rights reserved. 3 | // 4 | 5 | namespace Corvus.Extensions 6 | { 7 | using System; 8 | using System.Linq.Expressions; 9 | 10 | /// 11 | /// Class to cast to a sepcified type. 12 | /// 13 | /// Target type. 14 | /// 15 | /// The original code was derived from a StackOverlow answer here https://stackoverflow.com/a/23391746. 16 | /// 17 | public static class CastTo 18 | { 19 | /// 20 | /// Casts from the source type to the target type. 21 | /// 22 | /// An instance of the source type to be case to the target type. 23 | /// Source type to cast from. Usually a generic type. 24 | /// An instance of the target type, cast from the source type. 25 | /// 26 | /// This does not cause boxing for value types. It is especially useful in generic methods. 27 | /// 28 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "The intended usage is meant to be self describing, e.g. CastTo.From(x) means 'cast to int from x', so the usual reasoning behind CA1000 does not apply here")] 29 | public static T From(TSource s) 30 | { 31 | return Cache.Caster(s); 32 | } 33 | 34 | private static class Cache 35 | { 36 | public static readonly Func Caster = Get(); 37 | 38 | private static Func Get() 39 | { 40 | ParameterExpression p = Expression.Parameter(typeof(TSource)); 41 | UnaryExpression c = Expression.ConvertChecked(p, typeof(T)); 42 | return Expression.Lambda>(c, p).Compile(); 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions/Corvus/Extensions/CollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Endjin Limited. All rights reserved. 3 | // 4 | 5 | namespace Corvus.Extensions 6 | { 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | 10 | /// 11 | /// Extensions for . 12 | /// 13 | public static class CollectionExtensions 14 | { 15 | /// 16 | /// Add a range of items to the collection. 17 | /// 18 | /// The type of items in the collection. 19 | /// The collection to which items will be added. 20 | /// The items to add. 21 | public static void AddRange(this ICollection destination, IEnumerable items) 22 | { 23 | if (destination is null) 24 | { 25 | throw new System.ArgumentNullException(nameof(destination)); 26 | } 27 | 28 | if (items is null) 29 | { 30 | throw new System.ArgumentNullException(nameof(items)); 31 | } 32 | 33 | if (destination == items) 34 | { 35 | throw new System.ArgumentException("Cannot add a collection to itself.", nameof(items)); 36 | } 37 | 38 | items.ForEach(t => destination.Add(t)); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions/Corvus/Extensions/DictionaryExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Endjin Limited. All rights reserved. 3 | // 4 | 5 | namespace Corvus.Extensions 6 | { 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | 10 | /// 11 | /// Extensions for . 12 | /// 13 | public static class DictionaryExtensions 14 | { 15 | /// 16 | /// Adds an item if there is no item with that key in the dictionary. 17 | /// 18 | /// The type of the key. 19 | /// The type of the value. 20 | /// The dictionary to which to add the item. 21 | /// The key. 22 | /// The value. 23 | /// True if the item as added. 24 | public static bool AddIfNotExists(this IDictionary dictionary, TKey key, TValue value) 25 | { 26 | if (dictionary is null) 27 | { 28 | throw new System.ArgumentNullException(nameof(dictionary)); 29 | } 30 | 31 | if (dictionary.ContainsKey(key)) 32 | { 33 | return false; 34 | } 35 | 36 | dictionary.Add(key, value); 37 | return true; 38 | } 39 | 40 | /// 41 | /// Merges two dictionaries. 42 | /// 43 | /// The key type. 44 | /// The value type. 45 | /// The source dictionary, into which the other is merged. 46 | /// The dictionary from which values are to be merged. 47 | /// The original dictionary, with the merged items added. 48 | /// If a key is already present in the dictionary, then the original value is preserved. 49 | public static IDictionary Merge(this IDictionary that, IDictionary dictionary) 50 | { 51 | if (that is null) 52 | { 53 | throw new System.ArgumentNullException(nameof(that)); 54 | } 55 | 56 | if (dictionary is null) 57 | { 58 | throw new System.ArgumentNullException(nameof(dictionary)); 59 | } 60 | 61 | dictionary.Keys.ForEach( 62 | key => that.AddIfNotExists(key, dictionary[key])); 63 | 64 | return that; 65 | } 66 | 67 | /// 68 | /// Replaces an item if there is an item with that key in the dictionary, otherwise adds it. 69 | /// 70 | /// The type of the key. 71 | /// The type of the value. 72 | /// The dictionary to which to add the item. 73 | /// The key. 74 | /// The value. 75 | /// True if the item was replaced, false if it was added. 76 | public static bool ReplaceIfExists(this IDictionary dictionary, TKey key, TValue value) 77 | { 78 | if (dictionary is null) 79 | { 80 | throw new System.ArgumentNullException(nameof(dictionary)); 81 | } 82 | 83 | if (dictionary.ContainsKey(key)) 84 | { 85 | dictionary[key] = value; 86 | return true; 87 | } 88 | else 89 | { 90 | dictionary.Add(key, value); 91 | return false; 92 | } 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions/Corvus/Extensions/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Endjin Limited. All rights reserved. 3 | // 4 | 5 | namespace Corvus.Extensions 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | 11 | /// 12 | /// Extensions for . 13 | /// 14 | public static class EnumerableExtensions 15 | { 16 | /// 17 | /// Gets the distinct values from the collection, preserving ordering. 18 | /// 19 | /// The type of the items. 20 | /// The enumerable of items. 21 | /// An enumerable of the distinct items in the enumerable. 22 | public static IEnumerable DistinctPreserveOrder(this IEnumerable list) 23 | { 24 | if (list is null) 25 | { 26 | throw new ArgumentNullException(nameof(list)); 27 | } 28 | 29 | return Enumerate(); 30 | 31 | IEnumerable Enumerate() 32 | { 33 | var hashSet = new HashSet(); 34 | foreach (T item in list) 35 | { 36 | if (hashSet.Contains(item)) 37 | { 38 | continue; 39 | } 40 | 41 | hashSet.Add(item); 42 | yield return item; 43 | } 44 | } 45 | } 46 | 47 | /// 48 | /// Concatenates a set of enumerables. 49 | /// 50 | /// The type of items in the enumerable. 51 | /// The first enumerable. 52 | /// Second and subsequent items. 53 | /// An enumerable which concatenates the prvovided lists. 54 | public static IEnumerable Concatenate(this IEnumerable first, params IEnumerable[] lists) 55 | { 56 | if (first is null) 57 | { 58 | throw new ArgumentNullException(nameof(first)); 59 | } 60 | 61 | if (lists is null) 62 | { 63 | throw new ArgumentNullException(nameof(lists)); 64 | } 65 | 66 | return Enumerate(); 67 | 68 | IEnumerable Enumerate() 69 | { 70 | foreach (T item in first) 71 | { 72 | yield return item; 73 | } 74 | 75 | foreach (IEnumerable list in lists) 76 | { 77 | foreach (T item in list) 78 | { 79 | yield return item; 80 | } 81 | } 82 | } 83 | } 84 | 85 | /// 86 | /// Determine if an enumerable has at least a specified number of items. 87 | /// 88 | /// The type of item in the enumerable. 89 | /// The enumerable to test. 90 | /// The minumum number of items. 91 | /// True if the enumerable contains at least this number of items. 92 | public static bool HasMinimumCount(this IEnumerable enumerable, int count) 93 | { 94 | if (enumerable is null) 95 | { 96 | throw new ArgumentNullException(nameof(enumerable)); 97 | } 98 | 99 | if (count < 1) 100 | { 101 | throw new ArgumentOutOfRangeException(nameof(count), ExceptionMessages.EnumerableExtensions_HasMinimumCount_CountMustBeGreaterThanZero); 102 | } 103 | 104 | if (enumerable is ICollection collection) 105 | { 106 | return collection.Count >= count; 107 | } 108 | 109 | return enumerable.Skip(count - 1).Any(); 110 | } 111 | 112 | /// 113 | /// Determine if an enumerable is non-empty and all elements meet a particular criterion. 114 | /// 115 | /// The type of item in the enumerable. 116 | /// The enumerable to test. 117 | /// The test to apply. 118 | /// 119 | /// True if the enumerable has at least one item, and the predicate returned true for all 120 | /// of them. False if the enumerable was empty, or if it contains at least one item for 121 | /// which the predicate returns false. 122 | /// 123 | /// 124 | /// 125 | /// The standard LINQ All operator has the sometimes-undesirable characteristic of 126 | /// reporting what logicians call a 'vacuous truth', i.e., that it returns true if you pass 127 | /// it empty input. This is in keeping with the standard definition of the universal 128 | /// quantifier in formal logic, but it's not always what you want. 129 | /// 130 | /// 131 | public static bool AllAndAtLeastOne(this IEnumerable enumerable, Func predicate) 132 | { 133 | if (enumerable is null) 134 | { 135 | throw new ArgumentNullException(nameof(enumerable)); 136 | } 137 | 138 | if (predicate is null) 139 | { 140 | throw new ArgumentNullException(nameof(predicate)); 141 | } 142 | 143 | // Logically, this is: 144 | // enumerable.Any() && enumerable.All(predicate) 145 | // However that's inefficient because it will start to enumerate twice. (The Any and 146 | // All operators will each create their own enumerators. Admittedly, Any does not 147 | // proceed to the end, but some enumerables do work at the start of each enumeration, 148 | // and we'd like to avoid that, since it's unnecessary.) 149 | // This enumerates through the items just once, and will (like All) bail out early if 150 | // it detects an item that fails the test. 151 | bool atLeastOne = false; 152 | foreach (T item in enumerable) 153 | { 154 | if (!predicate(item)) 155 | { 156 | return false; 157 | } 158 | 159 | atLeastOne = true; 160 | } 161 | 162 | return atLeastOne; 163 | } 164 | 165 | /// 166 | /// Distinct operator enabling lambda-based criteria. 167 | /// 168 | /// The element type. 169 | /// The type on which uniqueness is based. 170 | /// The source elements. 171 | /// 172 | /// A callback that returns the value to use to determine whether elements are distinct. 173 | /// E.g. this might pull out an 'id' property. 174 | /// 175 | /// The distinct elements. 176 | public static IEnumerable DistinctBy(this IEnumerable source, Func identitySelector) 177 | where TIdentity : notnull 178 | { 179 | if (source is null) 180 | { 181 | throw new ArgumentNullException(nameof(source)); 182 | } 183 | 184 | if (identitySelector is null) 185 | { 186 | throw new ArgumentNullException(nameof(identitySelector)); 187 | } 188 | 189 | return source.Distinct(new DelegateEqualityComparer(identitySelector)); 190 | } 191 | 192 | private class DelegateEqualityComparer : IEqualityComparer 193 | where TIdentity : notnull 194 | { 195 | private readonly Func identitySelector; 196 | 197 | public DelegateEqualityComparer(Func identitySelector) 198 | { 199 | this.identitySelector = identitySelector; 200 | } 201 | 202 | public bool Equals(T x, T y) => Equals(this.identitySelector(x), this.identitySelector(y)); 203 | 204 | public int GetHashCode(T obj) => this.identitySelector(obj).GetHashCode(); 205 | } 206 | } 207 | } -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions/Corvus/Extensions/ExceptionMessages.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Corvus.Extensions { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class ExceptionMessages { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal ExceptionMessages() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Corvus.Extensions.ExceptionMessages", typeof(ExceptionMessages).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to The count specified must be greater than 0.. 65 | /// 66 | internal static string EnumerableExtensions_HasMinimumCount_CountMustBeGreaterThanZero { 67 | get { 68 | return ResourceManager.GetString("EnumerableExtensions_HasMinimumCount_CountMustBeGreaterThanZero", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to The expression is not a member access expression.. 74 | /// 75 | internal static string LambdaExpressionExtensions_TheExpressionIsNotAMemberAccessExpression { 76 | get { 77 | return ResourceManager.GetString("LambdaExpressionExtensions_TheExpressionIsNotAMemberAccessExpression", resourceCulture); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions/Corvus/Extensions/ExceptionMessages.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | The count specified must be greater than 0. 122 | 123 | 124 | The expression is not a member access expression. 125 | 126 | -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions/Corvus/Extensions/LambdaExpressionExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Endjin Limited. All rights reserved. 3 | // 4 | 5 | namespace Corvus.Extensions 6 | { 7 | using System; 8 | using System.Linq.Expressions; 9 | 10 | /// 11 | /// Extensions for . 12 | /// 13 | public static class LambdaExpressionExtensions 14 | { 15 | /// 16 | /// Extract a property name from a lambda expression. 17 | /// 18 | /// The property expression. 19 | /// The name of the property expression. 20 | public static string ExtractPropertyName(this LambdaExpression expression) 21 | { 22 | if (expression is null) 23 | { 24 | throw new ArgumentNullException(nameof(expression)); 25 | } 26 | 27 | MemberExpression memberExpression = GetMemberExpression(expression); 28 | 29 | return memberExpression.Member.Name; 30 | } 31 | 32 | /// 33 | /// Gets a member expression from the body of a lambda expression. 34 | /// 35 | /// The lambda expression. 36 | /// The member expression. 37 | public static MemberExpression GetMemberExpression(this LambdaExpression expression) 38 | { 39 | if (expression == null) 40 | { 41 | throw new ArgumentNullException(nameof(expression)); 42 | } 43 | 44 | if (expression.Body is not MemberExpression memberExpression) 45 | { 46 | throw new ArgumentException(ExceptionMessages.LambdaExpressionExtensions_TheExpressionIsNotAMemberAccessExpression, nameof(expression)); 47 | } 48 | 49 | return memberExpression; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions/Corvus/Extensions/ListExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Endjin Limited. All rights reserved. 3 | // 4 | 5 | namespace Corvus.Extensions 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | 10 | /// 11 | /// Extensions for . 12 | /// 13 | public static class ListExtensions 14 | { 15 | /// 16 | /// Remove all the items in the list which match a particular predicate. 17 | /// 18 | /// The type of items in the list. 19 | /// The list from which to remove items. 20 | /// The predicate. 21 | public static void RemoveAll(this IList destination, Predicate match) 22 | { 23 | if (destination is null) 24 | { 25 | throw new ArgumentNullException(nameof(destination)); 26 | } 27 | 28 | if (match is null) 29 | { 30 | throw new ArgumentNullException(nameof(match)); 31 | } 32 | 33 | if (destination is List list) 34 | { 35 | list.RemoveAll(match); 36 | } 37 | else 38 | { 39 | new List(destination).ForEachAtIndex((i, j) => 40 | { 41 | if (match(i)) 42 | { 43 | destination.RemoveAt(j); 44 | } 45 | }); 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions/Corvus/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Endjin Limited. All rights reserved. 3 | // 4 | 5 | namespace Corvus.Extensions 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Globalization; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Text; 13 | 14 | /// 15 | /// Extension methods for strings. 16 | /// 17 | public static class StringExtensions 18 | { 19 | /// 20 | /// Convert the provided string to a base 64 representation of its byte representation in a particular encoding. 21 | /// 22 | /// The value to convert. 23 | /// The encoding to use. 24 | /// The Base 64 encoded string. 25 | public static string AsBase64(this string value, Encoding encoding) 26 | { 27 | if (value is null) 28 | { 29 | throw new ArgumentNullException(nameof(value)); 30 | } 31 | 32 | if (encoding is null) 33 | { 34 | throw new ArgumentNullException(nameof(encoding)); 35 | } 36 | 37 | return Convert.ToBase64String(encoding.GetBytes(value)); 38 | } 39 | 40 | /// 41 | /// Convert the provided string to a base 64 representation of its byte representation in the UTF8 encoding. 42 | /// 43 | /// The value to convert. 44 | /// The Base 64 encoded string. 45 | public static string AsBase64(this string value) 46 | { 47 | if (value is null) 48 | { 49 | throw new ArgumentNullException(nameof(value)); 50 | } 51 | 52 | return AsBase64(value, Encoding.UTF8); 53 | } 54 | 55 | /// 56 | /// Convert the provided string to a base 64 representation of its byte representation in the UTF8 encoding, 57 | /// with a URL-safe representation. 58 | /// 59 | /// The value to convert. 60 | /// The Base 64 encoded string. 61 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1055:Uri return values should not be strings", Justification = "FxCop's heuristics have misfired - this does not return a URI")] 62 | public static string Base64UrlEncode(this string input) 63 | { 64 | if (input is null) 65 | { 66 | throw new ArgumentNullException(nameof(input)); 67 | } 68 | 69 | byte[] arg = Encoding.UTF8.GetBytes(input); 70 | string s = Convert.ToBase64String(arg); // Regular base64 encoder 71 | s = s.Split('=')[0]; // Remove any trailing '='s 72 | s = s.Replace('+', '-'); // 62nd char of encoding 73 | return s.Replace('/', '_'); // 63rd char of encoding 74 | } 75 | 76 | /// 77 | /// Convert the provided string from a base 64 representation of its byte representation in the UTF8 encoding 78 | /// with a URL-safe representation. 79 | /// 80 | /// The value to convert. 81 | /// The original string. 82 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1055:Uri return values should not be strings", Justification = "FxCop's heuristics have misfired - this does not return a URI")] 83 | public static string Base64UrlDecode(this string arg) 84 | { 85 | if (arg is null) 86 | { 87 | throw new ArgumentNullException(nameof(arg)); 88 | } 89 | 90 | string s = arg; 91 | s = s.Replace('-', '+'); // 62nd char of encoding 92 | s = s.Replace('_', '/'); // 63rd char of encoding 93 | 94 | // Pad with trailing '='s 95 | switch (s.Length % 4) 96 | { 97 | case 0: break; // No pad chars in this case 98 | case 2: s += "=="; break; // Two pad chars 99 | case 3: s += "="; break; // One pad char 100 | default: 101 | throw new InvalidOperationException($"Illegal base64url string '{arg}'"); 102 | } 103 | 104 | byte[] string64 = Convert.FromBase64String(s); // Standard base64 decoder 105 | return Encoding.UTF8.GetString(string64); 106 | } 107 | 108 | /// 109 | /// Provide a stream over the string in the UTF8 encoding. 110 | /// 111 | /// The string. 112 | /// A stream over the UTF8-encoded representation of the string. 113 | public static Stream AsStream(this string value) 114 | { 115 | if (value is null) 116 | { 117 | throw new ArgumentNullException(nameof(value)); 118 | } 119 | 120 | return AsStream(value, Encoding.UTF8); 121 | } 122 | 123 | /// 124 | /// Provide a stream over the string in the specified encoding. 125 | /// 126 | /// The string. 127 | /// The encoding. 128 | /// A stream over the encoded representation of the string. 129 | public static Stream AsStream(this string value, Encoding encoding) 130 | { 131 | if (value is null) 132 | { 133 | throw new ArgumentNullException(nameof(value)); 134 | } 135 | 136 | if (encoding is null) 137 | { 138 | throw new ArgumentNullException(nameof(encoding)); 139 | } 140 | 141 | byte[] bytes = encoding.GetBytes(value); 142 | return new MemoryStream(bytes); 143 | } 144 | 145 | /// 146 | /// Escapes a content type string. 147 | /// 148 | /// The content type to escape. 149 | /// An escaped version of the content type, suitable for use in a Uri. 150 | /// 151 | public static string EscapeContentType(this string contentType) 152 | { 153 | if (contentType is null) 154 | { 155 | throw new ArgumentNullException(nameof(contentType)); 156 | } 157 | 158 | return Uri.EscapeDataString(contentType.Replace('/', '`').Replace('.', '|')); 159 | } 160 | 161 | /// 162 | /// Decode a string from a base64-encoded byte array with the specified text encoding. 163 | /// 164 | /// The base-64 encoded byte array. 165 | /// The encoding of the byte array. 166 | /// The original string represented by the base-64 encoded byte array. 167 | public static string FromBase64(this string value, Encoding encoding) 168 | { 169 | if (value is null) 170 | { 171 | throw new ArgumentNullException(nameof(value)); 172 | } 173 | 174 | if (encoding is null) 175 | { 176 | throw new ArgumentNullException(nameof(encoding)); 177 | } 178 | 179 | byte[] bytes = Convert.FromBase64String(value); 180 | 181 | return encoding.GetString(bytes, 0, bytes.Length); 182 | } 183 | 184 | /// 185 | /// Decode a string from a base64-encoded JTF8 byte array. 186 | /// 187 | /// The base-64 encoded byte array. 188 | /// The original string represented by the base-64 encoded byte array. 189 | public static string FromBase64(this string value) 190 | { 191 | if (value is null) 192 | { 193 | throw new ArgumentNullException(nameof(value)); 194 | } 195 | 196 | return FromBase64(value, Encoding.UTF8); 197 | } 198 | 199 | /// 200 | /// Enumerate the grapheme clusters in a string. 201 | /// 202 | /// The string for which to enumerate grapheme clusters. 203 | /// An enumerable set of strings representing each grapheme cluster in the string. 204 | public static IEnumerable GetGraphemeClusters(this string s) 205 | { 206 | if (s is null) 207 | { 208 | throw new ArgumentNullException(nameof(s)); 209 | } 210 | 211 | return Enumerate(); 212 | 213 | IEnumerable Enumerate() 214 | { 215 | TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(s); 216 | while (enumerator.MoveNext()) 217 | { 218 | yield return (string)enumerator.Current; 219 | } 220 | } 221 | } 222 | 223 | /// 224 | /// Reverse the string. 225 | /// 226 | /// The string to reverse. 227 | /// The reversed string, respecting grapheme clusters. 228 | public static string Reverse(this string s) 229 | { 230 | if (s is null) 231 | { 232 | throw new ArgumentNullException(nameof(s)); 233 | } 234 | 235 | return string.Concat(s.GetGraphemeClusters().Reverse().ToArray()); 236 | } 237 | 238 | /// 239 | /// Unescapes a content type string. 240 | /// 241 | /// The content type to escape. 242 | /// The unescaped content type. 243 | /// 244 | public static string UnescapeContentType(this string contentType) 245 | { 246 | if (contentType is null) 247 | { 248 | throw new ArgumentNullException(nameof(contentType)); 249 | } 250 | 251 | return Uri.UnescapeDataString(contentType).Replace('`', '/').Replace('|', '.'); 252 | } 253 | 254 | /// 255 | /// Converts a string to camel case from pascal case. 256 | /// 257 | /// The string to convert. 258 | /// The string with the initial character lowercased. 259 | public static string ToCamelCase(this string s) 260 | { 261 | if (string.IsNullOrEmpty(s)) 262 | { 263 | return s; 264 | } 265 | 266 | if (!char.IsUpper(s[0])) 267 | { 268 | return s; 269 | } 270 | 271 | string camelCase = char.ToLowerInvariant(s[0]).ToString(CultureInfo.InvariantCulture); 272 | 273 | if (s.Length > 1) 274 | { 275 | camelCase += s.Substring(1); 276 | } 277 | 278 | return camelCase; 279 | } 280 | } 281 | } -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions/Corvus/Extensions/TaskEx.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Endjin Limited. All rights reserved. 3 | // 4 | 5 | namespace Corvus.Extensions.Tasks 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | 12 | /// 13 | /// Extensions to Task.* methods. 14 | /// 15 | #pragma warning disable CA1711 // Identifiers should not have incorrect suffix - this is the established public name so we can't change it 16 | public static class TaskEx 17 | #pragma warning restore CA1711 18 | { 19 | /// 20 | /// Flattening of the outputs from Task.WhenAll. 21 | /// 22 | /// The type of the items in the source list. 23 | /// The type of the items in the resulting list. 24 | /// The source. 25 | /// The function used to map from each item in the source list to the result. 26 | /// A fanned out list of T2. 27 | public static async Task> WhenAllMany(IEnumerable source, Func>> mapper) 28 | { 29 | if (source is null) 30 | { 31 | throw new ArgumentNullException(nameof(source)); 32 | } 33 | 34 | if (mapper is null) 35 | { 36 | throw new ArgumentNullException(nameof(mapper)); 37 | } 38 | 39 | return (await Task.WhenAll( 40 | source.Select(sourceItem => mapper(sourceItem))).ConfigureAwait(false)) 41 | .SelectMany(result => result).ToList(); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /Solutions/Corvus.Extensions/Corvus/Extensions/TaskExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Endjin Limited. All rights reserved. 3 | // 4 | 5 | namespace Corvus.Extensions 6 | { 7 | using System; 8 | using System.Reflection; 9 | using System.Threading.Tasks; 10 | 11 | /// 12 | /// Extension methods for s. 13 | /// 14 | public static class TaskExtensions 15 | { 16 | /// 17 | /// Checks to see if a Task actually has a result and converts to a task of the relevant type. 18 | /// 19 | /// The type of the result. 20 | /// The task to await. 21 | /// A for the relevant type. 22 | /// Thrown if you cannot cast between the result type of the task, and the desired type. 23 | /// 24 | /// Note that this async method will wait for the task to complete before returning a 25 | /// task of the relevant type. 26 | /// 27 | public static async Task CastWithConversion(this Task task) 28 | { 29 | if (task is null) 30 | { 31 | throw new ArgumentNullException(nameof(task)); 32 | } 33 | 34 | await task.ConfigureAwait(false); 35 | 36 | PropertyInfo resultPropertyInfo = task.GetType().GetProperty("Result"); 37 | 38 | if (resultPropertyInfo == null) 39 | { 40 | throw new InvalidCastException(); 41 | } 42 | 43 | return CastTo.From(resultPropertyInfo.GetValue(task)); 44 | } 45 | 46 | /// 47 | /// Casts from a to a . 48 | /// 49 | /// The type of the result. 50 | /// The task two await. 51 | /// A for the relevant type. 52 | /// The task was not a task of the given type. 53 | public static Task Cast(this Task task) 54 | { 55 | if (task is null) 56 | { 57 | throw new ArgumentNullException(nameof(task)); 58 | } 59 | 60 | if (task is Task result) 61 | { 62 | return result; 63 | } 64 | 65 | throw new InvalidCastException(); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /Solutions/PackageIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corvus-dotnet/Corvus.Extensions/382cedc468a72b00723e77942849160602eb9fad/Solutions/PackageIcon.png -------------------------------------------------------------------------------- /Solutions/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | // Note: this file was added to your solution as a result of one or more projects using the Endjin.RecommendedPractices.Build.Common NuGet package. 3 | // You can edit this file (e.g., to remove these comments), and it will not be updated - the package just checks for its presence, and copies 4 | // this file. If you don't want this file (but you want to use the NuGet package that puts it here), add this setting to all projects 5 | // using Endjin.RecommendedPractices.Build.Common: 6 | // true 7 | // and then delete this file. That setting will prevent the package from recreating this file. 8 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 9 | "settings": { 10 | "documentationRules": { 11 | "companyName": "Endjin Limited" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Runs a .NET flavoured build process. 4 | .DESCRIPTION 5 | This script was scaffolded using a template from the Endjin.RecommendedPractices.Build PowerShell module. 6 | It uses the InvokeBuild module to orchestrate an opinonated software build process for .NET solutions. 7 | .EXAMPLE 8 | PS C:\> ./build.ps1 9 | Downloads any missing module dependencies (Endjin.RecommendedPractices.Build & InvokeBuild) and executes 10 | the build process. 11 | .PARAMETER Tasks 12 | Optionally override the default task executed as the entry-point of the build. 13 | .PARAMETER Configuration 14 | The build configuration, defaults to 'Release'. 15 | .PARAMETER BuildRepositoryUri 16 | Optional URI that supports pulling MSBuild logic from a web endpoint (e.g. a GitHub blob). 17 | .PARAMETER SourcesDir 18 | The path where the source code to be built is located, defaults to the current working directory. 19 | .PARAMETER CoverageDir 20 | The output path for the test coverage data, if run. 21 | .PARAMETER TestReportTypes 22 | The test report format that should be generated by the test report generator, if run. 23 | .PARAMETER PackagesDir 24 | The output path for any packages produced as part of the build. 25 | .PARAMETER LogLevel 26 | The logging verbosity. 27 | .PARAMETER Clean 28 | When true, the .NET solution will be cleaned and all output/intermediate folders deleted. 29 | .PARAMETER BuildModulePath 30 | The path to import the Endjin.RecommendedPractices.Build module from. This is useful when 31 | testing pre-release versions of the Endjin.RecommendedPractices.Build that are not yet 32 | available in the PowerShell Gallery. 33 | .PARAMETER BuildModuleVersion 34 | The version of the Endjin.RecommendedPractices.Build module to import. This is useful when 35 | testing pre-release versions of the Endjin.RecommendedPractices.Build that are not yet 36 | available in the PowerShell Gallery. 37 | .PARAMETER InvokeBuildModuleVersion 38 | The version of the InvokeBuild module to be used. 39 | #> 40 | [CmdletBinding()] 41 | param ( 42 | [Parameter(Position=0)] 43 | [string[]] $Tasks = @("."), 44 | 45 | [Parameter()] 46 | [string] $Configuration = "Debug", 47 | 48 | [Parameter()] 49 | [string] $BuildRepositoryUri = "", 50 | 51 | [Parameter()] 52 | [string] $SourcesDir = $PWD, 53 | 54 | [Parameter()] 55 | [string] $CoverageDir = "_codeCoverage", 56 | 57 | [Parameter()] 58 | [string] $TestReportTypes = "Cobertura", 59 | 60 | [Parameter()] 61 | [string] $PackagesDir = "_packages", 62 | 63 | [Parameter()] 64 | [ValidateSet("minimal","normal","detailed")] 65 | [string] $LogLevel = "minimal", 66 | 67 | [Parameter()] 68 | [switch] $Clean, 69 | 70 | [Parameter()] 71 | [string] $BuildModulePath, 72 | 73 | [Parameter()] 74 | [version] $BuildModuleVersion = "1.5.12", 75 | 76 | [Parameter()] 77 | [version] $InvokeBuildModuleVersion = "5.11.3" 78 | ) 79 | 80 | $ErrorActionPreference = $ErrorActionPreference ? $ErrorActionPreference : 'Stop' 81 | $InformationPreference = 'Continue' 82 | 83 | $here = Split-Path -Parent $PSCommandPath 84 | 85 | #region InvokeBuild setup 86 | if (!(Get-Module -ListAvailable InvokeBuild)) { 87 | Install-Module InvokeBuild -RequiredVersion $InvokeBuildModuleVersion -Scope CurrentUser -Force -Repository PSGallery 88 | } 89 | Import-Module InvokeBuild 90 | # This handles calling the build engine when this file is run like a normal PowerShell script 91 | # (i.e. avoids the need to have another script to setup the InvokeBuild environment and issue the 'Invoke-Build' command ) 92 | if ($MyInvocation.ScriptName -notlike '*Invoke-Build.ps1') { 93 | try { 94 | Invoke-Build $Tasks $MyInvocation.MyCommand.Path @PSBoundParameters 95 | } 96 | catch { 97 | $_.ScriptStackTrace 98 | throw 99 | } 100 | return 101 | } 102 | #endregion 103 | 104 | #region Import shared tasks and initialise build framework 105 | if (!($BuildModulePath)) { 106 | if (!(Get-Module -ListAvailable Endjin.RecommendedPractices.Build | ? { $_.Version -eq $BuildModuleVersion })) { 107 | Write-Information "Installing 'Endjin.RecommendedPractices.Build' module..." 108 | Install-Module Endjin.RecommendedPractices.Build -RequiredVersion $BuildModuleVersion -Scope CurrentUser -Force -Repository PSGallery 109 | } 110 | $BuildModulePath = "Endjin.RecommendedPractices.Build" 111 | } 112 | else { 113 | Write-Information "BuildModulePath: $BuildModulePath" 114 | } 115 | Import-Module $BuildModulePath -RequiredVersion $BuildModuleVersion -Force 116 | 117 | # Load the build process & tasks 118 | . Endjin.RecommendedPractices.Build.tasks 119 | #endregion 120 | 121 | 122 | # 123 | # Build process control options 124 | # 125 | $SkipInit = $false 126 | $SkipVersion = $false 127 | $SkipBuild = $false 128 | $CleanBuild = $Clean 129 | $SkipTest = $false 130 | $SkipTestReport = $false 131 | $SkipAnalysis = $false 132 | $SkipPackage = $false 133 | $SkipPublish = $false 134 | 135 | 136 | # 137 | # Build process configuration 138 | # 139 | $SolutionToBuild = (Resolve-Path (Join-Path $here "Solutions\Corvus.Extensions.sln")).Path 140 | $ProjectsToPublish = @( 141 | # "Solutions/MySolution/MyWebSite/MyWebSite.csproj" 142 | ) 143 | $NuSpecFilesToPackage = @( 144 | # "Solutions/MySolution/MyProject/MyProject.nuspec" 145 | ) 146 | 147 | # 148 | # Specify files to exclude from code coverage 149 | # This option is for excluding generated code 150 | # - Use file path or directory path with globbing (e.g dir1/*.cs) 151 | # - Use single or multiple paths (separated by comma) (e.g. **/dir1/class1.cs,**/dir2/*.cs,**/dir3/**/*.cs) 152 | # 153 | $ExcludeFilesFromCodeCoverage = "" 154 | 155 | # Synopsis: Build, Test and Package 156 | task . FullBuild 157 | 158 | 159 | # build extensibility tasks 160 | task RunFirst {} 161 | task PreInit {} 162 | task PostInit {} 163 | task PreVersion {} 164 | task PostVersion {} 165 | task PreBuild {} 166 | task PostBuild {} 167 | task PreTest {} 168 | task PostTest {} 169 | task PreTestReport {} 170 | task PostTestReport {} 171 | task PreAnalysis {} 172 | task PostAnalysis {} 173 | task PrePackage {} 174 | task PostPackage {} 175 | task PrePublish {} 176 | task PostPublish {} 177 | task RunLast {} 178 | 179 | -------------------------------------------------------------------------------- /imm.yaml: -------------------------------------------------------------------------------- 1 | - Name: Shared Engineering Standards 2 | Id: 74e29f9b-6dca-4161-8fdd-b468a1eb185d 3 | Measures: 4 | - Score: 1 5 | Description: Configured 6 | - Name: Coding Standards 7 | Id: f6f6490f-9493-4dc3-a674-15584fa951d8 8 | Measures: 9 | - Score: 1 10 | Description: Enforced via tooling 11 | - Name: Executable Specifications 12 | Id: bb49fb94-6ab5-40c3-a6da-dfd2e9bc4b00 13 | Measures: 14 | - Score: 0 15 | Description: None 16 | - Name: Code Coverage 17 | Id: 0449cadc-0078-4094-b019-520d75cc6cbb 18 | Measures: 19 | - Score: 1 20 | Description: 26-50 21 | - Name: Benchmarks 22 | Id: 64ed80dc-d354-45a9-9a56-c32437306afa 23 | Measures: 24 | - Score: 0 25 | Description: None 26 | - Name: Reference Documentation 27 | Id: 2a7fc206-d578-41b0-85f6-a28b6b0fec5f 28 | Measures: 29 | - Score: 1 30 | Description: Good quality 31 | - Name: Design & Implementation Documentation 32 | Id: f026d5a2-ce1a-4e04-af15-5a35792b164b 33 | Measures: 34 | - Score: 0 35 | Description: None 36 | - Name: How-to Documentation 37 | Id: 145f2e3d-bb05-4ced-989b-7fb218fc6705 38 | Measures: 39 | - Score: 1 40 | Description: Common scenarios 41 | - Name: Date of Last IP Review 42 | Id: da4ed776-0365-4d8a-a297-c4e91a14d646 43 | Measures: 44 | - Date: 2020-03-16 45 | - Name: Framework Version 46 | Id: 6c0402b3-f0e3-4bd7-83fe-04bb6dca7924 47 | Measures: 48 | - Framework: netcoreapp3.1 49 | - Name: Associated Work Items 50 | Id: 79b8ff50-7378-4f29-b07c-bcd80746bfd4 51 | Measures: 52 | - Score: 1 53 | Description: Bugs & Features 54 | - Name: Source Code Availability 55 | Id: 30e1b40b-b27d-4631-b38d-3172426593ca 56 | Measures: 57 | - Score: 3 58 | Description: Public OSS 59 | - Name: License 60 | Id: d96b5bdc-62c7-47b6-bcc4-de31127c08b7 61 | Measures: 62 | - Score: 1 63 | Description: Copyright headers in each source file 64 | - Score: 1 65 | Description: License in Source & Packages 66 | - Score: 1 67 | Description: Contributor License Agreement Configured in Repo 68 | - Name: Production Use 69 | Id: 87ee2c3e-b17a-4939-b969-2c9c034d05d7 70 | Measures: 71 | - Score: 0 72 | Description: None 73 | - Name: Insights 74 | Id: 71a02488-2dc9-4d25-94fa-8c2346169f8b 75 | OptOut: true 76 | - Name: Packaging 77 | Id: 547fd9f5-9caf-449f-82d9-4fba9e7ce13a 78 | Measures: 79 | - Score: 1 80 | Description: Packaged 81 | - Score: 1 82 | Description: Versioned 83 | - Name: Deployment 84 | Id: edea4593-d2dd-485b-bc1b-aaaf18f098f9 85 | OptOut: true 86 | - Name: OpenChain 87 | Id: 66efac1a-662c-40cf-b4ec-8b34c29e9fd7 88 | Measures: 89 | - Score: 1 90 | Description: SBOM Available 91 | 92 | --------------------------------------------------------------------------------